diff --git a/src/Android/Perspex.Android/SystemDialogImpl.cs b/src/Android/Perspex.Android/SystemDialogImpl.cs index 60f9de914c..d60fad8230 100644 --- a/src/Android/Perspex.Android/SystemDialogImpl.cs +++ b/src/Android/Perspex.Android/SystemDialogImpl.cs @@ -12,5 +12,10 @@ namespace Perspex.Android { throw new NotImplementedException(); } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs b/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs index 869bd2eb70..304de86fc3 100644 --- a/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs +++ b/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs @@ -53,5 +53,10 @@ namespace Perspex.Gtk dlg.Show(); return tcs.Task; } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + throw new NotImplementedException(); + } } } diff --git a/src/Perspex.Controls/Platform/ISystemDialogImpl.cs b/src/Perspex.Controls/Platform/ISystemDialogImpl.cs index d3af982f95..f45eec8f95 100644 --- a/src/Perspex.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Perspex.Controls/Platform/ISystemDialogImpl.cs @@ -18,5 +18,7 @@ namespace Perspex.Controls.Platform /// The parent window. /// A task returning the selected filenames. Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent); + + Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent); } } diff --git a/src/Perspex.Controls/SystemDialog.cs b/src/Perspex.Controls/SystemDialog.cs index 6eae1e9e9d..bac791dd36 100644 --- a/src/Perspex.Controls/SystemDialog.cs +++ b/src/Perspex.Controls/SystemDialog.cs @@ -5,17 +5,20 @@ using Perspex.Controls.Platform; namespace Perspex.Controls { - public abstract class FileDialog : SystemDialog + public abstract class FileDialog : FileSystemDialog { public List Filters { get; set; } = new List(); - public string InitialFileName { get; set; } - public string InitialDirectory { get; set; } + public string InitialFileName { get; set; } } + public abstract class FileSystemDialog : SystemDialog + { + public string InitialDirectory { get; set; } + } public class SaveFileDialog : FileDialog { - public string DefaultExtension { get; set; } + public string DefaultExtension { get; set; } public async Task ShowAsync(Window window = null) => @@ -31,6 +34,14 @@ namespace Perspex.Controls => PerspexLocator.Current.GetService().ShowFileDialogAsync(this, window?.PlatformImpl); } + public class OpenFolderDialog : FileSystemDialog + { + public string DefaultDirectory { get; set; } + + public Task ShowAsync(Window window = null) + => PerspexLocator.Current.GetService().ShowFolderDialogAsync(this, window?.PlatformImpl); + } + public abstract class SystemDialog { public string Title { get; set; } diff --git a/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs index 5bb2038813..4d87d0e033 100644 --- a/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -702,6 +703,13 @@ namespace Perspex.Win32.Interop return SetClassLong64(hWnd, nIndex, dwNewLong); } + + [ComImport, ClassInterface(ClassInterfaceType.None), TypeLibType(TypeLibTypeFlags.FCanCreate), Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")] + internal class FileOpenDialogRCW { } + + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); [DllImport("user32.dll", SetLastError = true)] public static extern bool OpenClipboard(IntPtr hWndOwner); @@ -873,6 +881,41 @@ namespace Perspex.Win32.Interop } + public enum HRESULT : long + { + S_FALSE = 0x0001, + S_OK = 0x0000, + E_INVALIDARG = 0x80070057, + E_OUTOFMEMORY = 0x8007000E + } + + public const uint SIGDN_FILESYSPATH = 0x80058000; + + [Flags] + internal enum FOS : uint + { + FOS_OVERWRITEPROMPT = 0x00000002, + FOS_STRICTFILETYPES = 0x00000004, + FOS_NOCHANGEDIR = 0x00000008, + FOS_PICKFOLDERS = 0x00000020, + FOS_FORCEFILESYSTEM = 0x00000040, // Ensure that items returned are filesystem items. + FOS_ALLNONSTORAGEITEMS = 0x00000080, // Allow choosing items that have no storage. + FOS_NOVALIDATE = 0x00000100, + FOS_ALLOWMULTISELECT = 0x00000200, + FOS_PATHMUSTEXIST = 0x00000800, + FOS_FILEMUSTEXIST = 0x00001000, + FOS_CREATEPROMPT = 0x00002000, + FOS_SHAREAWARE = 0x00004000, + FOS_NOREADONLYRETURN = 0x00008000, + FOS_NOTESTFILECREATE = 0x00010000, + FOS_HIDEMRUPLACES = 0x00020000, + FOS_HIDEPINNEDPLACES = 0x00040000, + FOS_NODEREFERENCELINKS = 0x00100000, + FOS_DONTADDTORECENT = 0x02000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000 + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct OpenFileName { @@ -899,6 +942,104 @@ namespace Perspex.Win32.Interop public IntPtr reservedPtr; public int reservedInt; public int flagsEx; - } + } + } + + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IFileDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypeIndex([In] uint iFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileTypeIndex(out uint piFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Unadvise([In] uint dwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Close([MarshalAs(UnmanagedType.Error)] uint hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint ClearClientData(); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + } + + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IShellItem + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } } diff --git a/src/Windows/Perspex.Win32/SystemDialogImpl.cs b/src/Windows/Perspex.Win32/SystemDialogImpl.cs index 69f394c75c..84aa78a2a7 100644 --- a/src/Windows/Perspex.Win32/SystemDialogImpl.cs +++ b/src/Windows/Perspex.Win32/SystemDialogImpl.cs @@ -13,6 +13,7 @@ using Perspex.Win32.Interop; namespace Perspex.Win32 { + class SystemDialogImpl : ISystemDialogImpl { public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) @@ -44,16 +45,15 @@ namespace Perspex.Win32 dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); string userSelectedExt = null; - + fixed (char* pFileBuffer = fileBuffer) fixed (char* pFilterBuffer = filterBuffer) fixed (char* pDefExt = defExt) fixed (char* pInitDir = dialog.InitialDirectory) fixed (char* pTitle = dialog.Title) { - var ofn = new UnmanagedMethods.OpenFileName() - { + { hwndOwner = hWnd, hInstance = IntPtr.Zero, lCustData = IntPtr.Zero, @@ -128,5 +128,65 @@ namespace Perspex.Win32 return files.Select(f => Path.Combine(dir, f)).ToArray(); }); } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return Task.Factory.StartNew(() => + { + string result = string.Empty; + + var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var frm = (IFileDialog)(new UnmanagedMethods.FileOpenDialogRCW()); + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + frm.SetOptions(options); + + if (dialog.InitialDirectory != null) + { + IShellItem directoryShellItem; + var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + frm.SetFolder(directoryShellItem); + } + } + + if (dialog.DefaultDirectory != null) + { + IShellItem directoryShellItem; + var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + frm.SetDefaultFolder(directoryShellItem); + } + } + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + result = Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + } + } + + return result; + }); + } } }