From dfe07b16ae67e5d0e72f9f02f88ef4cc200ce55b Mon Sep 17 00:00:00 2001 From: rbalsleyMSFT <53497092+rbalsleyMSFT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:37:22 -0700 Subject: [PATCH] Adds support for an initial directory in folder picker Enhances the modern folder browser to accept and open to a specified initial directory. This improves the user experience by starting the "Browse for Drivers" dialog in the project's 'Drivers' subfolder, reducing the need for manual navigation. The implementation uses the Win32 API to create a shell item from the initial path and set it as the dialog's starting folder. --- .../FFUUI.Core/FFUUI.Core.Handlers.psm1 | 3 +- .../FFUUI.Core/FFUUI.Core.Shared.psm1 | 310 ++++++++++-------- 2 files changed, 170 insertions(+), 143 deletions(-) diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 index bd16885..bc0792e 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Handlers.psm1 @@ -594,7 +594,8 @@ function Register-EventHandlers { param($eventSource, $routedEventArgs) $window = [System.Windows.Window]::GetWindow($eventSource) $localState = $window.Tag - $selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select Drivers Folder" + $initialDir = Join-Path -Path $localState.FFUDevelopmentPath -ChildPath "Drivers" + $selectedPath = Invoke-BrowseAction -Type 'Folder' -Title "Select Drivers Folder" -InitialDirectory $initialDir if ($selectedPath) { $localState.Controls.txtDriversFolder.Text = $selectedPath } diff --git a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 index 38f46ad..f8b21b0 100644 --- a/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 +++ b/FFUDevelopment/FFUUI.Core/FFUUI.Core.Shared.psm1 @@ -620,165 +620,192 @@ function Invoke-ListViewSort { # 1) Define a C# class that uses the correct GUIDs for IFileDialog, IFileOpenDialog, and FileOpenDialog, # while omitting conflicting "GetResults/GetSelectedItems" from IFileDialog. -Add-Type -TypeDefinition @" -using System; -using System.Runtime.InteropServices; +if (-not ("ModernFolderBrowser" -as [type])) { + $modernFolderBrowserCode = @" + using System; + using System.Runtime.InteropServices; -public static class ModernFolderBrowser -{ - // Flags for IFileDialog - [Flags] - private enum FileDialogOptions : uint + public static class ModernFolderBrowser { - OverwritePrompt = 0x00000002, - StrictFileTypes = 0x00000004, - NoChangeDir = 0x00000008, - PickFolders = 0x00000020, - ForceFileSystem = 0x00000040, - AllNonStorageItems = 0x00000080, - NoValidate = 0x00000100, - AllowMultiSelect = 0x00000200, - PathMustExist = 0x00000800, - FileMustExist = 0x00001000, - CreatePrompt = 0x00002000, - ShareAware = 0x00004000, - NoReadOnlyReturn = 0x00008000, - NoTestFileCreate = 0x00010000, - DontAddToRecent = 0x02000000, - ForceShowHidden = 0x10000000 - } - - // IFileDialog (GUID from Windows SDK) - // - Omitting GetResults / GetSelectedItems to avoid overshadow. - [ComImport] - [Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface IFileDialog - { - [PreserveSig] - int Show(IntPtr parent); - - void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec); - void SetFileTypeIndex(uint iFileType); - void GetFileTypeIndex(out uint piFileType); - void Advise(IntPtr pfde, out uint pdwCookie); - void Unadvise(uint dwCookie); - void SetOptions(FileDialogOptions fos); - void GetOptions(out FileDialogOptions pfos); - void SetDefaultFolder(IShellItem psi); - void SetFolder(IShellItem psi); - void GetFolder(out IShellItem ppsi); - void GetCurrentSelection(out IShellItem ppsi); - void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); - void GetFileName(out IntPtr pszName); - void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); - void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - void GetResult(out IShellItem ppsi); - void AddPlace(IShellItem psi, int fdap); - void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - void Close(int hr); - void SetClientGuid(ref Guid guid); - void ClearClientData(); - void SetFilter(IntPtr pFilter); - - // NOTE: We intentionally do NOT define GetResults and GetSelectedItems here, - // because they cause overshadow warnings in IFileOpenDialog. - } - - // IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name, - // which otherwise cause overshadow warnings. We'll define them only here. - [ComImport] - [Guid("D57C7288-D4AD-4768-BE02-9D969532D960")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface IFileOpenDialog : IFileDialog - { - // These two come after the parent's vtable: - void GetResults(out IntPtr ppenum); - void GetSelectedItems(out IntPtr ppsai); - } - - // The coclass for creating an IFileOpenDialog - [ComImport] - [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] - private class FileOpenDialog - { - } - - // IShellItem - [ComImport] - [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - private interface IShellItem - { - void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); - void GetParent(out IShellItem ppsi); - void GetDisplayName(uint sigdnName, out IntPtr ppszName); - void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); - void Compare(IShellItem psi, uint hint, out int piOrder); - } - - private const uint SIGDN_FILESYSPATH = 0x80058000; - - public static string ShowDialog(string title, IntPtr parentHandle) - { - // Create COM dialog instance - IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog()); - - // Get current options - FileDialogOptions opts; - dialog.GetOptions(out opts); - - // Add flags for picking folders - opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem; - dialog.SetOptions(opts); - - // Set title - if (!string.IsNullOrEmpty(title)) + // Flags for IFileDialog + [Flags] + private enum FileDialogOptions : uint + { - dialog.SetTitle(title); + OverwritePrompt = 0x00000002, + StrictFileTypes = 0x00000004, + NoChangeDir = 0x00000008, + PickFolders = 0x00000020, + ForceFileSystem = 0x00000040, + AllNonStorageItems = 0x00000080, + NoValidate = 0x00000100, + AllowMultiSelect = 0x00000200, + PathMustExist = 0x00000800, + FileMustExist = 0x00001000, + CreatePrompt = 0x00002000, + ShareAware = 0x00004000, + NoReadOnlyReturn = 0x00008000, + NoTestFileCreate = 0x00010000, + DontAddToRecent = 0x02000000, + ForceShowHidden = 0x10000000 } - // Show the dialog - int hr = dialog.Show(parentHandle); - // 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so. - if (hr != 0) + // IFileDialog (GUID from Windows SDK) + // - Omitting GetResults / GetSelectedItems to avoid overshadow. + [ComImport] + [Guid("42F85136-DB7E-439C-85F1-E4075D135FC8")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IFileDialog { - if ((uint)hr == 0x800704C7 || hr == 1) - { - return null; // Canceled - } - else - { - Marshal.ThrowExceptionForHR(hr); - } + [PreserveSig] + int Show(IntPtr parent); + + void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec); + void SetFileTypeIndex(uint iFileType); + void GetFileTypeIndex(out uint piFileType); + void Advise(IntPtr pfde, out uint pdwCookie); + void Unadvise(uint dwCookie); + void SetOptions(FileDialogOptions fos); + void GetOptions(out FileDialogOptions pfos); + void SetDefaultFolder(IShellItem psi); + void SetFolder(IShellItem psi); + void GetFolder(out IShellItem ppsi); + void GetCurrentSelection(out IShellItem ppsi); + void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); + void GetFileName(out IntPtr pszName); + void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); + void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + void GetResult(out IShellItem ppsi); + void AddPlace(IShellItem psi, int fdap); + void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + void Close(int hr); + void SetClientGuid(ref Guid guid); + void ClearClientData(); + void SetFilter(IntPtr pFilter); + + // NOTE: We intentionally do NOT define GetResults and GetSelectedItems here, + // because they cause overshadow warnings in IFileOpenDialog. } - // Retrieve the selection (IShellItem) - IShellItem shellItem; - dialog.GetResult(out shellItem); - if (shellItem == null) return null; + // IFileOpenDialog extends IFileDialog by adding 2 new methods with the same name, + // which otherwise cause overshadow warnings. We'll define them only here. + [ComImport] + [Guid("D57C7288-D4AD-4768-BE02-9D969532D960")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IFileOpenDialog : IFileDialog + { + // These two come after the parent's vtable: + void GetResults(out IntPtr ppenum); + void GetSelectedItems(out IntPtr ppsai); + } - // Convert to file system path - IntPtr pszPath = IntPtr.Zero; - shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath); - if (pszPath == IntPtr.Zero) return null; + // The coclass for creating an IFileOpenDialog + [ComImport] + [Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] + private class FileOpenDialog + { + } - string folderPath = Marshal.PtrToStringAuto(pszPath); - Marshal.FreeCoTaskMem(pszPath); + // IShellItem + [ComImport] + [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IShellItem + { + void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); + void GetParent(out IShellItem ppsi); + void GetDisplayName(uint sigdnName, out IntPtr ppszName); + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + void Compare(IShellItem psi, uint hint, out int piOrder); + } - return folderPath; + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface, IidParameterIndex = 2)] out IShellItem ppv); + + private const uint SIGDN_FILESYSPATH = 0x80058000; + private static readonly Guid IID_IShellItem = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + + public static string ShowDialog(string title, IntPtr parentHandle, string initialDirectory) + { + // Create COM dialog instance + IFileOpenDialog dialog = (IFileOpenDialog)(new FileOpenDialog()); + + // Get current options + FileDialogOptions opts; + dialog.GetOptions(out opts); + + // Add flags for picking folders + opts |= FileDialogOptions.PickFolders | FileDialogOptions.PathMustExist | FileDialogOptions.ForceFileSystem; + dialog.SetOptions(opts); + + // Set initial directory if provided + if (!string.IsNullOrEmpty(initialDirectory)) + { + try + { + Guid iid = IID_IShellItem; // Create a local copy to pass by ref + if (SHCreateItemFromParsingName(initialDirectory, IntPtr.Zero, ref iid, out IShellItem initialFolder) == 0) + { + dialog.SetFolder(initialFolder); + Marshal.ReleaseComObject(initialFolder); + } + } + catch + { + // Ignore errors in setting initial directory (e.g., path doesn't exist) + } + } + + // Set title + if (!string.IsNullOrEmpty(title)) + { + dialog.SetTitle(title); + } + + // Show the dialog + int hr = dialog.Show(parentHandle); + // 0 = S_OK. 1 or 0x800704C7 often means user canceled. Return null if so. + if (hr != 0) + { + if ((uint)hr == 0x800704C7 || hr == 1) + { + return null; // Canceled + } + else + { + Marshal.ThrowExceptionForHR(hr); + } + } + + // Retrieve the selection (IShellItem) + IShellItem shellItem; + dialog.GetResult(out shellItem); + if (shellItem == null) return null; + + // Convert to file system path + IntPtr pszPath = IntPtr.Zero; + shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszPath); + if (pszPath == IntPtr.Zero) return null; + + string folderPath = Marshal.PtrToStringAuto(pszPath); + Marshal.FreeCoTaskMem(pszPath); + + return folderPath; + } } +"@ + Add-Type -TypeDefinition $modernFolderBrowserCode -Language CSharp } -"@ -Language CSharp # 2) Define a PowerShell function that invokes our C# wrapper function Show-ModernFolderPicker { param( - [string]$Title = "Select a folder" + [string]$Title = "Select a folder", + [string]$InitialDirectory ) # For a simple test, pass IntPtr.Zero as the parent window handle - return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero) + return [ModernFolderBrowser]::ShowDialog($Title, [IntPtr]::Zero, $InitialDirectory) } function Invoke-BrowseAction { @@ -797,8 +824,7 @@ function Invoke-BrowseAction { switch ($Type) { 'Folder' { - # Show-ModernFolderPicker does not currently support setting an initial directory. - return Show-ModernFolderPicker -Title $Title + return Show-ModernFolderPicker -Title $Title -InitialDirectory $InitialDirectory } 'OpenFile' { $dialog = New-Object Microsoft.Win32.OpenFileDialog