Browse Source

Merge branch 'master' into master

feature/pen-eraser-detection
Krzysztof Krysiński 10 months ago
committed by GitHub
parent
commit
3a048c310e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      samples/ControlCatalog/Pages/DialogsPage.xaml
  2. 72
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 15
      src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs
  4. 56
      src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
  5. 6
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  6. 9
      src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
  7. 9
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  8. 31
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  9. 20
      src/Avalonia.Native/StorageProviderApi.cs
  10. 2
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  11. 16
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  12. 17
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  13. 14
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs
  14. BIN
      tests/Avalonia.RenderTests/Assets/MiSans-Normal.ttf
  15. 23
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

6
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -31,6 +31,12 @@
<ComboBoxItem>TXT mime only</ComboBoxItem>
<ComboBoxItem>TXT apple type id only</ComboBoxItem>
</ComboBox>
<StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox Name="UseSuggestedFilter">Use SuggestedFileType</CheckBox>
<ComboBox Name="SuggestedFilterSelector" MinWidth="160">
<ComboBoxItem>First filter</ComboBoxItem>
</ComboBox>
</StackPanel>
<Expander Header="FilePicker API">
<StackPanel Spacing="4">
<CheckBox Name="ForceManaged">Force managed dialog</CheckBox>

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

@ -37,6 +37,8 @@ namespace ControlCatalog.Pages
var openedFileContent = OpenedFileContent;
var openMultiple = OpenMultiple;
var currentFolderBox = CurrentFolderBox;
var useSuggestedFilter = UseSuggestedFilter;
var suggestedFilterSelector = SuggestedFilterSelector;
currentFolderBox.TextChanged += async (sender, args) =>
{
@ -76,7 +78,7 @@ namespace ControlCatalog.Pages
}).ToList() ?? new List<FileDialogFilter>();
}
List<FilePickerFileType>? GetFileTypes()
List<FilePickerFileType>? BuildFileTypes()
{
var selectedItem = (FilterSelector.SelectedItem as ComboBoxItem)?.Content
?? "None";
@ -115,6 +117,64 @@ namespace ControlCatalog.Pages
};
}
List<FilePickerFileType>? GetFileTypes()
{
var types = BuildFileTypes();
UpdateSuggestedFilterSelector(types);
return types;
}
void UpdateSuggestedFilterSelector(IReadOnlyList<FilePickerFileType>? types)
{
var previouslySelected = (suggestedFilterSelector.SelectedItem as ComboBoxItem)?.Tag as FilePickerFileType;
suggestedFilterSelector.Items.Clear();
suggestedFilterSelector.Items.Add(new ComboBoxItem { Content = "First filter", Tag = null });
var desiredIndex = 0;
if (types is { Count: > 0 })
{
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
var item = new ComboBoxItem { Content = type.Name, Tag = type };
suggestedFilterSelector.Items.Add(item);
if (previouslySelected is not null && ReferenceEquals(previouslySelected, type))
{
desiredIndex = i + 1;
}
}
}
suggestedFilterSelector.SelectedIndex = desiredIndex;
}
FilePickerFileType? GetSuggestedFileType(IReadOnlyList<FilePickerFileType>? types)
{
if (useSuggestedFilter.IsChecked == true && types is { Count: > 0 })
{
if (suggestedFilterSelector.SelectedItem is ComboBoxItem { Tag: FilePickerFileType selectedType }
&& types.Any(t => ReferenceEquals(t, selectedType)))
{
return selectedType;
}
return types.FirstOrDefault();
}
return null;
}
void UpdateSuggestedFilterSelectorState() =>
suggestedFilterSelector.IsEnabled = useSuggestedFilter.IsChecked == true;
useSuggestedFilter.Checked += (_, _) => UpdateSuggestedFilterSelectorState();
useSuggestedFilter.Unchecked += (_, _) => UpdateSuggestedFilterSelectorState();
UpdateSuggestedFilterSelectorState();
FilterSelector.SelectionChanged += (_, _) => UpdateSuggestedFilterSelector(BuildFileTypes());
UpdateSuggestedFilterSelector(BuildFileTypes());
OpenFile.Click += async delegate
{
// Almost guaranteed to exist
@ -229,10 +289,12 @@ namespace ControlCatalog.Pages
OpenFilePicker.Click += async delegate
{
var fileTypes = GetFileTypes();
var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Open file",
FileTypeFilter = GetFileTypes(),
FileTypeFilter = fileTypes,
SuggestedFileType = GetSuggestedFileType(fileTypes),
SuggestedFileName = "FileName",
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
@ -243,10 +305,12 @@ namespace ControlCatalog.Pages
SaveFilePicker.Click += async delegate
{
var fileTypes = GetFileTypes();
var suggestedType = GetSuggestedFileType(fileTypes);
var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
{
Title = "Save file",
FileTypeChoices = fileTypes,
SuggestedFileType = suggestedType,
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
ShowOverwritePrompt = true
@ -278,10 +342,12 @@ namespace ControlCatalog.Pages
};
SaveFilePickerWithResult.Click += async delegate
{
var saveFileTypes = new[] { FilePickerFileTypes.Json, FilePickerFileTypes.Xml };
var result = await GetStorageProvider().SaveFilePickerWithResultAsync(new FilePickerSaveOptions()
{
Title = "Save file",
FileTypeChoices = [FilePickerFileTypes.Json, FilePickerFileTypes.Xml],
FileTypeChoices = saveFileTypes,
SuggestedFileType = GetSuggestedFileType(saveFileTypes),
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
ShowOverwritePrompt = true

15
src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs

@ -13,17 +13,26 @@ namespace Avalonia.Data.Converters
/// <typeparam name="TOut">The output type.</typeparam>
public class FuncMultiValueConverter<TIn, TOut> : IMultiValueConverter
{
private readonly Func<IEnumerable<TIn?>, TOut> _convert;
private readonly Func<IReadOnlyList<TIn?>, TOut> _convert;
/// <summary>
/// Initializes a new instance of the <see cref="FuncValueConverter{TIn, TOut}"/> class.
/// Initializes a new instance of the <see cref="FuncMultiValueConverter{TIn, TOut}"/> class.
/// </summary>
/// <param name="convert">The convert function.</param>
public FuncMultiValueConverter(Func<IEnumerable<TIn?>, TOut> convert)
public FuncMultiValueConverter(Func<IReadOnlyList<TIn?>, TOut> convert)
{
_convert = convert;
}
/// <summary>
/// Initializes a new instance of the <see cref="FuncMultiValueConverter{TIn, TOut}"/> class.
/// </summary>
/// <param name="convert">The convert function.</param>
public FuncMultiValueConverter(Func<IEnumerable<TIn?>, TOut> convert)
: this(new Func<IReadOnlyList<TIn?>, TOut>(convert))
{
}
/// <inheritdoc/>
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{

56
src/Avalonia.Base/Data/Converters/FuncValueConverter.cs

@ -13,16 +13,28 @@ namespace Avalonia.Data.Converters
public class FuncValueConverter<TIn, TOut> : IValueConverter
{
private readonly Func<TIn?, TOut> _convert;
private readonly Func<TOut?, TIn>? _convertBack;
/// <summary>
/// Initializes a new instance of the <see cref="FuncValueConverter{TIn, TOut}"/> class.
/// </summary>
/// <param name="convert">The convert function.</param>
/// <param name="convert">The function to convert TIn to TOut.</param>
public FuncValueConverter(Func<TIn?, TOut> convert)
{
_convert = convert;
}
/// <summary>
/// Initializes a new instance of the <see cref="FuncValueConverter{TIn, TOut}"/> class.
/// </summary>
/// <param name="convert">The function to convert TIn to TOut.</param>
/// <param name="convertBack">The function to convert TOut back to In.</param>
public FuncValueConverter(Func<TIn?, TOut> convert, Func<TOut?, TIn>? convertBack)
{
_convert = convert;
_convertBack = convertBack;
}
/// <inheritdoc/>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
@ -39,7 +51,19 @@ namespace Avalonia.Data.Converters
/// <inheritdoc/>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
if (_convertBack == null)
{
throw new NotImplementedException();
}
if (TypeUtilities.CanCast<TOut>(value))
{
return _convertBack((TOut?)value);
}
else
{
return AvaloniaProperty.UnsetValue;
}
}
}
@ -53,16 +77,28 @@ namespace Avalonia.Data.Converters
public class FuncValueConverter<TIn, TParam, TOut> : IValueConverter
{
private readonly Func<TIn?, TParam?, TOut> _convert;
private readonly Func<TOut?, TParam?, TIn>? _convertBack;
/// <summary>
/// Initializes a new instance of the <see cref="FuncValueConverter{TIn, TParam, TOut}"/> class.
/// </summary>
/// <param name="convert">The convert function.</param>
/// <param name="convert">The function to convert TIn to TOut.</param>
public FuncValueConverter(Func<TIn?, TParam?, TOut> convert)
{
_convert = convert;
}
/// <summary>
/// Initializes a new instance of the <see cref="FuncValueConverter{TIn, TParam, TOut}"/> class.
/// </summary>
/// <param name="convert">The function to convert TIn to TOut.</param>
/// <param name="convertBack">The function to convert TOut back to In.</param>
public FuncValueConverter(Func<TIn?, TParam?, TOut> convert, Func<TOut?, TParam?, TIn>? convertBack = null)
{
_convert = convert;
_convertBack = convertBack;
}
/// <inheritdoc/>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
@ -79,7 +115,19 @@ namespace Avalonia.Data.Converters
/// <inheritdoc/>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
if (_convertBack == null)
{
throw new NotImplementedException();
}
if (TypeUtilities.CanCast<TOut>(value) && TypeUtilities.CanCast<TParam>(parameter))
{
return _convertBack((TOut?)value, (TParam?)parameter);
}
else
{
return AvaloniaProperty.UnsetValue;
}
}
}
}

6
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -71,10 +71,16 @@ namespace Avalonia.Media.Fonts
if(matchedKey != key)
{
//Create a synthetic glyph typeface. The successfull result will be cached.
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
{
glyphTypeface = syntheticGlyphTypeface;
}
else
{
//Add the matched glyph typeface to the cache
glyphTypefaces.TryAdd(key, glyphTypeface);
}
}
return true;

9
src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs

@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public class FilePickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file type that should be preselected when the dialog is opened.
/// </summary>
/// <remarks>
/// This value should reference one of the items in <see cref="FileTypeChoices"/>.
/// If not set, the first file type in <see cref="FileTypeChoices"/> may be selected by default.
/// </remarks>
public FilePickerFileType? SuggestedFileType { get; set; }
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
/// </summary>

9
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -7,6 +7,15 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public class FilePickerSaveOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file type that should be preselected when the dialog is opened.
/// </summary>
/// <remarks>
/// This value should reference one of the items in <see cref="FileTypeChoices"/>.
/// If not set, the first file type in <see cref="FileTypeChoices"/> may be selected by default.
/// </remarks>
public FilePickerFileType? SuggestedFileType { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>

31
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -63,8 +63,13 @@ namespace Avalonia.FreeDesktop
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeFilter, out var filters))
if (TryParseFilters(options.FileTypeFilter, options.SuggestedFileType, out var filters,
out var currentFilter))
{
chooserOptions.Add("filters", filters);
if (currentFilter is { } filter)
chooserOptions.Add("current_filter", filter);
}
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
@ -106,8 +111,13 @@ namespace Avalonia.FreeDesktop
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeChoices, out var filters))
if (TryParseFilters(options.FileTypeChoices, options.SuggestedFileType, out var filters,
out var currentFilter))
{
chooserOptions.Add("filters", filters);
if (currentFilter is { } filter)
chooserOptions.Add("current_filter", filter);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", VariantValue.String(currentName));
@ -203,7 +213,10 @@ namespace Avalonia.FreeDesktop
.Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
}
private static bool TryParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes, out VariantValue result)
private static bool TryParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes,
FilePickerFileType? suggestedFileType,
out VariantValue result,
out VariantValue? currentFilter)
{
const uint GlobStyle = 0u;
const uint MimeStyle = 1u;
@ -212,10 +225,12 @@ namespace Avalonia.FreeDesktop
if (fileTypes is null)
{
result = default;
currentFilter = null;
return false;
}
var filters = new Array<Struct<string, Array<Struct<uint, string>>>>();
currentFilter = null;
foreach (var fileType in fileTypes)
{
@ -228,7 +243,15 @@ namespace Avalonia.FreeDesktop
else
continue;
filters.Add(Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions)));
var filterStruct = Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions));
filters.Add(filterStruct);
if (suggestedFileType is not null && ReferenceEquals(fileType, suggestedFileType))
{
currentFilter = VariantValue.Struct(
VariantValue.String(filterStruct.Item1),
filterStruct.Item2.AsVariantValue());
}
}
result = filters.AsVariantValue();

20
src/Avalonia.Native/StorageProviderApi.cs

@ -155,7 +155,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
public async Task<IReadOnlyList<IStorageFile>> OpenFileDialog(TopLevelImpl? topLevel, FilePickerOpenOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null);
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeFilter, null, options.SuggestedFileType);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var (items, _) = await OpenDialogAsync(events =>
@ -174,7 +174,7 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
public async Task<(IStorageFile? file, FilePickerFileType? selectedType)> SaveFileDialog(TopLevelImpl? topLevel, FilePickerSaveOptions options)
{
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension);
using var fileTypes = new FilePickerFileTypesWrapper(options.FileTypeChoices, options.DefaultExtension, options.SuggestedFileType);
var suggestedDirectory = options.SuggestedStartLocation?.Path.AbsoluteUri ?? string.Empty;
var (items, selectedFilterIndex) = await OpenDialogAsync(events =>
@ -237,15 +237,25 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
internal class FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension)
string? defaultExtension,
FilePickerFileType? suggestedType)
: NativeCallbackBase, IAvnFilePickerFileTypes
{
private readonly List<IDisposable> _disposables = new();
public int Count => types?.Count ?? 0;
public int IsDefaultType(int index) => (defaultExtension is not null &&
types![index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
public int IsDefaultType(int index)
{
if (types is null)
return false.AsComBool();
if (suggestedType is not null && ReferenceEquals(types[index], suggestedType))
return true.AsComBool();
return (defaultExtension is not null &&
types[index].TryGetExtensions()?.Any(defaultExtension.EndsWith) == true).AsComBool();
}
public int IsAnyType(int index) =>
(types![index].Patterns?.Contains("*.*") == true || types[index].MimeTypes?.Contains("*.*") == true)

2
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -101,6 +101,8 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_get_filter(IntPtr chooser);
[DllImport(GtkName)]
public static extern void gtk_file_chooser_set_filter(IntPtr chooser, IntPtr filter);
[DllImport(GtkName)]
public static extern void gtk_widget_realize(IntPtr gtkWidget);

16
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -40,7 +40,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.FileTypeFilter, null, false)
options.AllowMultiple, options.SuggestedStartLocation, null, options.SuggestedFileType, options.FileTypeFilter, null, false)
.ConfigureAwait(false);
return files?.Where(f => File.Exists(f)).Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ??
Array.Empty<IStorageFile>();
@ -53,7 +53,7 @@ namespace Avalonia.X11.NativeDialogs
{
var (folders, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
options.AllowMultiple, options.SuggestedStartLocation, null,
null, null, false)
null, null, null, false)
.ConfigureAwait(false);
return folders?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() ??
Array.Empty<IStorageFolder>();
@ -65,7 +65,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
false, options.SuggestedStartLocation, options.SuggestedFileName,options.SuggestedFileType, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return files?.FirstOrDefault() is { } file
@ -79,7 +79,7 @@ namespace Avalonia.X11.NativeDialogs
return await await RunOnGlibThread(async () =>
{
var (files, selectedFilter) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.SuggestedFileType, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
var file = files?.FirstOrDefault() is { } path
@ -92,7 +92,7 @@ namespace Avalonia.X11.NativeDialogs
private unsafe Task<(string[]? files, FilePickerFileType? selectedFilter)> ShowDialog(string? title,
IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName, FilePickerFileType? suggestedFileType,
IEnumerable<FilePickerFileType>? filters, string? defaultExtension, bool overwritePrompt)
{
IntPtr dlg;
@ -165,8 +165,14 @@ namespace Avalonia.X11.NativeDialogs
}
gtk_file_chooser_add_filter(dlg, filter);
if (suggestedFileType != null && suggestedFileType == f)
{
gtk_file_chooser_set_filter(dlg, filter);
}
}
}
}
disposables = new List<IDisposable>

17
src/Windows/Avalonia.Win32/Win32StorageProvider.cs

@ -4,6 +4,7 @@ using System.IO;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls.Utils;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Win32.Interop;
@ -33,7 +34,7 @@ namespace Avalonia.Win32
var (folders, _) = await ShowFilePicker(
true, true,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation, null, null,
options.Title, options.SuggestedFileName, null, options.SuggestedStartLocation, null, null,
f => new BclStorageFolder(new DirectoryInfo(f)))
.ConfigureAwait(false);
return folders;
@ -44,7 +45,7 @@ namespace Avalonia.Win32
var (files, _) = await ShowFilePicker(
true, false,
options.AllowMultiple, false,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
null, options.FileTypeFilter,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -56,7 +57,7 @@ namespace Avalonia.Win32
var (files, _) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -68,7 +69,7 @@ namespace Avalonia.Win32
var (files, index) = await ShowFilePicker(
false, false,
false, options.ShowOverwritePrompt,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedFileType, options.SuggestedStartLocation,
options.DefaultExtension, options.FileTypeChoices,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);
@ -88,6 +89,7 @@ namespace Avalonia.Win32
bool? showOverwritePrompt,
string? title,
string? suggestedFileName,
FilePickerFileType? suggestedFileType,
IStorageFolder? folder,
string? defaultExtension,
IReadOnlyList<FilePickerFileType>? filters,
@ -118,6 +120,7 @@ namespace Avalonia.Win32
{
options &= ~FILEOPENDIALOGOPTIONS.FOS_OVERWRITEPROMPT;
}
frm.SetOptions(options);
defaultExtension ??= string.Empty;
@ -152,6 +155,12 @@ namespace Avalonia.Win32
}
}
if (suggestedFileType != null &&
filters?.IndexOf(suggestedFileType) is { } fi and > -1)
{
frm.SetFileTypeIndex((uint)(fi + 1));
}
if (folder?.TryGetLocalPath() is { } folderPath)
{
var riid = UnmanagedMethods.ShellIds.IShellItem;

14
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs

@ -153,6 +153,20 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(",Bar,Baz", value);
}
[Fact]
public void MultiValueConverter_Supports_Indexing_The_Parameters()
{
var target = new FuncMultiValueConverter<string, string>(v => v[0]);
object value = target.Convert(new[] { "Foo", "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture);
Assert.Equal("Foo", value);
value = target.Convert(new[] { null, "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture);
Assert.Null(value);
}
private struct StringValueTypeWrapper
{

BIN
tests/Avalonia.RenderTests/Assets/MiSans-Normal.ttf

Binary file not shown.

23
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@ -19,6 +19,8 @@ namespace Avalonia.Skia.UnitTests.Media
private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope";
private const string s_misans = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#MiSans";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)]
@ -120,6 +122,27 @@ namespace Avalonia.Skia.UnitTests.Media
}
}
[Fact]
public void Should_Cache_Nearest_Match_For_MiSans()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri(s_misans, UriKind.Absolute);
var fontCollection = new TestEmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var regularGlyphTypeface));
Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Bold, FontStretch.Normal, out var boldGlyphTypeface));
Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("MiSans", out var glyphTypefaces));
Assert.Equal(3, glyphTypefaces.Count);
}
}
private class TestEmbeddedFontCollection : EmbeddedFontCollection
{
private bool _createSyntheticTypefaces;

Loading…
Cancel
Save