Skip to content

Commit 7464507

Browse files
committed
Init
1 parent 360b496 commit 7464507

File tree

10 files changed

+232
-62
lines changed

10 files changed

+232
-62
lines changed

src/Files.App.CsWin32/NativeMethods.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,7 @@ PSGetPropertyKeyFromName
135135
ShellExecuteEx
136136
CoTaskMemFree
137137
QueryDosDevice
138+
BHID_EnumItems
139+
BHID_SFUIObject
140+
IContextMenu
141+
CMF_OPTIMIZEFORINVOKE

src/Files.App/Actions/Sidebar/PinFolderToSidebarAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public async Task ExecuteAsync(object? parameter = null)
4141
}
4242
else if (context.Folder is not null)
4343
{
44-
await service.PinToSidebarAsync(context.Folder.ItemPath);
44+
await service.PinToSidebarAsync([context.Folder.ItemPath]);
4545
}
4646
}
4747

src/Files.App/Actions/Sidebar/UnpinFolderToSidebarAction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task ExecuteAsync(object? parameter = null)
3838
}
3939
else if (context.Folder is not null)
4040
{
41-
await service.UnpinFromSidebarAsync(context.Folder.ItemPath);
41+
await service.UnpinFromSidebarAsync([context.Folder.ItemPath]);
4242
}
4343
}
4444

src/Files.App/Data/Contracts/IQuickAccessService.cs

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,14 @@ namespace Files.App.Data.Contracts
55
{
66
public interface IQuickAccessService
77
{
8-
/// <summary>
9-
/// Gets the list of quick access items
10-
/// </summary>
11-
/// <returns></returns>
128
Task<IEnumerable<ShellFileItem>> GetPinnedFoldersAsync();
139

14-
/// <summary>
15-
/// Pins a folder to the quick access list
16-
/// </summary>
17-
/// <param name="folderPath">The folder to pin</param>
18-
/// <returns></returns>
19-
Task PinToSidebarAsync(string folderPath);
20-
21-
/// <summary>
22-
/// Pins folders to the quick access list
23-
/// </summary>
24-
/// <param name="folderPaths">The array of folders to pin</param>
25-
/// <returns></returns>
2610
Task PinToSidebarAsync(string[] folderPaths);
2711

28-
/// <summary>
29-
/// Unpins a folder from the quick access list
30-
/// </summary>
31-
/// <param name="folderPath">The folder to unpin</param>
32-
/// <returns></returns>
33-
Task UnpinFromSidebarAsync(string folderPath);
34-
35-
/// <summary>
36-
/// Unpins folders from the quick access list
37-
/// </summary>
38-
/// <param name="folderPaths">The array of folders to unpin</param>
39-
/// <returns></returns>
4012
Task UnpinFromSidebarAsync(string[] folderPaths);
4113

42-
/// <summary>
43-
/// Checks if a folder is pinned to the quick access list
44-
/// </summary>
45-
/// <param name="folderPath">The path of the folder</param>
46-
/// <returns>true if the item is pinned</returns>
4714
bool IsItemPinned(string folderPath);
4815

49-
/// <summary>
50-
/// Saves a state of pinned folder items in the sidebar
51-
/// </summary>
52-
/// <param name="items">The array of items to save</param>
53-
/// <returns></returns>
5416
Task SaveAsync(string[] items);
5517
}
5618
}

src/Files.App/Data/Models/PinnedFoldersManager.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections.Specialized;
55
using System.IO;
6-
using System.Text.Json.Serialization;
76

87
namespace Files.App.Data.Models
98
{
@@ -20,7 +19,6 @@ public sealed class PinnedFoldersManager
2019

2120
public readonly List<INavigationControlItem> _PinnedFolderItems = [];
2221

23-
[JsonIgnore]
2422
public IReadOnlyList<INavigationControlItem> PinnedFolderItems
2523
{
2624
get

src/Files.App/Services/Windows/WindowsQuickAccessService.cs

Lines changed: 219 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,238 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using Files.App.Utils.Shell;
5-
using Files.App.UserControls.Widgets;
4+
using Microsoft.Extensions.Logging;
5+
using System.Collections.Specialized;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using Windows.Win32;
9+
using Windows.Win32.Foundation;
10+
using Windows.Win32.System.Com;
11+
using Windows.Win32.System.SystemServices;
12+
using Windows.Win32.UI.Shell;
13+
using Windows.Win32.UI.WindowsAndMessaging;
14+
using WinRT;
615

716
namespace Files.App.Services
817
{
918
internal sealed class QuickAccessService : IQuickAccessService
1019
{
11-
// Quick access shell folder (::{679f85cb-0220-4080-b29b-5540cc05aab6}) contains recent files
12-
// which are unnecessary for getting pinned folders, so we use frequent places shell folder instead.
13-
private readonly static string guid = "::{3936e9e4-d92c-4eee-a85a-bc16d5ea0819}";
20+
// Fields
21+
22+
private readonly SystemIO.FileSystemWatcher? _watcher;
23+
24+
// Properties
25+
26+
private readonly List<INavigationControlItem> _PinnedFolders = [];
27+
/// <inheritdoc/>
28+
public IReadOnlyList<INavigationControlItem> PinnedFolders
29+
{
30+
get
31+
{
32+
lock (_PinnedFolders)
33+
return _PinnedFolders.ToList().AsReadOnly();
34+
}
35+
}
36+
37+
/// <inheritdoc/>
38+
public event EventHandler<NotifyCollectionChangedEventArgs>? PinnedFoldersChanged;
39+
40+
public QuickAccessService()
41+
{
42+
_watcher = new()
43+
{
44+
Path = SystemIO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Recent), "AutomaticDestinations"),
45+
Filter = "f01b4d95cf55d32a.automaticDestinations-ms",
46+
NotifyFilter = SystemIO.NotifyFilters.DirectoryName | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.LastWrite,
47+
};
48+
49+
_watcher.Changed += Watcher_Changed;
50+
_watcher.Deleted += Watcher_Changed;
51+
_watcher.EnableRaisingEvents = true;
52+
}
53+
54+
public async Task<bool> UpdatePinnedFoldersAsync()
55+
{
56+
return await Task.Run(UpdatePinnedFolders);
57+
58+
unsafe bool UpdatePinnedFolders()
59+
{
60+
try
61+
{
62+
HRESULT hr = default;
63+
64+
string szFolderShellPath = "Shell:::{3936E9E4-D92C-4EEE-A85A-BC16D5EA0819}";
65+
66+
// Get IShellItem of the shell folder
67+
var shellItemIid = typeof(IShellItem).GUID;
68+
using ComPtr<IShellItem> pFolderShellItem = default;
69+
fixed (char* pszFolderShellPath = szFolderShellPath)
70+
hr = PInvoke.SHCreateItemFromParsingName(pszFolderShellPath, null, &shellItemIid, (void**)pFolderShellItem.GetAddressOf());
71+
72+
// Get IEnumShellItems of the quick access shell folder
73+
var enumItemsBHID = PInvoke.BHID_EnumItems;
74+
Guid enumShellItemIid = typeof(IEnumShellItems).GUID;
75+
using ComPtr<IEnumShellItems> pEnumShellItems = default;
76+
hr = pFolderShellItem.Get()->BindToHandler(null, &enumItemsBHID, &enumShellItemIid, (void**)pEnumShellItems.GetAddressOf());
77+
78+
// Enumerate recent items and populate the list
79+
int index = 0;
80+
List<LocationItem> items = [];
81+
using ComPtr<IShellItem> pShellItem = default;
82+
while (pEnumShellItems.Get()->Next(1, pShellItem.GetAddressOf()) == HRESULT.S_OK)
83+
{
84+
// Get top 20 items
85+
if (index is 20)
86+
break;
87+
88+
// Get whether the item is pined or not
89+
using ComPtr<IShellItem2> pShellItem2 = pShellItem.As<IShellItem2>(typeof(IShellItem2).GUID);
90+
hr = PInvoke.PSGetPropertyKeyFromName("System.Home.IsPinned", out var propertyKey);
91+
hr = pShellItem2.Get()->GetString(propertyKey, out var szPropertyValue);
92+
if (bool.TryParse(szPropertyValue.ToString(), out var isPinned) && !isPinned)
93+
continue;
94+
95+
// Get the target path
96+
pShellItem.Get()->GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out var szDisplayName);
97+
var targetPath = szDisplayName.ToString();
98+
PInvoke.CoTaskMemFree(szDisplayName.Value);
99+
100+
// Get the display name
101+
pShellItem.Get()->GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY, out szDisplayName);
102+
var fileName = szDisplayName.ToString();
103+
PInvoke.CoTaskMemFree(szDisplayName.Value);
104+
105+
items.Add(new()
106+
{
107+
Path = targetPath,
108+
Text = fileName,
109+
});
110+
111+
index++;
112+
}
113+
114+
if (items.Count is 0)
115+
return false;
116+
117+
var snapshot = PinnedFolders;
118+
119+
lock (_PinnedFolders)
120+
{
121+
_PinnedFolders.Clear();
122+
_PinnedFolders.AddRange(items);
123+
}
124+
125+
//var eventArgs = GetChangedActionEventArgs(snapshot, items);
126+
127+
//PinnedFoldersChanged?.Invoke(this, eventArgs);
128+
129+
return true;
130+
}
131+
catch
132+
{
133+
return false;
134+
}
135+
}
136+
}
137+
138+
public async Task<bool> PinFolderAsync(string path)
139+
{
140+
return await Task.Run(() =>
141+
{
142+
return PinFolder(path);
143+
});
144+
145+
unsafe bool PinFolder(string path)
146+
{
147+
HRESULT hr = default;
148+
149+
// Get IShellItem of the shell folder
150+
var shellItemIid = typeof(IShellItem).GUID;
151+
using ComPtr<IShellItem> pShellItem = default;
152+
fixed (char* pszFolderShellPath = path)
153+
hr = PInvoke.SHCreateItemFromParsingName(pszFolderShellPath, null, &shellItemIid, (void**)pShellItem.GetAddressOf());
154+
155+
var bhid = PInvoke.BHID_SFUIObject;
156+
var contextMenuIid = typeof(IContextMenu).GUID;
157+
using ComPtr<IContextMenu> pContextMenu = default;
158+
hr = pShellItem.Get()->BindToHandler(null, &bhid, &contextMenuIid, (void**)pContextMenu.GetAddressOf());
159+
HMENU hMenu = PInvoke.CreatePopupMenu();
160+
hr = pContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 0x7FFF, PInvoke.CMF_OPTIMIZEFORINVOKE);
161+
162+
CMINVOKECOMMANDINFO cmi = default;
163+
cmi.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO);
164+
cmi.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE;
165+
166+
fixed (byte* pVerb = Encoding.ASCII.GetBytes("pintohome"))
167+
{
168+
cmi.lpVerb = new(pVerb);
169+
hr = pContextMenu.Get()->InvokeCommand(cmi);
170+
if (hr != HRESULT.S_OK)
171+
return false;
172+
}
173+
174+
return true;
175+
}
176+
}
177+
178+
public async Task<bool> UnpinFolderAsync(string path)
179+
{
180+
return await Task.Run(() =>
181+
{
182+
return UnpinFolder(path);
183+
});
184+
185+
unsafe bool UnpinFolder(string path)
186+
{
187+
HRESULT hr = default;
188+
189+
// Get IShellItem of the shell folder
190+
var shellItemIid = typeof(IShellItem).GUID;
191+
using ComPtr<IShellItem> pShellItem = default;
192+
fixed (char* pszFolderShellPath = path)
193+
hr = PInvoke.SHCreateItemFromParsingName(pszFolderShellPath, null, &shellItemIid, (void**)pShellItem.GetAddressOf());
194+
195+
var bhid = PInvoke.BHID_SFUIObject;
196+
var contextMenuIid = typeof(IContextMenu).GUID;
197+
using ComPtr<IContextMenu> pContextMenu = default;
198+
hr = pShellItem.Get()->BindToHandler(null, &bhid, &contextMenuIid, (void**)pContextMenu.GetAddressOf());
199+
HMENU hMenu = PInvoke.CreatePopupMenu();
200+
hr = pContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 0x7FFF, PInvoke.CMF_OPTIMIZEFORINVOKE);
201+
202+
CMINVOKECOMMANDINFO cmi = default;
203+
cmi.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO);
204+
cmi.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE;
205+
206+
fixed (byte* pVerb = Encoding.ASCII.GetBytes("unpinfromhome"))
207+
{
208+
cmi.lpVerb = new(pVerb);
209+
hr = pContextMenu.Get()->InvokeCommand(cmi);
210+
if (hr != HRESULT.S_OK)
211+
return false;
212+
}
213+
214+
return true;
215+
}
216+
}
217+
218+
private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e)
219+
{
220+
_ = UpdatePinnedFoldersAsync();
221+
}
222+
223+
///// ---------------------------------------------------------------------------------------------
14224

15225
public async Task<IEnumerable<ShellFileItem>> GetPinnedFoldersAsync()
16226
{
17-
var result = (await Win32Helper.GetShellFolderAsync(guid, false, true, 0, int.MaxValue, "System.Home.IsPinned")).Enumerate
227+
var result = (await Win32Helper.GetShellFolderAsync("::{3936e9e4-d92c-4eee-a85a-bc16d5ea0819}", false, true, 0, int.MaxValue, "System.Home.IsPinned")).Enumerate
18228
.Where(link => link.IsFolder);
19229
return result;
20230
}
21231

22-
public Task PinToSidebarAsync(string folderPath) => PinToSidebarAsync(new[] { folderPath });
23-
24232
public Task PinToSidebarAsync(string[] folderPaths) => PinToSidebarAsync(folderPaths, true);
25233

234+
public Task UnpinFromSidebarAsync(string[] folderPaths) => UnpinFromSidebarAsync(folderPaths, true);
235+
26236
private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget)
27237
{
28238
foreach (string folderPath in folderPaths)
@@ -33,15 +243,11 @@ private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAcc
33243
App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(folderPaths, true));
34244
}
35245

36-
public Task UnpinFromSidebarAsync(string folderPath) => UnpinFromSidebarAsync(new[] { folderPath });
37-
38-
public Task UnpinFromSidebarAsync(string[] folderPaths) => UnpinFromSidebarAsync(folderPaths, true);
39-
40246
private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget)
41247
{
42248
Type? shellAppType = Type.GetTypeFromProgID("Shell.Application");
43249
object? shell = Activator.CreateInstance(shellAppType);
44-
dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [$"shell:{guid}"]);
250+
dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [$"shell:::{{3936e9e4-d92c-4eee-a85a-bc16d5ea0819}}"]);
45251

46252
if (folderPaths.Length == 0)
47253
folderPaths = (await GetPinnedFoldersAsync())

src/Files.App/Utils/Global/QuickAccessManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task InitializeAsync()
4444
PinnedItemsModified += Model.LoadAsync;
4545

4646
if (!Model.PinnedFolders.Contains(Constants.UserEnvironmentPaths.RecycleBinPath) && SystemInformation.Instance.IsFirstRun)
47-
await QuickAccessService.PinToSidebarAsync(Constants.UserEnvironmentPaths.RecycleBinPath);
47+
await QuickAccessService.PinToSidebarAsync([Constants.UserEnvironmentPaths.RecycleBinPath]);
4848

4949
await Model.LoadAsync();
5050
}

src/Files.App/ViewModels/UserControls/SidebarViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,7 @@ private void PinItem()
879879
private void UnpinItem()
880880
{
881881
if (rightClickedItem.Section == SectionType.Pinned || rightClickedItem is DriveItem)
882-
_ = QuickAccessService.UnpinFromSidebarAsync(rightClickedItem.Path);
882+
_ = QuickAccessService.UnpinFromSidebarAsync([rightClickedItem.Path]);
883883
}
884884

885885
private void HideSection()
@@ -1283,7 +1283,7 @@ private async Task HandleLocationItemDroppedAsync(LocationItem locationItem, Ite
12831283
foreach (var item in storageItems)
12841284
{
12851285
if (item.ItemType == FilesystemItemType.Directory && !SidebarPinnedModel.PinnedFolders.Contains(item.Path))
1286-
await QuickAccessService.PinToSidebarAsync(item.Path);
1286+
await QuickAccessService.PinToSidebarAsync([item.Path]);
12871287
}
12881288
}
12891289
else

0 commit comments

Comments
 (0)