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
716namespace 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 ( ) )
0 commit comments