Skip to content

Commit 4c8b9da

Browse files
authored
Add support for browsing shell folders (#9089)
1 parent 82129fe commit 4c8b9da

21 files changed

+816
-220
lines changed

src/Files.FullTrust/ContextMenu.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Files.Shared;
2+
using Files.FullTrust.Helpers;
23
using System;
34
using System.Collections.Generic;
45
using System.Diagnostics;
@@ -93,7 +94,7 @@ public static ContextMenu GetContextMenuForFiles(string[] filePathList, Shell32.
9394
{
9495
foreach (var fp in filePathList.Where(x => !string.IsNullOrEmpty(x)))
9596
{
96-
shellItems.Add(new ShellItem(fp));
97+
shellItems.Add(ShellFolderExtensions.GetShellItemFromPathOrPidl(fp));
9798
}
9899

99100
return GetContextMenuForFiles(shellItems.ToArray(), flags, itemFilter);
@@ -112,7 +113,7 @@ public static ContextMenu GetContextMenuForFiles(string[] filePathList, Shell32.
112113
}
113114
}
114115

115-
private static ContextMenu GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, Func<string, bool> itemFilter = null)
116+
public static ContextMenu GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, Func<string, bool> itemFilter = null)
116117
{
117118
if (shellItems == null || !shellItems.Any())
118119
{

src/Files.FullTrust/Helpers/ShellFolderHelpers.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
4242
}
4343
var parsingPath = folderItem.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing);
4444
parsingPath ??= folderItem.FileSystemPath; // True path on disk
45+
if (parsingPath == null || !Path.IsPathRooted(parsingPath))
46+
{
47+
// Use PIDL as path
48+
parsingPath = $@"\\SHELL\{string.Join("\\", folderItem.PIDL.Select(x => x.GetBytes()).Select(x => Convert.ToBase64String(x, 0, x.Length)))}";
49+
}
4550
folderItem.Properties.TryGetValue<string>(
4651
Ole32.PROPERTYKEY.System.ItemNameDisplay, out var fileName);
4752
fileName ??= Path.GetFileName(folderItem.Name); // Original file name
@@ -103,5 +108,31 @@ public static string GetParsingPath(this ShellItem item)
103108
}
104109
return item.IsFileSystem ? item.FileSystemPath : item.ParsingName;
105110
}
111+
112+
public static bool GetStringAsPidl(string pathOrPidl, out Shell32.PIDL pidl)
113+
{
114+
if (pathOrPidl.StartsWith(@"\\SHELL\", StringComparison.Ordinal))
115+
{
116+
pidl = pathOrPidl.Replace(@"\\SHELL\", "", StringComparison.Ordinal)
117+
.Split('\\', StringSplitOptions.RemoveEmptyEntries)
118+
.Select(x => new Shell32.PIDL(Convert.FromBase64String(x)))
119+
.Aggregate((x, y) => Shell32.PIDL.Combine(x, y));
120+
return true;
121+
}
122+
else
123+
{
124+
pidl = Shell32.PIDL.Null;
125+
return false;
126+
}
127+
}
128+
129+
public static ShellItem GetShellItemFromPathOrPidl(string pathOrPidl)
130+
{
131+
if (GetStringAsPidl(pathOrPidl, out var pidl))
132+
{
133+
return ShellItem.Open(pidl);
134+
}
135+
return ShellItem.Open(pathOrPidl);
136+
}
106137
}
107138
}

src/Files.FullTrust/MessageHandlers/ApplicationLaunchHandler.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Files.Shared.Extensions;
2+
using Files.FullTrust.Helpers;
23
using System;
34
using System.Collections;
45
using System.Collections.Generic;
@@ -134,7 +135,7 @@ private async Task<bool> HandleApplicationLaunch(string application, Dictionary<
134135
{
135136
try
136137
{
137-
return await Win32API.StartSTATask(() =>
138+
var opened = await Win32API.StartSTATask(() =>
138139
{
139140
var split = application.Split('|').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => GetMtpPath(x));
140141
if (split.Count() == 1)
@@ -161,6 +162,20 @@ private async Task<bool> HandleApplicationLaunch(string application, Dictionary<
161162
}
162163
return true;
163164
});
165+
if (!opened)
166+
{
167+
if (application.StartsWith(@"\\SHELL\", StringComparison.Ordinal))
168+
{
169+
opened = await Win32API.StartSTATask(() =>
170+
{
171+
using var si = ShellFolderExtensions.GetShellItemFromPathOrPidl(application);
172+
using var cMenu = ContextMenu.GetContextMenuForFiles(new[] { si }, Shell32.CMF.CMF_DEFAULTONLY);
173+
cMenu?.InvokeItem(cMenu?.Items.FirstOrDefault().ID ?? -1);
174+
return true;
175+
});
176+
}
177+
}
178+
return opened;
164179
}
165180
catch (Win32Exception)
166181
{

src/Files.FullTrust/MessageHandlers/Win32MessageHandler.cs

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.IO.Pipes;
1111
using System.Linq;
1212
using System.Runtime.Versioning;
13-
using System.Threading;
1413
using System.Threading.Tasks;
1514
using Vanara.PInvoke;
1615
using Vanara.Windows.Shell;
@@ -25,6 +24,14 @@ public class Win32MessageHandler : Disposable, IMessageHandler
2524
{
2625
private IList<FileSystemWatcher> dirWatchers;
2726
private PipeStream connection;
27+
private ShellFolder controlPanel, controlPanelCategoryView;
28+
29+
public Win32MessageHandler()
30+
{
31+
dirWatchers = new List<FileSystemWatcher>();
32+
controlPanel = new ShellFolder(Shell32.KNOWNFOLDERID.FOLDERID_ControlPanelFolder);
33+
controlPanelCategoryView = new ShellFolder("::{26EE0668-A00A-44D7-9371-BEB064C98683}");
34+
}
2835

2936
public void Initialize(PipeStream connection)
3037
{
@@ -33,8 +40,6 @@ public void Initialize(PipeStream connection)
3340
DetectIsSetAsDefaultFileManager();
3441
DetectIsSetAsOpenFileDialog();
3542
ApplicationData.Current.LocalSettings.Values["TEMP"] = Environment.GetEnvironmentVariable("TEMP");
36-
37-
dirWatchers = new List<FileSystemWatcher>();
3843
}
3944

4045
private static void DetectIsSetAsDefaultFileManager()
@@ -94,41 +99,72 @@ public async Task ParseArgumentsAsync(PipeStream connection, Dictionary<string,
9499
}, message.Get("RequestID", (string)null));
95100
break;
96101

102+
case "ShellItem":
103+
var itemPath = (string)message["item"];
104+
var siAction = (string)message["action"];
105+
var siResponseEnum = new ValueSet();
106+
var item = await Win32API.StartSTATask(() =>
107+
{
108+
using var shellItem = ShellFolderExtensions.GetShellItemFromPathOrPidl(itemPath);
109+
return ShellFolderExtensions.GetShellFileItem(shellItem);
110+
});
111+
siResponseEnum.Add("Item", JsonConvert.SerializeObject(item));
112+
await Win32API.SendMessageAsync(connection, siResponseEnum, message.Get("RequestID", (string)null));
113+
break;
114+
97115
case "ShellFolder":
98-
// Enumerate shell folder contents and send response to UWP
99116
var folderPath = (string)message["folder"];
100-
var responseEnum = new ValueSet();
101-
var folderContentsList = await Win32API.StartSTATask(() =>
117+
if (folderPath.StartsWith("::{", StringComparison.Ordinal))
118+
{
119+
folderPath = $"shell:{folderPath}";
120+
}
121+
var sfAction = (string)message["action"];
122+
var fromIndex = (int)message.Get("from", 0L);
123+
var maxItems = (int)message.Get("count", (long)int.MaxValue);
124+
var sfResponseEnum = new ValueSet();
125+
var (folder, folderContentsList) = await Win32API.StartSTATask(() =>
102126
{
103127
var flc = new List<ShellFileItem>();
128+
var folder = (ShellFileItem)null;
104129
try
105130
{
106-
using var shellFolder = new ShellFolder(folderPath);
107-
108-
foreach (var folderItem in shellFolder)
131+
using var shellFolder = ShellFolderExtensions.GetShellItemFromPathOrPidl(folderPath) as ShellFolder;
132+
folder = ShellFolderExtensions.GetShellFileItem(shellFolder);
133+
if ((controlPanel.PIDL.IsParentOf(shellFolder.PIDL, false) || controlPanelCategoryView.PIDL.IsParentOf(shellFolder.PIDL, false))
134+
&& !shellFolder.Any())
109135
{
110-
try
111-
{
112-
var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem);
113-
flc.Add(shellFileItem);
114-
}
115-
catch (FileNotFoundException)
116-
{
117-
// Happens if files are being deleted
118-
}
119-
finally
136+
// Return null to force open unsupported items in explorer
137+
// Only if inside control panel and folder appears empty
138+
return (null, flc);
139+
}
140+
if (sfAction == "Enumerate")
141+
{
142+
foreach (var folderItem in shellFolder.Skip(fromIndex).Take(maxItems))
120143
{
121-
folderItem.Dispose();
144+
try
145+
{
146+
var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem);
147+
flc.Add(shellFileItem);
148+
}
149+
catch (FileNotFoundException)
150+
{
151+
// Happens if files are being deleted
152+
}
153+
finally
154+
{
155+
folderItem.Dispose();
156+
}
122157
}
123158
}
124159
}
125160
catch
126161
{
127162
}
128-
return flc;
163+
return (folder, flc);
129164
});
130-
responseEnum.Add("Enumerate", JsonConvert.SerializeObject(folderContentsList));
131-
await Win32API.SendMessageAsync(connection, responseEnum, message.Get("RequestID", (string)null));
165+
sfResponseEnum.Add("Folder", JsonConvert.SerializeObject(folder));
166+
sfResponseEnum.Add("Enumerate", JsonConvert.SerializeObject(folderContentsList));
167+
await Win32API.SendMessageAsync(connection, sfResponseEnum, message.Get("RequestID", (string)null));
132168
break;
133169

134170
case "GetFolderIconsFromDLL":
@@ -349,6 +385,8 @@ protected override void Dispose(bool disposing)
349385
{
350386
watcher.Dispose();
351387
}
388+
controlPanel.Dispose();
389+
controlPanelCategoryView.Dispose();
352390
}
353391
}
354392
}

src/Files.FullTrust/Win32API.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Files.Shared;
22
using Files.Shared.Extensions;
3+
using Files.FullTrust.Helpers;
34
using Newtonsoft.Json;
45
using System;
56
using System.Collections.Generic;
@@ -168,7 +169,7 @@ public static (string icon, string overlay) GetFileIconAndOverlay(string path, i
168169

169170
if (!onlyGetOverlay)
170171
{
171-
using var shellItem = new Vanara.Windows.Shell.ShellItem(path);
172+
using var shellItem = ShellFolderExtensions.GetShellItemFromPathOrPidl(path);
172173
if (shellItem.IShellItem is Shell32.IShellItemImageFactory fctry)
173174
{
174175
var flags = Shell32.SIIGBF.SIIGBF_BIGGERSIZEOK;
@@ -190,12 +191,10 @@ public static (string icon, string overlay) GetFileIconAndOverlay(string path, i
190191
if (getOverlay)
191192
{
192193
var shfi = new Shell32.SHFILEINFO();
193-
var ret = Shell32.SHGetFileInfo(
194-
path,
195-
0,
196-
ref shfi,
197-
Shell32.SHFILEINFO.Size,
198-
Shell32.SHGFI.SHGFI_OVERLAYINDEX | Shell32.SHGFI.SHGFI_ICON | Shell32.SHGFI.SHGFI_SYSICONINDEX | Shell32.SHGFI.SHGFI_ICONLOCATION);
194+
var flags = Shell32.SHGFI.SHGFI_OVERLAYINDEX | Shell32.SHGFI.SHGFI_ICON | Shell32.SHGFI.SHGFI_SYSICONINDEX | Shell32.SHGFI.SHGFI_ICONLOCATION;
195+
var ret = ShellFolderExtensions.GetStringAsPidl(path, out var pidl) ?
196+
Shell32.SHGetFileInfo(pidl, 0, ref shfi, Shell32.SHFILEINFO.Size, Shell32.SHGFI.SHGFI_PIDL | flags) :
197+
Shell32.SHGetFileInfo(path, 0, ref shfi, Shell32.SHFILEINFO.Size, flags);
199198
if (ret == IntPtr.Zero)
200199
{
201200
return (iconStr, null);
@@ -598,8 +597,8 @@ public static void OpenFolderInExistingShellWindow(string folderPath)
598597
var folderPidl = new Shell32.PIDL(IntPtr.Zero);
599598
if (folder.GetCurFolder(ref folderPidl).Succeeded)
600599
{
601-
if (Shell32.ILIsParent(folderPidl.DangerousGetHandle(), targetFolder.PIDL.DangerousGetHandle(), true) ||
602-
Shell32.ILIsEqual(folderPidl.DangerousGetHandle(), controlPanelCategoryView.PIDL.DangerousGetHandle()))
600+
if (folderPidl.IsParentOf(targetFolder.PIDL.DangerousGetHandle(), true) ||
601+
folderPidl.Equals(controlPanelCategoryView.PIDL))
603602
{
604603
if (shellBrowser.BrowseObject(targetFolder.PIDL.DangerousGetHandle(), Shell32.SBSP.SBSP_SAMEBROWSER | Shell32.SBSP.SBSP_ABSOLUTE).Succeeded)
605604
{

src/Files.Uwp/CommandLine/CommandLineParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ private static ParsedCommands ParseSplitArguments(List<KeyValuePair<string, stri
4848
//case "-Cmdless":
4949
try
5050
{
51-
if (kvp.Value.StartsWith("::{", StringComparison.Ordinal) || kvp.Value.StartsWith("shell:", StringComparison.Ordinal))
51+
if (kvp.Value.StartsWith("::{", StringComparison.Ordinal) || kvp.Value.StartsWith("shell:", StringComparison.OrdinalIgnoreCase))
5252
{
5353
command.Type = ParsedCommandType.ExplorerShellCommand;
5454
}

src/Files.Uwp/Files.Uwp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@
176176
<Compile Include="Filesystem\StorageItems\BaseQueryResults.cs" />
177177
<Compile Include="Filesystem\StorageItems\FtpStorageFile.cs" />
178178
<Compile Include="Filesystem\StorageItems\FtpStorageFolder.cs" />
179+
<Compile Include="Filesystem\StorageItems\ShellStorageFile.cs" />
180+
<Compile Include="Filesystem\StorageItems\ShellStorageFolder.cs" />
179181
<Compile Include="Filesystem\StorageItems\StreamWithContentType.cs" />
180182
<Compile Include="Filesystem\StorageItems\SystemStorageFile.cs" />
181183
<Compile Include="Filesystem\StorageItems\SystemStorageFolder.cs" />

src/Files.Uwp/Filesystem/BaseStorage/BaseStorageFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public IAsyncOperation<StorageItemThumbnail> GetScaledImageAsThumbnailAsync(Thum
9090

9191
public static IAsyncOperation<BaseStorageFile> GetFileFromPathAsync(string path)
9292
=> AsyncInfo.Run(async (cancellationToken)
93-
=> await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path)
93+
=> await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await ShellStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path)
9494
);
9595

9696
public async Task<string> ReadTextAsync(int maxLength = -1)

src/Files.Uwp/Filesystem/BaseStorage/BaseStorageFolder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ IAsyncOperation<StorageFolder> IStorageFolder.GetFolderAsync(string name)
7878

7979
public static IAsyncOperation<BaseStorageFolder> GetFolderFromPathAsync(string path)
8080
=> AsyncInfo.Run(async (cancellationToken)
81-
=> await ZipStorageFolder.FromPathAsync(path) ?? await FtpStorageFolder.FromPathAsync(path) ?? await SystemStorageFolder.FromPathAsync(path)
81+
=> await ZipStorageFolder.FromPathAsync(path) ?? await FtpStorageFolder.FromPathAsync(path) ?? await ShellStorageFolder.FromPathAsync(path) ?? await SystemStorageFolder.FromPathAsync(path)
8282
);
8383

8484
public abstract IAsyncOperation<IReadOnlyList<BaseStorageFolder>> GetFoldersAsync();

src/Files.Uwp/Filesystem/ListedItem.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ public void SetDefaultIcon(BitmapImage img)
441441

442442
public class RecycleBinItem : ListedItem
443443
{
444-
public RecycleBinItem(string folderRelativeId, string returnFormat) : base(folderRelativeId, returnFormat)
444+
public RecycleBinItem(string folderRelativeId, string returnFormat = null) : base(folderRelativeId, returnFormat)
445445
{
446446
}
447447

@@ -506,7 +506,7 @@ public FtpItem(FtpListItem item, string folder, string dateReturnFormat = null)
506506

507507
public class ShortcutItem : ListedItem
508508
{
509-
public ShortcutItem(string folderRelativeId, string returnFormat) : base(folderRelativeId, returnFormat)
509+
public ShortcutItem(string folderRelativeId, string returnFormat = null) : base(folderRelativeId, returnFormat)
510510
{
511511
}
512512

@@ -541,7 +541,7 @@ public override string ItemName
541541

542542
public class ZipItem : ListedItem
543543
{
544-
public ZipItem(string folderRelativeId, string returnFormat) : base(folderRelativeId, returnFormat)
544+
public ZipItem(string folderRelativeId, string returnFormat = null) : base(folderRelativeId, returnFormat)
545545
{
546546
}
547547

0 commit comments

Comments
 (0)