Skip to content

Fixes & improvements for Recent Items #10760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Files.App/Filesystem/RecentItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public BitmapImage FileImg
public bool FileIconVis { get; set; }
public bool IsFile { get => Type == StorageItemTypes.File; }
public DateTime LastModified { get; set; }
public byte[] PIDL { get; set; }

public RecentItem()
{
Expand Down Expand Up @@ -56,6 +57,7 @@ public RecentItem(ShellLinkItem linkItem) : base()
FolderImg = linkItem.IsFolder;
FileIconVis = !linkItem.IsFolder;
LastModified = linkItem.ModifiedDate;
PIDL = linkItem.PIDL;
}

/// <summary>
Expand All @@ -71,6 +73,7 @@ public RecentItem(ShellFileItem fileItem) : base()
FolderImg = fileItem.IsFolder;
FileIconVis = !fileItem.IsFolder;
LastModified = fileItem.ModifiedDate;
PIDL = fileItem.PIDL;
}

public async Task LoadRecentItemIcon()
Expand Down
24 changes: 13 additions & 11 deletions src/Files.App/Filesystem/RecentItems.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Files.App.Helpers;
using Files.App.Shell;
using Files.Shared.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
Expand Down Expand Up @@ -51,7 +52,7 @@ public RecentItems()

private async void OnRecentItemsChanged(object? sender, EventArgs e)
{
await ListRecentFilesAsync();
await UpdateRecentFilesAsync();
}

/// <summary>
Expand Down Expand Up @@ -105,6 +106,7 @@ public async Task UpdateRecentFoldersAsync()
public async Task<List<RecentItem>> ListRecentFilesAsync()
{
return (await Win32Shell.GetShellFolderAsync(QuickAccessGuid, "Enumerate", 0, int.MaxValue)).Enumerate
.Where(link => !link.IsFolder)
.Select(link => new RecentItem(link)).ToList();
}

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

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ CancellationToken cancellationToken
else if (FileExtensionHelpers.IsShortcutOrUrlFile(findData.cFileName))
{
var isUrl = FileExtensionHelpers.IsWebLinkFile(findData.cFileName);
var shInfo = await ParseLinkAsync(itemPath);
var shInfo = await FileOperationsHelpers.ParseLinkAsync(itemPath);
if (shInfo is null)
{
return null;
Expand Down Expand Up @@ -388,37 +388,5 @@ CancellationToken cancellationToken
}
return null;
}

private async static Task<ShellLinkItem> ParseLinkAsync(string linkPath)
{
try
{
if (FileExtensionHelpers.IsShortcutFile(linkPath))
{
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
return ShellFolderExtensions.GetShellLinkItem(link);
}
else if (FileExtensionHelpers.IsWebLinkFile(linkPath))
{
var linkUrl = await Win32API.StartSTATask(() =>
{
var ipf = new Url.IUniformResourceLocator();
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Load(linkPath, 0);
ipf.GetUrl(out var retVal);
return retVal;
});
return new ShellLinkItem() { TargetPath = linkUrl };
}
else
{
throw new Exception();
}
}
catch (Exception)
{
// TODO: Log this properly
return await Task.FromResult<ShellLinkItem>(null);
}
}
}
}
29 changes: 23 additions & 6 deletions src/Files.App/Helpers/FileOperationsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -546,33 +546,50 @@ public static void TryCancelOperation(string operationId)
}
}

public static Task<(string, ShellLinkItem)> ParseLinkAsync(string linkPath)
public static async Task<ShellLinkItem?> ParseLinkAsync(string linkPath)
{
if (string.IsNullOrEmpty(linkPath))
return null;

string targetPath = string.Empty;

try
{
if (FileExtensionHelpers.IsShortcutFile(linkPath))
{
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
return Task.FromResult((string.Empty, ShellFolderExtensions.GetShellLinkItem(link)));
targetPath = link.TargetPath;
return ShellFolderExtensions.GetShellLinkItem(link);
}
else if (FileExtensionHelpers.IsWebLinkFile(linkPath))
{
return Win32API.StartSTATask(() =>
targetPath = await Win32API.StartSTATask(() =>
{
var ipf = new Url.IUniformResourceLocator();
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Load(linkPath, 0);
ipf.GetUrl(out var retVal);
return Task.FromResult<(string, ShellLinkItem)>((retVal, null));
return retVal;
});
return string.IsNullOrEmpty(targetPath) ? null : new ShellLinkItem { TargetPath = targetPath };
}
return null;
}
catch (FileNotFoundException ex) // Could not parse shortcut
{
App.Logger?.Warn(ex, ex.Message);
// Return a item containing the invalid target path
return new ShellLinkItem
{
TargetPath = string.IsNullOrEmpty(targetPath) ? string.Empty : targetPath,
InvalidTarget = true
};
}
catch (Exception ex)
{
// Could not parse shortcut
App.Logger.Warn(ex, ex.Message);
return null;
}

return Task.FromResult((string.Empty, new ShellLinkItem()));
}

public static Task<bool> CreateOrUpdateLinkAsync(string linkSavePath, string targetPath, string arguments = "", string workingDirectory = "", bool runAsAdmin = false)
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Helpers/NavigationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public static async Task<bool> OpenPath(string path, IShellPage associatedInstan
{
if (isShortcut)
{
var shInfo = await Win32Shell.ParseLink(path);
var shInfo = await FileOperationsHelpers.ParseLinkAsync(path);

if (shInfo is null)
return false;
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/IAddressToolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class PathNavigationEventArgs
{
public string ItemPath { get; set; }
public string ItemName { get; set; }
public bool IsFile { get; set; }
}

public class ToolbarFlyoutOpenedEventArgs
Expand Down
36 changes: 24 additions & 12 deletions src/Files.App/Shell/ContextMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,7 @@ public async static Task<ContextMenu> GetContextMenuForFiles(string[] filePathLi
{
shellItems.Add(ShellFolderExtensions.GetShellItemFromPathOrPidl(fp));
}
if (!shellItems.Any())
{
return null;
}

using var sf = shellItems[0].Parent; // HP: the items are all in the same folder
Shell32.IContextMenu menu = sf.GetChildrenUIObjects<Shell32.IContextMenu>(default, shellItems.ToArray());
var hMenu = User32.CreatePopupMenu();
menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags);
var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread);
ContextMenu.EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter);
return contextMenu;
return GetContextMenuForFiles(shellItems.ToArray(), flags, owningThread, itemFilter);
}
catch (Exception ex) when (ex is ArgumentException || ex is FileNotFoundException)
{
Expand All @@ -139,6 +128,29 @@ public async static Task<ContextMenu> GetContextMenuForFiles(string[] filePathLi
});
}

public async static Task<ContextMenu> GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, Func<string, bool> itemFilter = null)
{
var owningThread = new ThreadWithMessageQueue();
return await owningThread.PostMethod<ContextMenu>(
() => GetContextMenuForFiles(shellItems, flags, owningThread, itemFilter));
}

private static ContextMenu GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, ThreadWithMessageQueue owningThread, Func<string, bool> itemFilter = null)
{
if (!shellItems.Any())
{
return null;
}

using var sf = shellItems[0].Parent; // HP: the items are all in the same folder
Shell32.IContextMenu menu = sf.GetChildrenUIObjects<Shell32.IContextMenu>(default, shellItems);
var hMenu = User32.CreatePopupMenu();
menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags);
var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread);
ContextMenu.EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter);
return contextMenu;
}

public async static Task<ContextMenu> GetContextMenuForFolder(string folderPath, Shell32.CMF flags, Func<string, bool> itemFilter = null)
{
var owningThread = new ThreadWithMessageQueue();
Expand Down
14 changes: 8 additions & 6 deletions src/Files.App/Shell/ShellFolderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
{
return null;
}
bool isFolder = folderItem.IsFolder && !folderItem.Attributes.HasFlag(ShellItemAttribute.Stream);
// Zip archives are also shell folders, check for STREAM attribute
// Do not use folderItem's Attributes property, throws unimplemented for some shell folders
bool isFolder = folderItem.IsFolder && folderItem.IShellItem?.GetAttributes(Shell32.SFGAO.SFGAO_STREAM) is 0;
var parsingPath = folderItem.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing);
parsingPath ??= folderItem.FileSystemPath; // True path on disk
if (parsingPath is null || !Path.IsPathRooted(parsingPath))
Expand All @@ -61,8 +63,8 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
fileName ??= Path.GetFileName(folderItem.Name); // Original file name
fileName ??= folderItem.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing);
var itemNameOrOriginalPath = folderItem.Name ?? fileName;
string filePath = string.IsNullOrEmpty(Path.GetDirectoryName(parsingPath)) ? // Null if root
parsingPath : Path.Combine(Path.GetDirectoryName(parsingPath), itemNameOrOriginalPath); // In recycle bin "Name" contains original file path + name
string filePath = Path.IsPathRooted(itemNameOrOriginalPath) ?
itemNameOrOriginalPath : parsingPath; // In recycle bin "Name" contains original file path + name
if (!isFolder && !string.IsNullOrEmpty(parsingPath) && Path.GetExtension(parsingPath) is string realExtension && !string.IsNullOrEmpty(realExtension))
{
if (!string.IsNullOrEmpty(fileName) && !fileName.EndsWith(realExtension, StringComparison.OrdinalIgnoreCase))
Expand All @@ -79,14 +81,14 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
var recycleDate = fileTime?.ToDateTime().ToLocalTime() ?? DateTime.Now; // This is LocalTime
fileTime = folderItem.Properties.TryGetProperty<System.Runtime.InteropServices.ComTypes.FILETIME?>(
Ole32.PROPERTYKEY.System.DateModified);
var modifiedDate = fileTime?.ToDateTime().ToLocalTime() ?? folderItem.FileInfo?.LastWriteTime ?? DateTime.Now; // This is LocalTime
var modifiedDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.LastWriteTime) ?? DateTime.Now; // This is LocalTime
fileTime = folderItem.Properties.TryGetProperty<System.Runtime.InteropServices.ComTypes.FILETIME?>(
Ole32.PROPERTYKEY.System.DateCreated);
var createdDate = fileTime?.ToDateTime().ToLocalTime() ?? folderItem.FileInfo?.CreationTime ?? DateTime.Now; // This is LocalTime
var createdDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.CreationTime) ?? DateTime.Now; // This is LocalTime
var fileSizeBytes = folderItem.Properties.TryGetProperty<ulong?>(Ole32.PROPERTYKEY.System.Size);
string fileSize = fileSizeBytes is not null ? folderItem.Properties.GetPropertyString(Ole32.PROPERTYKEY.System.Size) : null;
var fileType = folderItem.Properties.TryGetProperty<string>(Ole32.PROPERTYKEY.System.ItemTypeText);
return new ShellFileItem(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType);
return new ShellFileItem(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType, folderItem.PIDL.GetBytes());
}

public static ShellLinkItem GetShellLinkItem(ShellLink linkItem)
Expand Down
46 changes: 0 additions & 46 deletions src/Files.App/Shell/Win32Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,51 +88,5 @@ public static (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin(
return (false, 0, 0);
}
}

public static async Task<ShellLinkItem?> ParseLink(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return null;

string targetPath = string.Empty;

try
{
if (FileExtensionHelpers.IsShortcutFile(filePath))
{
using var link = new ShellLink(filePath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
targetPath = link.TargetPath;
return ShellFolderExtensions.GetShellLinkItem(link);
}

if (FileExtensionHelpers.IsWebLinkFile(filePath))
{
targetPath = await Win32API.StartSTATask(() =>
{
var ipf = new Url.IUniformResourceLocator();
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile)?.Load(filePath, 0);
ipf.GetUrl(out var retVal);
return retVal;
});

return string.IsNullOrEmpty(targetPath) ? null : new ShellLinkItem { TargetPath = targetPath };
}
}
catch (FileNotFoundException ex) // Could not parse shortcut
{
App.Logger?.Warn(ex, ex.Message);
// Return a item containing the invalid target path
return new ShellLinkItem
{
TargetPath = string.IsNullOrEmpty(targetPath) ? string.Empty : targetPath,
InvalidTarget = true
};
}
catch (Exception ex)
{
App.Logger?.Warn(ex, ex.Message);
}
return null;
}
}
}
Loading