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/IWindowsFolderWatcher.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolderWatcher.cs new file mode 100644 index 000000000000..4b1e7972d173 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsFolderWatcher.cs @@ -0,0 +1,41 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Foundation; + +namespace Files.App.Storage +{ + public interface IWindowsFolderWatcher : IFolderWatcher + { + public event TypedEventHandler? EventOccurred; + + 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 + } +} 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..b799dc741311 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolderWatcher.cs @@ -0,0 +1,284 @@ +// 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 : IWindowsFolderWatcher + { + // Fields + + private const uint WM_NOTIFYFOLDERCHANGE = PInvoke.WM_APP | 0x0001U; + private readonly WNDPROC _wndProc; + + private uint _watcherRegID = 0U; + private ITEMIDLIST* _folderPidl = default; + + // Properties + + public IMutableFolder Folder { get; private set; } + + // Events + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + public event TypedEventHandler? EventOccurred; + + 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); + } + } + + // Methods + + 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); + _folderPidl = 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; + + FireEvent((SHCNE_ID)lEvent, ppidl); + + 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) + { + //ITEMIDLIST* pOldPidl = ppidl[0]; + //ITEMIDLIST* pNewPidl = ppidl[1]; + + EventOccurred?.Invoke(this, new(eventType, null, null)); // WindowsStorable.TryParse(pOldPidl), WindowsStorable.TryParse(pNewPidl) + + 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; + } + } + + // Disposers + + public void Dispose() + { + PInvoke.SHChangeNotifyDeregister(_watcherRegID); + PInvoke.CoTaskMemFree(_folderPidl); + 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..e31ebe525f84 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs @@ -5,10 +5,11 @@ using Windows.Win32.Foundation; using Windows.Win32.System.SystemServices; using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; namespace Files.App.Storage { - public abstract class WindowsStorable : IWindowsStorable, IStorableChild, IEquatable + public abstract class WindowsStorable : IWindowsStorable { public ComPtr ThisPtr { get; protected set; } @@ -19,46 +20,59 @@ public abstract class WindowsStorable : IWindowsStorable, IStorableChild, IEquat public static unsafe WindowsStorable? TryParse(string parsablePath) { HRESULT hr = default; - ComPtr pShellItem = default; - var IID_IShellItem = typeof(IShellItem).GUID; + IShellItem* pShellItem = default; fixed (char* pszParsablePath = parsablePath) { hr = PInvoke.SHCreateItemFromParsingName( pszParsablePath, null, - &IID_IShellItem, - (void**)pShellItem.GetAddressOf()); + IID.IID_IShellItem, + (void**)&pShellItem); } - if (pShellItem.IsNull) + if (hr.ThrowIfFailedOnDebug().Failed) return null; - return pShellItem.HasShellAttributes(SFGAO_FLAGS.SFGAO_FOLDER) - ? new WindowsFolder(pShellItem) - : new WindowsFile(pShellItem); + bool isFolder = + pShellItem->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded && + returnedAttributes is SFGAO_FLAGS.SFGAO_FOLDER; + + return isFolder ? new WindowsFolder(pShellItem) : new WindowsFile(pShellItem); } public static unsafe WindowsStorable? TryParse(IShellItem* ptr) { - ComPtr pShellItem = default; - pShellItem.Attach(ptr); + bool isFolder = + ptr->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded && + returnedAttributes is SFGAO_FLAGS.SFGAO_FOLDER; + + return isFolder ? new WindowsFolder(ptr) : new WindowsFile(ptr); + } + + public static unsafe WindowsStorable? TryParse(ITEMIDLIST* pidl) + { + IShellItem* pShellItem = default; + HRESULT hr = PInvoke.SHCreateItemFromIDList(pidl, IID.IID_IShellItem, (void**)&pShellItem); + if (hr.ThrowIfFailedOnDebug().Failed || pShellItem is null) + return null; - return pShellItem.HasShellAttributes(SFGAO_FLAGS.SFGAO_FOLDER) - ? new WindowsFolder(pShellItem) - : new WindowsFile(pShellItem); + bool isFolder = + pShellItem->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded && + returnedAttributes is SFGAO_FLAGS.SFGAO_FOLDER; + + return isFolder ? new WindowsFolder(pShellItem) : new WindowsFile(pShellItem); } public unsafe Task GetParentAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - ComPtr pParentFolder = default; - HRESULT hr = ThisPtr.Get()->GetParent(pParentFolder.GetAddressOf()); + IShellItem* pParentFolder = default; + HRESULT hr = ThisPtr.Get()->GetParent(&pParentFolder); if (hr.Failed) { - if (!pParentFolder.IsNull) pParentFolder.Dispose(); - + if (pParentFolder is not null) pParentFolder->Release(); return Task.FromResult(null); } diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index 2739fb84162a..5dc025e820d2 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -33,8 +33,7 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW // Fields - // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable - private readonly SystemIO.FileSystemWatcher _quickAccessFolderWatcher; + private readonly IWindowsFolderWatcher _watcher; // Constructor @@ -46,19 +45,9 @@ 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 - }; - - _quickAccessFolderWatcher.Changed += async (s, e) => - { - await RefreshWidgetAsync(); - }; - - _quickAccessFolderWatcher.EnableRaisingEvents = true; + var quickAccessFolder = new WindowsFolder(new Guid("3936e9e4-d92c-4eee-a85a-bc16d5ea0819")); + _watcher = (IWindowsFolderWatcher)quickAccessFolder.GetFolderWatcherAsync(default).Result; + _watcher.EventOccurred += async (s, e) => { await RefreshWidgetAsync(); }; } // Methods