Browse Source

MacOS file type filter in native file dialog (#12899)

* Introduce IFilePickerFileTypes to have more control over file types in the native backend

* Update samples page for dialogs

* Rename to IAvnFilePickerFileTypes

* WIP

* Fix disabled popup

* Explicitly dispose AvnString and AvnStringArray + GetNSArrayOfStringsAndRelease

* Fix potential crash

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/13071/head
Max Katz 2 years ago
committed by GitHub
parent
commit
cd4bf7a02b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/src/OSX/AvnString.h
  2. 32
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 339
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  4. 10
      samples/ControlCatalog/Pages/DialogsPage.xaml
  5. 66
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  6. 3
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  7. 1
      src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
  8. 27
      src/Avalonia.Native/AvnString.cs
  9. 85
      src/Avalonia.Native/SystemDialogs.cs
  10. 17
      src/Avalonia.Native/avn.idl
  11. 2
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  12. 10
      src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

2
native/Avalonia.Native/src/OSX/AvnString.h

@ -15,4 +15,6 @@ extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
extern NSString* GetNSStringAndRelease(IAvnString* s);
extern NSString* GetNSStringWithoutRelease(IAvnString* s);
extern NSArray<NSString*>* GetNSArrayOfStringsAndRelease(IAvnStringArray* array);
#endif /* AvnString_h */

32
native/Avalonia.Native/src/OSX/AvnString.mm

@ -169,3 +169,35 @@ NSString* GetNSStringAndRelease(IAvnString* s)
return result;
}
NSString* GetNSStringWithoutRelease(IAvnString* s)
{
NSString* result = nil;
if (s != nullptr)
{
char* p;
if (s->Pointer((void**)&p) == S_OK && p != nullptr)
result = [NSString stringWithUTF8String:p];
}
return result;
}
NSArray<NSString*>* GetNSArrayOfStringsAndRelease(IAvnStringArray* array)
{
auto output = [NSMutableArray array];
if (array)
{
IAvnString* arrayItem;
for (int i = 0; i < array->GetCount(); i++)
{
if (array->Get(i, &arrayItem) == 0) {
NSString* ext = GetNSStringAndRelease(arrayItem);
[output addObject:ext];
}
}
array->Release();
}
return output;
}

339
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@ -1,9 +1,73 @@
#include "common.h"
#include "AvnString.h"
#include "INSWindowHolder.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
const int kFileTypePopupTag = 10975;
// Target for NSPopupButton control in file dialog's accessory view.
// ExtensionDropdownHandler is copied from Chromium MIT code of select_file_dialog_bridge
@interface ExtensionDropdownHandler : NSObject {
@private
// The file dialog to which this target object corresponds. Weak reference
// since the dialog_ will stay alive longer than this object.
NSSavePanel* _dialog;
// Two ivars serving the same purpose. While `_fileTypeLists` is for pre-macOS
// 11, and contains NSStrings with UTType identifiers, `_fileUTTypeLists` is
// for macOS 11 and later, and contains UTTypes.
NSArray<NSArray<NSString*>*>* __strong _fileTypeLists;
NSArray<NSArray<UTType*>*>* __strong _fileUTTypeLists
API_AVAILABLE(macos(11.0));
}
- (instancetype)initWithDialog:(NSSavePanel*)dialog
fileTypeLists:(NSArray<NSArray<NSString*>*>*)fileTypeLists;
- (instancetype)initWithDialog:(NSSavePanel*)dialog
fileUTTypeLists:(NSArray<NSArray<UTType*>*>*)fileUTTypeLists
API_AVAILABLE(macos(11.0));
- (void)popupAction:(id)sender;
@end
@implementation ExtensionDropdownHandler
- (instancetype)initWithDialog:(NSSavePanel*)dialog
fileTypeLists:(NSArray<NSArray<NSString*>*>*)fileTypeLists {
if ((self = [super init])) {
_dialog = dialog;
_fileTypeLists = fileTypeLists;
}
return self;
}
- (instancetype)initWithDialog:(NSSavePanel*)dialog
fileUTTypeLists:(NSArray<NSArray<UTType*>*>*)fileUTTypeLists
API_AVAILABLE(macos(11.0)) {
if ((self = [super init])) {
_dialog = dialog;
_fileUTTypeLists = fileUTTypeLists;
}
return self;
}
- (void)popupAction:(id)sender {
NSUInteger index = [sender indexOfSelectedItem];
if (@available(macOS 11, *)) {
_dialog.allowedContentTypes = [_fileUTTypeLists objectAtIndex:index];
} else {
_dialog.allowedFileTypes = [_fileTypeLists objectAtIndex:index];
}
}
@end
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
{
ExtensionDropdownHandler* __strong _extension_dropdown_handler;
public:
FORWARD_IUNKNOWN()
virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
@ -88,7 +152,7 @@ public:
const char* title,
const char* initialDirectory,
const char* initialFile,
const char* filters) override
IAvnFilePickerFileTypes* filters) override
{
@autoreleasepool
{
@ -113,25 +177,7 @@ public:
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
}
if(filters != nullptr)
{
auto filtersString = [NSString stringWithUTF8String:filters];
if(filtersString.length > 0)
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
}
}
SetAccessoryView(panel, filters, false);
auto handler = ^(NSModalResponse result) {
if(result == NSFileHandlingPanelOKButton)
@ -187,7 +233,7 @@ public:
const char* title,
const char* initialDirectory,
const char* initialFile,
const char* filters) override
IAvnFilePickerFileTypes* filters) override
{
@autoreleasepool
{
@ -210,28 +256,7 @@ public:
panel.nameFieldStringValue = [NSString stringWithUTF8String:initialFile];
}
if(filters != nullptr)
{
auto filtersString = [NSString stringWithUTF8String:filters];
if(filtersString.length > 0)
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
panel.allowsOtherFileTypes = false;
panel.extensionHidden = false;
}
}
SetAccessoryView(panel, filters, true);
auto handler = ^(NSModalResponse result) {
if(result == NSFileHandlingPanelOKButton)
@ -240,9 +265,9 @@ public:
auto url = [panel URL];
auto string = [url path];
auto string = [url path];
strings[0] = (void*)[string UTF8String];
events->OnCompleted(1, &strings[0]);
[panel orderOut:panel];
@ -274,31 +299,221 @@ public:
}
private:
NSMutableArray* ConvertToUTType(NSArray<NSString*>* allowedTypes)
NSView* CreateAccessoryView() {
// The label. Add attributes per-OS to match the labels that macOS uses.
NSTextField* label = [NSTextField labelWithString:@"File format"];
label.translatesAutoresizingMaskIntoConstraints = NO;
label.textColor = NSColor.secondaryLabelColor;
if (@available(macOS 11.0, *)) {
label.font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
}
// The popup.
NSPopUpButton* popup = [[NSPopUpButton alloc] initWithFrame:NSZeroRect
pullsDown:NO];
popup.translatesAutoresizingMaskIntoConstraints = NO;
popup.tag = kFileTypePopupTag;
[popup setAutoenablesItems:NO];
// A view to group the label and popup together. The top-level view used as
// the accessory view will be stretched horizontally to match the width of
// the dialog, and the label and popup need to be grouped together as one
// view to do centering within it, so use a view to group the label and
// popup.
NSView* group = [[NSView alloc] initWithFrame:NSZeroRect];
group.translatesAutoresizingMaskIntoConstraints = NO;
[group addSubview:label];
[group addSubview:popup];
// This top-level view will be forced by the system to have the width of the
// save dialog.
NSView* view = [[NSView alloc] initWithFrame:NSZeroRect];
view.translatesAutoresizingMaskIntoConstraints = NO;
[view addSubview:group];
NSMutableArray* constraints = [NSMutableArray array];
// The required constraints for the group, instantiated top-to-bottom:
// ┌───────────────────┐
// │ ↕︎ │
// │ ↔︎ label ↔︎ popup ↔︎ │
// │ ↕︎ │
// └───────────────────┘
// Top.
[constraints
addObject:[popup.topAnchor constraintEqualToAnchor:group.topAnchor
constant:10]];
// Leading.
[constraints
addObject:[label.leadingAnchor constraintEqualToAnchor:group.leadingAnchor
constant:10]];
// Horizontal and vertical baseline between the label and popup.
CGFloat labelPopupPadding;
if (@available(macOS 11.0, *)) {
labelPopupPadding = 8;
} else {
labelPopupPadding = 5;
}
[constraints addObject:[popup.leadingAnchor
constraintEqualToAnchor:label.trailingAnchor
constant:labelPopupPadding]];
[constraints
addObject:[popup.firstBaselineAnchor
constraintEqualToAnchor:label.firstBaselineAnchor]];
// Trailing.
[constraints addObject:[group.trailingAnchor
constraintEqualToAnchor:popup.trailingAnchor
constant:10]];
// Bottom.
[constraints
addObject:[group.bottomAnchor constraintEqualToAnchor:popup.bottomAnchor
constant:10]];
// Then the constraints centering the group in the accessory view. Vertical
// spacing is fully specified, but as the horizontal size of the accessory
// view will be forced to conform to the save dialog, only specify horizontal
// centering.
// ┌──────────────┐
// │ ↕︎ │
// │ ↔group↔︎ │
// │ ↕︎ │
// └──────────────┘
// Top.
[constraints
addObject:[group.topAnchor constraintEqualToAnchor:view.topAnchor]];
// Centering.
[constraints addObject:[group.centerXAnchor
constraintEqualToAnchor:view.centerXAnchor]];
// Bottom.
[constraints
addObject:[view.bottomAnchor constraintEqualToAnchor:group.bottomAnchor]];
[NSLayoutConstraint activateConstraints:constraints];
return view;
}
void SetAccessoryView(NSSavePanel* panel,
IAvnFilePickerFileTypes* filters,
bool is_save_panel)
{
auto originalCount = [allowedTypes count];
auto mapped = [[NSMutableArray alloc] init];
NSView* accessory_view = CreateAccessoryView();
NSPopUpButton* popup = [accessory_view viewWithTag:kFileTypePopupTag];
if (@available(macOS 11.0, *))
NSMutableArray<NSArray<NSString*>*>* file_type_lists = [NSMutableArray array];
NSMutableArray* file_uttype_lists = [NSMutableArray array];
int default_extension_index = -1;
for (int i = 0; i < filters->GetCount(); i++)
{
for (int i = 0; i < originalCount; i++)
{
auto utTypeStr = allowedTypes[i];
auto utType = [UTType typeWithIdentifier:utTypeStr];
if (utType == nil)
{
utType = [UTType typeWithMIMEType:utTypeStr];
NSString* type_description = GetNSStringAndRelease(filters->GetName(i));
[popup addItemWithTitle:type_description];
// If any type is included, enable allowsOtherFileTypes, and skip this filter on save panel.
if (filters->IsAnyType(i)) {
panel.allowsOtherFileTypes = YES;
}
// If default extension is specified, auto select it later.
if (filters->IsDefaultType(i)) {
default_extension_index = i;
}
IAvnStringArray* array;
// Prefer types priority of: file ext -> apple type id -> mime.
// On macOS 10 we only support file extensions.
if (@available(macOS 11, *)) {
NSMutableArray* file_uttype_array = [NSMutableArray array];
bool typeCompleted = false;
if (filters->IsAnyType(i)) {
UTType* type = [UTType typeWithIdentifier:@"public.item"];
[file_uttype_array addObject:type];
typeCompleted = true;
}
if (utType != nil)
{
[mapped addObject:utType];
if (!typeCompleted && filters->GetExtensions(i, &array) == 0) {
for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
{
UTType* type = [UTType typeWithFilenameExtension:ext];
if (type && ![file_uttype_array containsObject:type]) {
[file_uttype_array addObject:type];
typeCompleted = true;
}
}
}
if (!typeCompleted && filters->GetAppleUniformTypeIdentifiers(i, &array) == 0) {
for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
{
UTType* type = [UTType typeWithIdentifier:ext];
if (type && ![file_uttype_array containsObject:type]) {
[file_uttype_array addObject:type];
typeCompleted = true;
}
}
}
if (!typeCompleted && filters->GetMimeTypes(i, &array) == 0) {
for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
{
UTType* type = [UTType typeWithMIMEType:ext];
if (type && ![file_uttype_array containsObject:type]) {
[file_uttype_array addObject:type];
typeCompleted = true;
}
}
}
[file_uttype_lists addObject:file_uttype_array];
} else {
NSMutableArray<NSString*>* file_type_array = [NSMutableArray array];
if (filters->IsAnyType(i)) {
[file_type_array addObject:@"*.*"];
}
else if (filters->GetExtensions(i, &array) == 0) {
for (NSString* ext in GetNSArrayOfStringsAndRelease(array))
{
if (![file_type_array containsObject:ext]) {
[file_type_array addObject:ext];
}
}
}
[file_type_lists addObject:file_type_array];
}
}
if ([file_uttype_lists count] == 0 && [file_type_lists count] == 0)
return;
return mapped;
}
if (@available(macOS 11, *))
_extension_dropdown_handler = [[ExtensionDropdownHandler alloc] initWithDialog:panel
fileUTTypeLists:file_uttype_lists];
else
_extension_dropdown_handler = [[ExtensionDropdownHandler alloc] initWithDialog:panel
fileTypeLists:file_type_lists];
[popup setTarget: _extension_dropdown_handler];
[popup setAction: @selector(popupAction:)];
if (default_extension_index != -1) {
[popup selectItemAtIndex:default_extension_index];
} else {
// Select the first item.
[popup selectItemAtIndex:0];
}
[_extension_dropdown_handler popupAction:popup];
if (popup.numberOfItems > 0) {
panel.accessoryView = accessory_view;
}
};
};
extern IAvnSystemDialogs* CreateSystemDialogs()

10
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -22,7 +22,15 @@
<TextBlock Margin="0,20,0,0" Text="Pickers:" />
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Label Target="FilterSelector" Content="Filter" />
<ComboBox Name="FilterSelector" SelectedIndex="0">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>All + TXT + BinLog</ComboBoxItem>
<ComboBoxItem>Binlog</ComboBoxItem>
<ComboBoxItem>TXT extension only</ComboBoxItem>
<ComboBoxItem>TXT mime only</ComboBoxItem>
<ComboBoxItem>TXT apple type id only</ComboBoxItem>
</ComboBox>
<Expander Header="FilePicker API">
<StackPanel Spacing="4">
<CheckBox Name="ForceManaged">Force managed dialog</CheckBox>

66
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -59,37 +59,49 @@ namespace ControlCatalog.Pages
List<FileDialogFilter> GetFilters()
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "Text files (.txt)", Extensions = new List<string> {"txt"}
},
new FileDialogFilter
{
Name = "All files",
Extensions = new List<string> {"*"}
}
};
return GetFileTypes()?.Select(f => new FileDialogFilter
{
Name = f.Name, Extensions = f.Patterns!.ToList()
}).ToList() ?? new List<FileDialogFilter>();
}
List<FilePickerFileType>? GetFileTypes()
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FilePickerFileType>
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain,
new("Binary Log")
{
Patterns = new[] { "*.binlog", "*.buildlog" },
MimeTypes = new[] { "application/binlog", "application/buildlog" },
AppleUniformTypeIdentifiers = new []{ "public.data" }
}
};
var selectedItem = (this.Get<ComboBox>("FilterSelector").SelectedItem as ComboBoxItem)?.Content
?? "None";
var binLogType = new FilePickerFileType("Binary Log")
{
Patterns = new[] { "*.binlog", "*.buildlog" },
MimeTypes = new[] { "application/binlog", "application/buildlog" },
AppleUniformTypeIdentifiers = new[] { "public.data" }
};
return selectedItem switch
{
"All + TXT + BinLog" => new List<FilePickerFileType>
{
FilePickerFileTypes.All, FilePickerFileTypes.TextPlain, binLogType
},
"Binlog" => new List<FilePickerFileType> { binLogType },
"TXT extension only" => new List<FilePickerFileType>
{
new("TXT") { Patterns = FilePickerFileTypes.TextPlain.Patterns }
},
"TXT mime only" => new List<FilePickerFileType>
{
new("TXT") { MimeTypes = FilePickerFileTypes.TextPlain.MimeTypes }
},
"TXT apple type id only" => new List<FilePickerFileType>
{
new("TXT")
{
AppleUniformTypeIdentifiers =
FilePickerFileTypes.TextPlain.AppleUniformTypeIdentifiers
}
},
_ => null
};
}
this.Get<Button>("OpenFile").Click += async delegate

3
src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs

@ -47,10 +47,11 @@ public sealed class FilePickerFileType
internal IReadOnlyList<string>? TryGetExtensions()
{
// Converts random glob pattern to a simple extension name.
// GetExtension should be sufficient here.
// Path.GetExtension should be sufficient here,
// Only exception is "*.*proj" patterns that should be filtered as well.
return Patterns?.Select(Path.GetExtension)
.Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith("."))
.Select(e => e!.TrimStart('.'))
.ToArray()!;
}
}

1
src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs

@ -8,6 +8,7 @@ public static class FilePickerFileTypes
public static FilePickerFileType All { get; } = new("All")
{
Patterns = new[] { "*.*" },
AppleUniformTypeIdentifiers = new[] { "public.item" },
MimeTypes = new[] { "*/*" }
};

27
src/Avalonia.Native/AvnString.cs

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@ -15,7 +17,7 @@ namespace Avalonia.Native.Interop
string[] ToStringArray();
}
internal class AvnString : NativeCallbackBase, IAvnString
internal sealed class AvnString : NativeCallbackBase, IAvnString
{
private IntPtr _native;
private int _nativeLen;
@ -61,6 +63,29 @@ namespace Avalonia.Native.Interop
}
}
}
internal sealed class AvnStringArray : NativeCallbackBase, IAvnStringArray
{
private readonly IAvnString[] _items;
public AvnStringArray(IEnumerable<string> items)
{
_items = items.Select(s => s.ToAvnString()).ToArray();
}
public string[] ToStringArray() => _items.Select(n => n.String).ToArray();
public uint Count => (uint)_items.Length;
public IAvnString Get(uint index) => _items[(int)index];
protected override void Destroyed()
{
foreach (var item in _items)
{
item.Dispose();
}
}
}
}
namespace Avalonia.Native.Interop.Impl
{

85
src/Avalonia.Native/SystemDialogs.cs

@ -32,6 +32,7 @@ namespace Avalonia.Native
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
using var events = new SystemDialogEvents();
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
@ -41,7 +42,7 @@ namespace Avalonia.Native
options.Title ?? string.Empty,
suggestedDirectory,
string.Empty,
PrepareFilterParameter(options.FileTypeFilter));
fileTypes);
var result = await events.Task.ConfigureAwait(false);
@ -52,6 +53,7 @@ namespace Avalonia.Native
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
using var events = new SystemDialogEvents();
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
@ -60,7 +62,7 @@ namespace Avalonia.Native
options.Title ?? string.Empty,
suggestedDirectory,
options.SuggestedFileName ?? string.Empty,
PrepareFilterParameter(options.FileTypeChoices));
fileTypes);
var result = await events.Task.ConfigureAwait(false);
return result.FirstOrDefault() is string file
@ -80,26 +82,69 @@ namespace Avalonia.Native
return result?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray()
?? Array.Empty<IStorageFolder>();
}
}
internal class FilePickerFileTypesWrapper : NativeCallbackBase, IAvnFilePickerFileTypes
{
private readonly IReadOnlyList<FilePickerFileType>? _types;
private readonly string? _defaultExtension;
private readonly List<IDisposable> _disposables;
private static string PrepareFilterParameter(IReadOnlyList<FilePickerFileType>? fileTypes)
public FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension)
{
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<string>();
}) ??
Array.Empty<string>());
_types = types;
_defaultExtension = defaultExtension;
_disposables = new List<IDisposable>();
}
public int Count => _types?.Count ?? 0;
public int IsDefaultType(int index) => (_defaultExtension is not null &&
_types![index].TryGetExtensions()?.Any(ext => _defaultExtension.EndsWith(ext)) == true).AsComBool();
public int IsAnyType(int index) =>
(_types![index].Patterns?.Contains("*.*") == true || _types[index].MimeTypes?.Contains("*.*") == true)
.AsComBool();
public IAvnString GetName(int index)
{
return EnsureDisposable(_types![index].Name.ToAvnString());
}
public IAvnStringArray GetPatterns(int index)
{
return EnsureDisposable(new AvnStringArray(_types![index].Patterns ?? Array.Empty<string>()));
}
public IAvnStringArray GetExtensions(int index)
{
return EnsureDisposable(new AvnStringArray(_types![index].TryGetExtensions() ?? Array.Empty<string>()));
}
public IAvnStringArray GetMimeTypes(int index)
{
return EnsureDisposable(new AvnStringArray(_types![index].MimeTypes ?? Array.Empty<string>()));
}
public IAvnStringArray GetAppleUniformTypeIdentifiers(int index)
{
return EnsureDisposable(new AvnStringArray(_types![index].AppleUniformTypeIdentifiers ?? Array.Empty<string>()));
}
protected override void Destroyed()
{
foreach (var disposable in _disposables)
{
disposable.Dispose();
}
}
private T EnsureDisposable<T>(T input) where T : IDisposable
{
_disposables.Add(input);
return input;
}
}

17
src/Avalonia.Native/avn.idl

@ -871,14 +871,27 @@ interface IAvnSystemDialogs : IUnknown
[const] char* title,
[const] char* initialDirectory,
[const] char* initialFile,
[const] char* filters);
IAvnFilePickerFileTypes* filters);
void SaveFileDialog(IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,
[const] char* title,
[const] char* initialDirectory,
[const] char* initialFile,
[const] char* filters);
IAvnFilePickerFileTypes* filters);
}
[uuid(4d7ab7db-a111-406f-abeb-11cb6aa033d5)]
interface IAvnFilePickerFileTypes : IUnknown
{
int GetCount();
bool IsDefaultType(int index);
bool IsAnyType(int index);
IAvnString* GetName(int index);
HRESULT GetPatterns(int index, IAvnStringArray**ppv);
HRESULT GetExtensions(int index, IAvnStringArray**ppv);
HRESULT GetMimeTypes(int index, IAvnStringArray**ppv);
HRESULT GetAppleUniformTypeIdentifiers(int index, IAvnStringArray**ppv);
}
[uuid(9a52bc7a-d8c7-4230-8d34-704a0b70a933)]

2
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -147,7 +147,7 @@ internal class BrowserStorageProvider : IStorageProvider
{
var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.ToArray()))
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.Select(e => "." + e).ToArray()))
.ToArray();
if (types?.Length == 0)
{

10
src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs

@ -39,17 +39,17 @@ internal class IOSStorageProvider : IStorageProvider
{
// We check for OS version outside of the lambda, it's safe.
#pragma warning disable CA1416
if (f.AppleUniformTypeIdentifiers?.Any() == true)
if (f.TryGetExtensions() is { } extensions && extensions.Any())
{
return f.AppleUniformTypeIdentifiers.Select(id => UTType.CreateFromIdentifier(id));
return extensions.Select(UTType.CreateFromExtension);
}
if (f.TryGetExtensions() is { } extensions && extensions.Any())
if (f.AppleUniformTypeIdentifiers?.Any() == true)
{
return extensions.Select(id => UTType.CreateFromExtension(id.TrimStart('.')));
return f.AppleUniformTypeIdentifiers.Select(UTType.CreateFromIdentifier);
}
if (f.MimeTypes?.Any() == true)
{
return f.MimeTypes.Select(id => UTType.CreateFromMimeType(id));
return f.MimeTypes.Select(UTType.CreateFromMimeType);
}
return Array.Empty<UTType>();
#pragma warning restore CA1416

Loading…
Cancel
Save