From 6a85db5bece6170be1e83598def09bdcde16b5a6 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Fri, 9 May 2025 15:40:15 +0900 Subject: [PATCH] Init --- src/Files.App.CsWin32/NativeMethods.txt | 9 + .../Storables/WindowsStorage/IWindowsFile.cs | 9 + .../WindowsStorage/IWindowsFolder.cs | 9 + .../WindowsStorage/IWindowsStorable.cs | 2 +- .../Storables/WindowsStorage/WindowsFile.cs | 2 +- .../Storables/WindowsStorage/WindowsFolder.cs | 8 +- .../WindowsStorage/WindowsFolderWatcher.cs | 273 ++++++++++++++++++ .../WindowsFolderWatcherEventArgs.cs | 23 ++ .../WindowsStorage/WindowsStorable.cs | 2 +- .../Widgets/QuickAccessWidgetViewModel.cs | 27 +- 10 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 src/Files.App.Storage/Storables/WindowsStorage/IWindowsFile.cs create mode 100644 src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolder.cs create mode 100644 src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs create mode 100644 src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcherEventArgs.cs diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index ce5524f6885e..255745967dc0 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -225,3 +225,12 @@ QITIPF_FLAGS GetKeyboardState MapVirtualKey GetKeyboardLayout +SHChangeNotifyRegister +SHChangeNotifyDeregister +SHChangeNotification_Lock +SHChangeNotification_Unlock +CoInitialize +CoUninitialize +PostQuitMessage +HWND_MESSAGE +SHCNE_ID diff --git a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFile.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFile.cs new file mode 100644 index 000000000000..43f30155f907 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFile.cs @@ -0,0 +1,9 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage +{ + public interface IWindowsFile : IWindowsStorable, IChildFile + { + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolder.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolder.cs new file mode 100644 index 000000000000..bc21ce68bf41 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolder.cs @@ -0,0 +1,9 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage +{ + public interface IWindowsFolder : IWindowsStorable, IChildFolder, IMutableFolder + { + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs index 421d7a68dddd..4dd82c398487 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs @@ -6,7 +6,7 @@ namespace Files.App.Storage { - public interface IWindowsStorable : IDisposable + public interface IWindowsStorable : IStorableChild, IEquatable, IDisposable { ComPtr ThisPtr { get; } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs index 3ce56f786c2f..728552e06127 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs @@ -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 nativeObject) { diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs index f4105687184f..fc2ff081fb31 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs @@ -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 nativeObject) { @@ -85,5 +85,11 @@ unsafe bool GetNext() } } } + + public Task GetFolderWatcherAsync(CancellationToken cancellationToken = default) + { + IFolderWatcher watcher = new WindowsFolderWatcher(this); + return Task.FromResult(watcher); + } } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs new file mode 100644 index 000000000000..b52a1eff1573 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs @@ -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 +{ + /// + /// Represents an implementation of that uses Windows Shell notifications to watch for changes in a folder. + /// + 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? ItemAssocChanged; // SHCNE_ASSOCCHANGED + public event TypedEventHandler? ItemAttributesChanged; // SHCNE_ATTRIBUTES + public event TypedEventHandler? ItemImageUpdated; // SHCNE_UPDATEIMAGE + + public event TypedEventHandler? FileRenamed; // SHCNE_RENAMEITEM + public event TypedEventHandler? FileCreated; // SHCNE_CREATE + public event TypedEventHandler? FileDeleted; // SHCNE_DELETE + public event TypedEventHandler? FileUpdated; // SHCNE_UPDATEITEM + + public event TypedEventHandler? FolderRenamed; // SHCNE_RENAMEFOLDER + public event TypedEventHandler? FolderCreated; // SHCNE_MKDIR + public event TypedEventHandler? FolderDeleted; // SHCNE_RMDIR + public event TypedEventHandler? FolderUpdated; // SHCNE_UPDATEDIR + + public event TypedEventHandler? MediaInserted; // SHCNE_MEDIAINSERTED + public event TypedEventHandler? MediaRemoved; // SHCNE_MEDIAREMOVED + public event TypedEventHandler? DriveRemoved; // SHCNE_DRIVEREMOVED + public event TypedEventHandler? DriveAdded; // SHCNE_DRIVEADD + public event TypedEventHandler? DriveAddedViaGUI; // SHCNE_DRIVEADDGUI + public event TypedEventHandler? FreeSpaceUpdated; // SHCNE_FREESPACE + + public event TypedEventHandler? SharingStarted; // SHCNE_NETSHARE + public event TypedEventHandler? SharingStopped; // SHCNE_NETUNSHARE + + public event TypedEventHandler? DisconnectedFromServer; // SHCNE_SERVERDISCONNECT + + public event TypedEventHandler? ExtendedEventOccurred; // SHCNE_EXTENDED_EVENT + public event TypedEventHandler? SystemInterruptOccurred; // SHCNE_INTERRUPT + + // Constructor + + /// Initializes a new instance of the class. + /// Specifies the folder to be monitored for changes. + 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])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; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcherEventArgs.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcherEventArgs.cs new file mode 100644 index 000000000000..69158c39a26b --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcherEventArgs.cs @@ -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; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs index 3fdc51e33389..6dd76debb3e0 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs @@ -8,7 +8,7 @@ namespace Files.App.Storage { - public abstract class WindowsStorable : IWindowsStorable, IStorableChild, IEquatable + public abstract class WindowsStorable : IWindowsStorable { public ComPtr ThisPtr { get; protected set; } diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index 2739fb84162a..75f958d77d4c 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -36,6 +36,8 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable private readonly SystemIO.FileSystemWatcher _quickAccessFolderWatcher; + private readonly IFolderWatcher _watcher; + // Constructor public QuickAccessWidgetViewModel() @@ -46,19 +48,22 @@ public QuickAccessWidgetViewModel() PinToSidebarCommand = new AsyncRelayCommand(ExecutePinToSidebarCommand); UnpinFromSidebarCommand = new AsyncRelayCommand(ExecuteUnpinFromSidebarCommand); - _quickAccessFolderWatcher = new() - { - Path = SystemIO.Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations"), - Filter = "f01b4d95cf55d32a.automaticDestinations-ms", - NotifyFilter = SystemIO.NotifyFilters.LastAccess | SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName - }; + var quickAccessFolder = new WindowsFolder(new Guid("3936e9e4-d92c-4eee-a85a-bc16d5ea0819")); + _watcher = quickAccessFolder.GetFolderWatcherAsync(default).Result; - _quickAccessFolderWatcher.Changed += async (s, e) => - { - await RefreshWidgetAsync(); - }; + //_quickAccessFolderWatcher = new() + //{ + // Path = SystemIO.Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations"), + // Filter = "f01b4d95cf55d32a.automaticDestinations-ms", + // NotifyFilter = SystemIO.NotifyFilters.LastAccess | SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName + //}; + + //_quickAccessFolderWatcher.Changed += async (s, e) => + //{ + // await RefreshWidgetAsync(); + //}; - _quickAccessFolderWatcher.EnableRaisingEvents = true; + //_quickAccessFolderWatcher.EnableRaisingEvents = true; } // Methods