Skip to content

Commit d6741fe

Browse files
committed
Init
1 parent 7b4325a commit d6741fe

File tree

9 files changed

+454
-16
lines changed

9 files changed

+454
-16
lines changed

src/Files.App.CsWin32/NativeMethods.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,12 @@ GetMenuItemCount
236236
GetMenuItemInfo
237237
IsWow64Process2
238238
GetCurrentProcess
239+
SHChangeNotifyRegister
240+
SHChangeNotifyDeregister
241+
SHChangeNotification_Lock
242+
SHChangeNotification_Unlock
243+
CoInitialize
244+
CoUninitialize
245+
PostQuitMessage
246+
HWND_MESSAGE
247+
SHCNE_ID

src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Files.App.Storage
77
{
8-
public unsafe interface IWindowsFolder : IWindowsStorable, IChildFolder
8+
public unsafe interface IWindowsFolder : IWindowsStorable, IChildFolder, IMutableFolder
99
{
1010
/// <summary>
1111
/// Gets or sets the cached <see cref="IContextMenu"/> for the ShellNew context menu.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using Windows.Foundation;
5+
6+
namespace Files.App.Storage
7+
{
8+
public interface IWindowsFolderWatcher : IFolderWatcher
9+
{
10+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? EventOccurred;
11+
12+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAssocChanged; // SHCNE_ASSOCCHANGED
13+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAttributesChanged; // SHCNE_ATTRIBUTES
14+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemImageUpdated; // SHCNE_UPDATEIMAGE
15+
16+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileRenamed; // SHCNE_RENAMEITEM
17+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileCreated; // SHCNE_CREATE
18+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileDeleted; // SHCNE_DELETE
19+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileUpdated; // SHCNE_UPDATEITEM
20+
21+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderRenamed; // SHCNE_RENAMEFOLDER
22+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderCreated; // SHCNE_MKDIR
23+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderDeleted; // SHCNE_RMDIR
24+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderUpdated; // SHCNE_UPDATEDIR
25+
26+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaInserted; // SHCNE_MEDIAINSERTED
27+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaRemoved; // SHCNE_MEDIAREMOVED
28+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveRemoved; // SHCNE_DRIVEREMOVED
29+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAdded; // SHCNE_DRIVEADD
30+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAddedViaGUI; // SHCNE_DRIVEADDGUI
31+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FreeSpaceUpdated; // SHCNE_FREESPACE
32+
33+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStarted; // SHCNE_NETSHARE
34+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStopped; // SHCNE_NETUNSHARE
35+
36+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DisconnectedFromServer; // SHCNE_SERVERDISCONNECT
37+
38+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ExtendedEventOccurred; // SHCNE_EXTENDED_EVENT
39+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SystemInterruptOccurred; // SHCNE_INTERRUPT
40+
}
41+
}

src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public WindowsFolder(Guid folderId)
4444
ThisPtr = pShellItem;
4545
}
4646

47+
/// <inheritdoc/>
4748
public IAsyncEnumerable<IStorableChild> GetItemsAsync(StorableType type = StorableType.All, CancellationToken cancellationToken = default)
4849
{
4950
using ComPtr<IEnumShellItems> pEnumShellItems = default;
@@ -75,6 +76,14 @@ public IAsyncEnumerable<IStorableChild> GetItemsAsync(StorableType type = Storab
7576
return childItems.ToAsyncEnumerable();
7677
}
7778

79+
/// <inheritdoc/>
80+
public Task<IFolderWatcher> GetFolderWatcherAsync(CancellationToken cancellationToken = default)
81+
{
82+
IFolderWatcher watcher = new WindowsFolderWatcher(this);
83+
return Task.FromResult(watcher);
84+
}
85+
86+
/// <inheritdoc/>
7887
public override void Dispose()
7988
{
8089
base.Dispose();
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using Files.Shared.Utils;
5+
using System.Collections.Specialized;
6+
using System.Runtime.InteropServices;
7+
using Windows.Foundation;
8+
using Windows.Win32;
9+
using Windows.Win32.Foundation;
10+
using Windows.Win32.System.Com;
11+
using Windows.Win32.UI.Shell;
12+
using Windows.Win32.UI.Shell.Common;
13+
using Windows.Win32.UI.WindowsAndMessaging;
14+
15+
namespace Files.App.Storage
16+
{
17+
/// <summary>
18+
/// Represents an implementation of <see cref="IFolderWatcher"/> that uses Windows Shell notifications to watch for changes in a folder.
19+
/// </summary>
20+
public unsafe partial class WindowsFolderWatcher : IWindowsFolderWatcher
21+
{
22+
// Fields
23+
24+
private const uint WM_NOTIFYFOLDERCHANGE = PInvoke.WM_APP | 0x0001U;
25+
private readonly WNDPROC _wndProc;
26+
27+
private uint _watcherRegID = 0U;
28+
private ITEMIDLIST* _folderPidl = default;
29+
private Debouncer _debouncer;
30+
31+
// Properties
32+
33+
public IMutableFolder Folder { get; private set; }
34+
35+
// Events
36+
37+
public event NotifyCollectionChangedEventHandler? CollectionChanged;
38+
39+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? EventOccurred;
40+
41+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAssocChanged; // SHCNE_ASSOCCHANGED
42+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAttributesChanged; // SHCNE_ATTRIBUTES
43+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemImageUpdated; // SHCNE_UPDATEIMAGE
44+
45+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileRenamed; // SHCNE_RENAMEITEM
46+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileCreated; // SHCNE_CREATE
47+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileDeleted; // SHCNE_DELETE
48+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileUpdated; // SHCNE_UPDATEITEM
49+
50+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderRenamed; // SHCNE_RENAMEFOLDER
51+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderCreated; // SHCNE_MKDIR
52+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderDeleted; // SHCNE_RMDIR
53+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderUpdated; // SHCNE_UPDATEDIR
54+
55+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaInserted; // SHCNE_MEDIAINSERTED
56+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaRemoved; // SHCNE_MEDIAREMOVED
57+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveRemoved; // SHCNE_DRIVEREMOVED
58+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAdded; // SHCNE_DRIVEADD
59+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAddedViaGUI; // SHCNE_DRIVEADDGUI
60+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FreeSpaceUpdated; // SHCNE_FREESPACE
61+
62+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStarted; // SHCNE_NETSHARE
63+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStopped; // SHCNE_NETUNSHARE
64+
65+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DisconnectedFromServer; // SHCNE_SERVERDISCONNECT
66+
67+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ExtendedEventOccurred; // SHCNE_EXTENDED_EVENT
68+
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SystemInterruptOccurred; // SHCNE_INTERRUPT
69+
70+
// Constructor
71+
72+
/// <summary>Initializes a new instance of the <see cref="WindowsFolderWatcher"/> class.</summary>
73+
/// <param name="folder">Specifies the folder to be monitored for changes.</param>
74+
public WindowsFolderWatcher(WindowsFolder folder, int debounceMilliseconds = 1000)
75+
{
76+
_debouncer = new(debounceMilliseconds);
77+
78+
Folder = folder;
79+
80+
fixed (char* pszClassName = $"FolderWatcherWindowClass{Guid.NewGuid():B}")
81+
{
82+
_wndProc = new(WndProc);
83+
84+
WNDCLASSEXW wndClass = default;
85+
wndClass.cbSize = (uint)sizeof(WNDCLASSEXW);
86+
wndClass.lpfnWndProc = (delegate* unmanaged[Stdcall]<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal.GetFunctionPointerForDelegate(_wndProc);
87+
wndClass.hInstance = PInvoke.GetModuleHandle(default(PWSTR));
88+
wndClass.lpszClassName = pszClassName;
89+
90+
PInvoke.RegisterClassEx(&wndClass);
91+
PInvoke.CreateWindowEx(0, pszClassName, null, 0, 0, 0, 0, 0, HWND.HWND_MESSAGE, default, wndClass.hInstance, null);
92+
}
93+
}
94+
95+
// Methods
96+
97+
private unsafe LRESULT WndProc(HWND hWnd, uint uMessage, WPARAM wParam, LPARAM lParam)
98+
{
99+
switch (uMessage)
100+
{
101+
case PInvoke.WM_CREATE:
102+
{
103+
PInvoke.CoInitialize();
104+
105+
ITEMIDLIST* pidl = default;
106+
IWindowsFolder folder = (IWindowsFolder)Folder;
107+
PInvoke.SHGetIDListFromObject((IUnknown*)folder.ThisPtr, &pidl);
108+
_folderPidl = pidl;
109+
110+
SHChangeNotifyEntry changeNotifyEntry = default;
111+
changeNotifyEntry.pidl = pidl;
112+
113+
_watcherRegID = PInvoke.SHChangeNotifyRegister(
114+
hWnd,
115+
SHCNRF_SOURCE.SHCNRF_ShellLevel | SHCNRF_SOURCE.SHCNRF_NewDelivery,
116+
(int)SHCNE_ID.SHCNE_ALLEVENTS,
117+
WM_NOTIFYFOLDERCHANGE,
118+
1,
119+
&changeNotifyEntry);
120+
121+
if (_watcherRegID is 0U)
122+
break;
123+
}
124+
break;
125+
case WM_NOTIFYFOLDERCHANGE:
126+
{
127+
ITEMIDLIST** ppidl;
128+
uint lEvent = 0;
129+
HANDLE hLock = PInvoke.SHChangeNotification_Lock((HANDLE)(nint)wParam.Value, (uint)lParam.Value, &ppidl, (int*)&lEvent);
130+
131+
if (hLock.IsNull)
132+
break;
133+
134+
ITEMIDLIST* pOldPidl = ppidl[0];
135+
ITEMIDLIST* pNewPidl = ppidl[1];
136+
137+
SHCNE_ID eventType = (SHCNE_ID)lEvent;
138+
var oldItem = WindowsStorable.TryParse(pOldPidl);
139+
var newItem = WindowsStorable.TryParse(pNewPidl);
140+
141+
_debouncer.Debounce(() =>
142+
{
143+
FireEvent(eventType, oldItem, newItem);
144+
});
145+
146+
PInvoke.SHChangeNotification_Unlock(hLock);
147+
148+
PInvoke.CoTaskMemFree(pOldPidl);
149+
PInvoke.CoTaskMemFree(pNewPidl);
150+
}
151+
break;
152+
case PInvoke.WM_DESTROY:
153+
{
154+
Dispose();
155+
}
156+
break;
157+
}
158+
159+
return PInvoke.DefWindowProc(hWnd, uMessage, wParam, lParam);
160+
}
161+
162+
private void FireEvent(SHCNE_ID eventType, IWindowsStorable? oldItem, IWindowsStorable? newItem)
163+
{
164+
EventOccurred?.Invoke(this, new(eventType, oldItem, newItem));
165+
166+
switch (eventType)
167+
{
168+
case SHCNE_ID.SHCNE_ASSOCCHANGED:
169+
{
170+
ItemAssocChanged?.Invoke(this, new(eventType, oldItem, newItem));
171+
}
172+
break;
173+
case SHCNE_ID.SHCNE_ATTRIBUTES:
174+
{
175+
ItemAttributesChanged?.Invoke(this, new(eventType, oldItem, newItem));
176+
}
177+
break;
178+
case SHCNE_ID.SHCNE_UPDATEIMAGE:
179+
{
180+
ItemImageUpdated?.Invoke(this, new(eventType, oldItem, newItem));
181+
}
182+
break;
183+
case SHCNE_ID.SHCNE_RENAMEITEM:
184+
{
185+
FileRenamed?.Invoke(this, new(eventType, oldItem, newItem));
186+
}
187+
break;
188+
case SHCNE_ID.SHCNE_CREATE:
189+
{
190+
FileCreated?.Invoke(this, new(eventType, oldItem, newItem));
191+
}
192+
break;
193+
case SHCNE_ID.SHCNE_DELETE:
194+
{
195+
FileDeleted?.Invoke(this, new(eventType, oldItem, newItem));
196+
}
197+
break;
198+
case SHCNE_ID.SHCNE_UPDATEITEM:
199+
{
200+
FileUpdated?.Invoke(this, new(eventType, oldItem, newItem));
201+
}
202+
break;
203+
case SHCNE_ID.SHCNE_RENAMEFOLDER:
204+
{
205+
FolderRenamed?.Invoke(this, new(eventType, oldItem, newItem));
206+
}
207+
break;
208+
case SHCNE_ID.SHCNE_MKDIR:
209+
{
210+
FolderCreated?.Invoke(this, new(eventType, oldItem, newItem));
211+
}
212+
break;
213+
case SHCNE_ID.SHCNE_RMDIR:
214+
{
215+
FolderDeleted?.Invoke(this, new(eventType, oldItem, newItem));
216+
}
217+
break;
218+
case SHCNE_ID.SHCNE_UPDATEDIR:
219+
{
220+
FolderUpdated?.Invoke(this, new(eventType, oldItem, newItem));
221+
}
222+
break;
223+
case SHCNE_ID.SHCNE_MEDIAINSERTED:
224+
{
225+
MediaInserted?.Invoke(this, new(eventType, oldItem, newItem));
226+
}
227+
break;
228+
case SHCNE_ID.SHCNE_MEDIAREMOVED:
229+
{
230+
MediaRemoved?.Invoke(this, new(eventType, oldItem, newItem));
231+
}
232+
break;
233+
case SHCNE_ID.SHCNE_DRIVEREMOVED:
234+
{
235+
DriveRemoved?.Invoke(this, new(eventType, oldItem, newItem));
236+
}
237+
break;
238+
case SHCNE_ID.SHCNE_DRIVEADD:
239+
{
240+
DriveAdded?.Invoke(this, new(eventType, oldItem, newItem));
241+
}
242+
break;
243+
case SHCNE_ID.SHCNE_DRIVEADDGUI:
244+
{
245+
DriveAddedViaGUI?.Invoke(this, new(eventType, oldItem, newItem));
246+
}
247+
break;
248+
case SHCNE_ID.SHCNE_FREESPACE:
249+
{
250+
FreeSpaceUpdated?.Invoke(this, new(eventType, oldItem, newItem));
251+
}
252+
break;
253+
case SHCNE_ID.SHCNE_NETSHARE:
254+
{
255+
SharingStarted?.Invoke(this, new(eventType, oldItem, newItem));
256+
}
257+
break;
258+
case SHCNE_ID.SHCNE_NETUNSHARE:
259+
{
260+
SharingStopped?.Invoke(this, new(eventType, oldItem, newItem));
261+
}
262+
break;
263+
case SHCNE_ID.SHCNE_SERVERDISCONNECT:
264+
{
265+
DisconnectedFromServer?.Invoke(this, new(eventType, oldItem, newItem));
266+
}
267+
break;
268+
case SHCNE_ID.SHCNE_EXTENDED_EVENT:
269+
{
270+
ExtendedEventOccurred?.Invoke(this, new(eventType, oldItem, newItem));
271+
}
272+
break;
273+
case SHCNE_ID.SHCNE_INTERRUPT:
274+
{
275+
SystemInterruptOccurred?.Invoke(this, new(eventType, oldItem, newItem));
276+
}
277+
break;
278+
}
279+
}
280+
281+
// Disposers
282+
283+
public void Dispose()
284+
{
285+
PInvoke.SHChangeNotifyDeregister(_watcherRegID);
286+
PInvoke.CoTaskMemFree(_folderPidl);
287+
PInvoke.CoUninitialize();
288+
PInvoke.PostQuitMessage(0);
289+
}
290+
291+
public ValueTask DisposeAsync()
292+
{
293+
Dispose();
294+
295+
return ValueTask.CompletedTask;
296+
}
297+
}
298+
}

0 commit comments

Comments
 (0)