From 06e141bf81dce8ebb180dac37b73db2436056b10 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Jun 2022 01:01:31 -0400 Subject: [PATCH] Update macOS implementations with some UTType support --- .../project.pbxproj | 4 + .../Avalonia.Native/src/OSX/SystemDialogs.mm | 52 +++++++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 - src/Avalonia.Native/SystemDialogs.cs | 118 ++++++++++++------ src/Avalonia.Native/WindowImplBase.cs | 7 +- src/Avalonia.Native/avn.idl | 7 +- 6 files changed, 145 insertions(+), 44 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 6fc3977d4e..ace4a71a56 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; + ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -101,6 +102,7 @@ AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; + ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +110,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */, 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */, 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */, AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */, @@ -122,6 +125,7 @@ AB661C1C2148230E00291242 /* Frameworks */ = { isa = PBXGroup; children = ( + ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */, 522D5958258159C1006F7F7A /* Carbon.framework */, 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */, 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */, diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index 535b6c3b66..e133a5d31f 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,5 +1,6 @@ #include "common.h" #include "INSWindowHolder.h" +#import class SystemDialogs : public ComSingleObject { @@ -7,6 +8,7 @@ public: FORWARD_IUNKNOWN() virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle, IAvnSystemDialogEvents* events, + bool allowMultiple, const char* title, const char* initialDirectory) override { @@ -14,6 +16,7 @@ public: { auto panel = [NSOpenPanel openPanel]; + panel.allowsMultipleSelection = allowMultiple; panel.canChooseDirectories = true; panel.canCreateDirectories = true; panel.canChooseFiles = false; @@ -118,7 +121,15 @@ public: { auto allowedTypes = [filtersString componentsSeparatedByString:@";"]; - panel.allowedFileTypes = allowedTypes; + // Prefer allowedContentTypes if available + if (@available(macOS 11.0, *)) + { + panel.allowedContentTypes = ConvertToUTType(allowedTypes); + } + else + { + panel.allowedFileTypes = allowedTypes; + } } } @@ -207,7 +218,18 @@ public: { auto allowedTypes = [filtersString componentsSeparatedByString:@";"]; - panel.allowedFileTypes = allowedTypes; + // Prefer allowedContentTypes if available + if (@available(macOS 11.0, *)) + { + panel.allowedContentTypes = ConvertToUTType(allowedTypes); + } + else + { + panel.allowedFileTypes = allowedTypes; + } + + panel.allowsOtherFileTypes = false; + panel.extensionHidden = false; } } @@ -250,6 +272,32 @@ public: } } } + +private: + NSMutableArray* ConvertToUTType(NSArray* allowedTypes) + { + auto originalCount = [allowedTypes count]; + auto mapped = [[NSMutableArray alloc] init]; + + if (@available(macOS 11.0, *)) + { + for (int i = 0; i < originalCount; i++) + { + auto utTypeStr = allowedTypes[i]; + auto utType = [UTType typeWithIdentifier:utTypeStr]; + if (utType == nil) + { + utType = [UTType typeWithMIMEType:utTypeStr]; + } + if (utType != nil) + { + [mapped addObject:utType]; + } + } + } + + return mapped; + } }; diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 4802fed554..6cbe888d9b 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -112,7 +112,6 @@ namespace Avalonia.Native .Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) - .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index d1d9c17ae3..49ba652b1a 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -1,70 +1,114 @@ -using System; +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Native.Interop; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { - internal class SystemDialogs : ISystemDialogImpl + internal class SystemDialogs : BclStorageProvider { - IAvnSystemDialogs _native; + private readonly WindowBaseImpl _window; + private readonly IAvnSystemDialogs _native; - public SystemDialogs(IAvnSystemDialogs native) + public SystemDialogs(WindowBaseImpl window, IAvnSystemDialogs native) { + _window = window; _native = native; } - public Task ShowFileDialogAsync(FileDialog dialog, Window parent) + public override bool CanOpen => true; + + public override bool CanSave => true; + + public override bool CanPickFolder => true; + + public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - var events = new SystemDialogEvents(); + using var events = new SystemDialogEvents(); - var nativeParent = GetNativeWindow(parent); + var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true + ? suggestedDirectoryTmp.LocalPath : string.Empty; - if (dialog is OpenFileDialog ofd) - { - _native.OpenFileDialog(nativeParent, - events, ofd.AllowMultiple.AsComBool(), - ofd.Title ?? "", - ofd.Directory ?? "", - ofd.InitialFileName ?? "", - string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); - } - else - { - _native.SaveFileDialog(nativeParent, - events, - dialog.Title ?? "", - dialog.Directory ?? "", - dialog.InitialFileName ?? "", - string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty())); - } + _native.OpenFileDialog((IAvnWindow)_window.Native, + events, + options.AllowMultiple.AsComBool(), + options.Title ?? string.Empty, + suggestedDirectory, + string.Empty, + PrepareFilterParameter(options.FileTypeFilter)); + + var result = await events.Task.ConfigureAwait(false); + + return result?.Select(f => new BclStorageFile(new FileInfo(f))).ToArray() + ?? Array.Empty(); + } - return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); + public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + using var events = new SystemDialogEvents(); + + var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true + ? suggestedDirectoryTmp.LocalPath : string.Empty; + + _native.SaveFileDialog((IAvnWindow)_window.Native, + events, + options.Title ?? string.Empty, + suggestedDirectory, + options.SuggestedFileName ?? string.Empty, + PrepareFilterParameter(options.FileTypeChoices)); + + var result = await events.Task.ConfigureAwait(false); + return result.FirstOrDefault() is string file + ? new BclStorageFile(new FileInfo(file)) + : null; } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) + public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - var events = new SystemDialogEvents(); + using var events = new SystemDialogEvents(); - var nativeParent = GetNativeWindow(parent); + var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true + ? suggestedDirectoryTmp.LocalPath : string.Empty; - _native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.Directory ?? ""); + _native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory); - return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); }); + var result = await events.Task.ConfigureAwait(false); + return result?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() + ?? Array.Empty(); } - private IAvnWindow GetNativeWindow(Window window) + private static string PrepareFilterParameter(IReadOnlyList? fileTypes) { - return (window?.PlatformImpl as WindowImpl)?.Native; + return string.Join(";", + fileTypes?.SelectMany(f => + { + // On the native side we will try to parse identifiers or mimetypes. + if (f.AppleUniformTypeIdentifiers?.Any() == true) + { + return f.AppleUniformTypeIdentifiers; + } + else if (f.MimeTypes?.Any() == true) + { + // MacOS doesn't accept "all" type, so it's pointless to pass it. + return f.MimeTypes.Where(t => t != "*/*"); + } + + return Array.Empty(); + }) ?? + Array.Empty()); } } internal unsafe class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents { - private TaskCompletionSource _tcs; + private readonly TaskCompletionSource _tcs; public SystemDialogEvents() { @@ -83,7 +127,7 @@ namespace Avalonia.Native for (int i = 0; i < numResults; i++) { - results[i] = Marshal.PtrToStringAnsi(*ptr); + results[i] = Marshal.PtrToStringAnsi(*ptr) ?? string.Empty; ptr++; } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 94a3a5ed9b..bb0af0151e 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -11,6 +11,7 @@ using Avalonia.Input.Raw; using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; +using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Threading; @@ -45,7 +46,7 @@ namespace Avalonia.Native } internal abstract class WindowBaseImpl : IWindowBaseImpl, - IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost + IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { protected readonly IAvaloniaNativeFactory _factory; protected IInputRoot _inputRoot; @@ -73,6 +74,7 @@ namespace Avalonia.Native _keyboard = AvaloniaLocator.Current.GetService(); _mouse = new MouseDevice(); _cursorFactory = AvaloniaLocator.Current.GetService(); + StorageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs()); } protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) @@ -84,6 +86,7 @@ namespace Avalonia.Native if (_gpu) _glSurface = new GlPlatformSurface(window, _glContext); Screen = new ScreenImpl(screens); + _savedLogicalSize = ClientSize; _savedScaling = RenderScaling; _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); @@ -514,5 +517,7 @@ namespace Avalonia.Native public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0); public IPlatformHandle Handle { get; private set; } + + public IStorageProvider StorageProvider { get; } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index a1c73e1f03..a98d213887 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -641,9 +641,10 @@ interface IAvnSystemDialogEvents : IUnknown interface IAvnSystemDialogs : IUnknown { void SelectFolderDialog(IAvnWindow* parentWindowHandle, - IAvnSystemDialogEvents* events, - [const] char* title, - [const] char* initialPath); + IAvnSystemDialogEvents* events, + bool allowMultiple, + [const] char* title, + [const] char* initialPath); void OpenFileDialog(IAvnWindow* parentWindowHandle, IAvnSystemDialogEvents* events,