Skip to content

Code Quality: Introduced a watcher for IWindowsFolder to the Quick Access widget #17114

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
9 changes: 9 additions & 0 deletions src/Files.App.CsWin32/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,12 @@ QITIPF_FLAGS
GetKeyboardState
MapVirtualKey
GetKeyboardLayout
SHChangeNotifyRegister
SHChangeNotifyDeregister
SHChangeNotification_Lock
SHChangeNotification_Unlock
CoInitialize
CoUninitialize
PostQuitMessage
HWND_MESSAGE
SHCNE_ID
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

namespace Files.App.Storage
{
public interface IWindowsFile : IWindowsStorable, IChildFile
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

namespace Files.App.Storage
{
public interface IWindowsFolder : IWindowsStorable, IChildFolder, IMutableFolder
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Files.App.Storage
{
public interface IWindowsStorable : IDisposable
public interface IWindowsStorable : IStorableChild, IEquatable<IWindowsStorable>, IDisposable
{
ComPtr<IShellItem> ThisPtr { get; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace Files.App.Storage
{
[DebuggerDisplay("{" + nameof(ToString) + "()}")]
public sealed class WindowsFile : WindowsStorable, IChildFile
public sealed class WindowsFile : WindowsStorable, IWindowsFile
{
public WindowsFile(ComPtr<IShellItem> nativeObject)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Files.App.Storage
{
[DebuggerDisplay("{" + nameof(ToString) + "()}")]
public sealed class WindowsFolder : WindowsStorable, IChildFolder
public sealed class WindowsFolder : WindowsStorable, IWindowsFolder
{
public WindowsFolder(ComPtr<IShellItem> nativeObject)
{
Expand Down Expand Up @@ -85,5 +85,11 @@ unsafe bool GetNext()
}
}
}

public Task<IFolderWatcher> GetFolderWatcherAsync(CancellationToken cancellationToken = default)
{
IFolderWatcher watcher = new WindowsFolderWatcher(this);
return Task.FromResult(watcher);
}
}
}
273 changes: 273 additions & 0 deletions src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using System.Collections.Specialized;
using System.Runtime.InteropServices;
using Windows.Foundation;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Com;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.Common;
using Windows.Win32.UI.WindowsAndMessaging;

namespace Files.App.Storage
{
/// <summary>
/// Represents an implementation of <see cref="IFolderWatcher"/> that uses Windows Shell notifications to watch for changes in a folder.
/// </summary>
public unsafe partial class WindowsFolderWatcher : IFolderWatcher
{
// Fields

private const uint WM_NOTIFYFOLDERCHANGE = PInvoke.WM_APP | 0x0001U;
private readonly WNDPROC _wndProc;

private uint _watcherRegID = 0U;
private ITEMIDLIST* _targetItemPIDL = default;

// Properties

public IMutableFolder Folder { get; private set; }

// Events

public event NotifyCollectionChangedEventHandler? CollectionChanged;

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAssocChanged; // SHCNE_ASSOCCHANGED
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemAttributesChanged; // SHCNE_ATTRIBUTES
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ItemImageUpdated; // SHCNE_UPDATEIMAGE

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileRenamed; // SHCNE_RENAMEITEM
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileCreated; // SHCNE_CREATE
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileDeleted; // SHCNE_DELETE
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FileUpdated; // SHCNE_UPDATEITEM

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderRenamed; // SHCNE_RENAMEFOLDER
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderCreated; // SHCNE_MKDIR
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderDeleted; // SHCNE_RMDIR
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FolderUpdated; // SHCNE_UPDATEDIR

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaInserted; // SHCNE_MEDIAINSERTED
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? MediaRemoved; // SHCNE_MEDIAREMOVED
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveRemoved; // SHCNE_DRIVEREMOVED
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAdded; // SHCNE_DRIVEADD
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DriveAddedViaGUI; // SHCNE_DRIVEADDGUI
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? FreeSpaceUpdated; // SHCNE_FREESPACE

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStarted; // SHCNE_NETSHARE
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SharingStopped; // SHCNE_NETUNSHARE

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? DisconnectedFromServer; // SHCNE_SERVERDISCONNECT

public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? ExtendedEventOccurred; // SHCNE_EXTENDED_EVENT
public event TypedEventHandler<WindowsFolderWatcher, WindowsFolderWatcherEventArgs>? SystemInterruptOccurred; // SHCNE_INTERRUPT

// Constructor

/// <summary>Initializes a new instance of the <see cref="WindowsFolderWatcher"/> class.</summary>
/// <param name="folder">Specifies the folder to be monitored for changes.</param>
public WindowsFolderWatcher(WindowsFolder folder)
{
Folder = folder;

fixed (char* pszClassName = $"FolderWatcherWindowClass{Guid.NewGuid():B}")
{
_wndProc = new(WndProc);

WNDCLASSEXW wndClass = default;
wndClass.cbSize = (uint)sizeof(WNDCLASSEXW);
wndClass.lpfnWndProc = (delegate* unmanaged[Stdcall]<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal.GetFunctionPointerForDelegate(_wndProc);
wndClass.hInstance = PInvoke.GetModuleHandle(default(PWSTR));
wndClass.lpszClassName = pszClassName;

PInvoke.RegisterClassEx(&wndClass);
PInvoke.CreateWindowEx(0, pszClassName, null, 0, 0, 0, 0, 0, HWND.HWND_MESSAGE, default, wndClass.hInstance, null);
}
}

private unsafe LRESULT WndProc(HWND hWnd, uint uMessage, WPARAM wParam, LPARAM lParam)
{
switch (uMessage)
{
case PInvoke.WM_CREATE:
{
PInvoke.CoInitialize();

ITEMIDLIST* pidl = default;
IWindowsFolder folder = (IWindowsFolder)Folder;
PInvoke.SHGetIDListFromObject((IUnknown*)folder.ThisPtr.Get(), &pidl);
_targetItemPIDL = pidl;

SHChangeNotifyEntry changeNotifyEntry = default;
changeNotifyEntry.pidl = pidl;

_watcherRegID = PInvoke.SHChangeNotifyRegister(
hWnd,
SHCNRF_SOURCE.SHCNRF_ShellLevel | SHCNRF_SOURCE.SHCNRF_NewDelivery,
(int)SHCNE_ID.SHCNE_ALLEVENTS,
WM_NOTIFYFOLDERCHANGE,
1,
&changeNotifyEntry);

if (_watcherRegID is 0U)
break;
}
break;
case WM_NOTIFYFOLDERCHANGE:
{
ITEMIDLIST** ppidl;
uint lEvent = 0;
HANDLE hLock = PInvoke.SHChangeNotification_Lock((HANDLE)(nint)wParam.Value, (uint)lParam.Value, &ppidl, (int*)&lEvent);

if (hLock.IsNull)
break;

// TODO: Fire events

PInvoke.SHChangeNotification_Unlock(hLock);
}
break;
case PInvoke.WM_DESTROY:
{
Dispose();
}
break;
}

return PInvoke.DefWindowProc(hWnd, uMessage, wParam, lParam);
}

private void FireEvent(SHCNE_ID eventType, ITEMIDLIST** ppidl)
{
switch (eventType)
{
case SHCNE_ID.SHCNE_ASSOCCHANGED:
{
ItemAssocChanged?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_ATTRIBUTES:
{
ItemAttributesChanged?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_UPDATEIMAGE:
{
ItemImageUpdated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_RENAMEITEM:
{
FileRenamed?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_CREATE:
{
FileCreated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_DELETE:
{
FileDeleted?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_UPDATEITEM:
{
FileUpdated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_RENAMEFOLDER:
{
FolderRenamed?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_MKDIR:
{
FolderCreated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_RMDIR:
{
FolderDeleted?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_UPDATEDIR:
{
FolderUpdated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_MEDIAINSERTED:
{
MediaInserted?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_MEDIAREMOVED:
{
MediaRemoved?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_DRIVEREMOVED:
{
DriveRemoved?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_DRIVEADD:
{
DriveAdded?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_DRIVEADDGUI:
{
DriveAddedViaGUI?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_FREESPACE:
{
FreeSpaceUpdated?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_NETSHARE:
{
SharingStarted?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_NETUNSHARE:
{
SharingStopped?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_SERVERDISCONNECT:
{
DisconnectedFromServer?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_EXTENDED_EVENT:
{
ExtendedEventOccurred?.Invoke(this, new(eventType));
}
break;
case SHCNE_ID.SHCNE_INTERRUPT:
{
SystemInterruptOccurred?.Invoke(this, new(eventType));
}
break;
}
}

public void Dispose()
{
PInvoke.SHChangeNotifyDeregister(_watcherRegID);
PInvoke.CoTaskMemFree(_targetItemPIDL);
PInvoke.CoUninitialize();
PInvoke.PostQuitMessage(0);
}

public ValueTask DisposeAsync()
{
Dispose();

return ValueTask.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Windows.Win32.UI.Shell;

namespace Files.App.Storage
{
public class WindowsFolderWatcherEventArgs : EventArgs
{
public SHCNE_ID EventType { get; init; }

public IWindowsStorable? OldItem { get; init; }

public IWindowsStorable? NewItem { get; init; }

public WindowsFolderWatcherEventArgs(SHCNE_ID eventType, IWindowsStorable? _oldItem = null, IWindowsStorable? _newItem = null)
{
EventType = eventType;
OldItem = _oldItem;
NewItem = _newItem;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Files.App.Storage
{
public abstract class WindowsStorable : IWindowsStorable, IStorableChild, IEquatable<IWindowsStorable>
public abstract class WindowsStorable : IWindowsStorable
{
public ComPtr<IShellItem> ThisPtr { get; protected set; }

Expand Down
Loading
Loading