From 905e620b820925363e62da11075910f82bccee16 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:13:14 +0800 Subject: [PATCH 1/7] Support Value pattern on editable ComboBox automation peer (#21545) * Support Value pattern on editable ComboBox automation peer * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Peers/ComboBoxAutomationPeer.cs | 34 +++++++- .../Automation/ComboBoxAutomationPeerTests.cs | 78 +++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Automation/ComboBoxAutomationPeerTests.cs diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs index 5d71e7a8e4..7f629db9e2 100644 --- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs @@ -22,19 +22,28 @@ namespace Avalonia.Automation.Peers public bool ShowsMenu => true; public void Collapse() => Owner.IsDropDownOpen = false; public void Expand() => Owner.IsDropDownOpen = true; - bool IValueProvider.IsReadOnly => true; + bool IValueProvider.IsReadOnly => !Owner.IsEditable; string? IValueProvider.Value { get { + if (Owner.IsEditable) + return Owner.Text; + var selection = GetSelection(); return selection.Count == 1 ? selection[0].GetName() : null; } } - - void IValueProvider.SetValue(string? value) => throw new NotSupportedException(); - + + void IValueProvider.SetValue(string? value) + { + if (!Owner.IsEditable) + throw new InvalidOperationException("Cannot set the value of a non-editable ComboBox."); + + Owner.SetCurrentValue(ComboBox.TextProperty, value); + } + protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.ComboBox; @@ -69,6 +78,23 @@ namespace Avalonia.Automation.Peers ToState((bool)e.OldValue!), ToState((bool)e.NewValue!)); } + else if (e.Property == ComboBox.TextProperty && Owner.IsEditable) + { + RaisePropertyChangedEvent( + ValuePatternIdentifiers.ValueProperty, + e.OldValue as string, + e.NewValue as string); + } + else if (e.Property == ComboBox.IsEditableProperty) + { + RaisePropertyChangedEvent( + ValuePatternIdentifiers.IsReadOnlyProperty, + !(bool)e.OldValue!, + !(bool)e.NewValue!); + + // The Value pattern switches between SelectedItem name and Text when IsEditable changes. + RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, null, null); + } } private static ExpandCollapseState ToState(bool value) diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ComboBoxAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ComboBoxAutomationPeerTests.cs new file mode 100644 index 0000000000..fde3f2f8bd --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/ComboBoxAutomationPeerTests.cs @@ -0,0 +1,78 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class ComboBoxAutomationPeerTests : ScopedTestBase +{ + private static IValueProvider Value(ComboBox comboBox) + => Assert.IsAssignableFrom(ControlAutomationPeer.CreatePeerForElement(comboBox)); + + [Fact] + public void Non_Editable_ComboBox_Is_ReadOnly() + { + var provider = Value(new ComboBox()); + + Assert.True(provider.IsReadOnly); + } + + [Fact] + public void Editable_ComboBox_Is_Not_ReadOnly() + { + var provider = Value(new ComboBox { IsEditable = true }); + + Assert.False(provider.IsReadOnly); + } + + [Fact] + public void Value_Returns_Text_When_Editable() + { + var provider = Value(new ComboBox { IsEditable = true, Text = "hello" }); + + Assert.Equal("hello", provider.Value); + } + + [Fact] + public void SetValue_Updates_Text_When_Editable() + { + var comboBox = new ComboBox { IsEditable = true }; + var provider = Value(comboBox); + + provider.SetValue("typed"); + + Assert.Equal("typed", comboBox.Text); + Assert.Equal("typed", provider.Value); + } + + [Fact] + public void SetValue_Throws_When_Not_Editable() + { + var provider = Value(new ComboBox()); + + Assert.Throws(() => provider.SetValue("x")); + } + + [Fact] + public void Text_Change_Raises_Value_PropertyChanged() + { + var comboBox = new ComboBox { IsEditable = true }; + var peer = ControlAutomationPeer.CreatePeerForElement(comboBox); + + var raised = 0; + peer.PropertyChanged += (_, e) => + { + if (e.Property == ValuePatternIdentifiers.ValueProperty) + { + Assert.Equal("abc", e.NewValue); + raised++; + } + }; + + comboBox.Text = "abc"; + + Assert.Equal(1, raised); + } +} From b7e173ab432075937327c75c614fde8f0417c3fe Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 11 Jun 2026 12:20:43 +0000 Subject: [PATCH 2/7] add support for hover events on android (#21551) --- src/Android/Avalonia.Android/AvaloniaView.Input.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/AvaloniaView.Input.cs b/src/Android/Avalonia.Android/AvaloniaView.Input.cs index c829be56be..4862daaf0b 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.Input.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.Input.cs @@ -27,7 +27,13 @@ namespace Avalonia.Android protected override bool DispatchHoverEvent(MotionEvent? e) { - return _accessHelper.DispatchHoverEvent(e!) || base.DispatchHoverEvent(e); + var res = _view.PointerHelper.DispatchMotionEvent(e, out var callBase); + if (res == false) + callBase = !_accessHelper.DispatchHoverEvent(e!) && callBase; + + var baseResult = callBase && base.DispatchHoverEvent(e); + + return res ?? baseResult; } protected override bool DispatchGenericPointerEvent(MotionEvent? e) From 289f9d76c8aaaffe82e49bb93798fc108c386ce9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 11 Jun 2026 06:39:22 -0700 Subject: [PATCH 3/7] Enhance Roslyn-compiler visible metadata (#21546) * Move "AdditionalFiles Include="@(AvaloniaXaml)"" to common AvaloniaBuildTasks * Add AdditionalFiles Include="@(AvaloniaResource)" * Add a comment * Add extra CompilerVisibleProperty * Rename _InjectAvaloniaAdditionalFiles, make it more specific... * Do not duplicate AdditionalFiles with AvaloniaXaml AvaloniaResource --- packages/Avalonia/AvaloniaBuildTasks.targets | 11 +++++++++++ .../Avalonia.Generators/Avalonia.Generators.props | 11 ++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index d3750935f9..9ed50e6137 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -39,6 +39,17 @@ Condition="'$(ApplicationIcon)' != ''" /> + + + + + + <_AvaloniaResourceOnly Include="@(AvaloniaResource)" + Exclude="@(AvaloniaXaml)" /> + + + + diff --git a/src/tools/Avalonia.Generators/Avalonia.Generators.props b/src/tools/Avalonia.Generators/Avalonia.Generators.props index 7eb3adff3d..3712b390ee 100644 --- a/src/tools/Avalonia.Generators/Avalonia.Generators.props +++ b/src/tools/Avalonia.Generators/Avalonia.Generators.props @@ -9,7 +9,9 @@ true + + @@ -17,10 +19,9 @@ + + + + - - - - - From 486df8acbadadcbc6ab462e29ece840ddacc65e6 Mon Sep 17 00:00:00 2001 From: deryaza <46353085+deryaza@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:44:02 +0400 Subject: [PATCH 4/7] Fix mishandled cancellation in DBusTextInputMethodBase (#21527) * Fix mishandled cancellation in DBusTextInputMethodBase * Revert "Fix mishandled cancellation in DBusTextInputMethodBase" This reverts commit ac18b6d878d08abb8bd19c12fd6cb933b3e37da6. * review --- .../DBusIme/DBusTextInputMethodBase.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index cfd28b29aa..236276f805 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -64,9 +64,9 @@ namespace Avalonia.FreeDesktop.DBusIme _watchCts = new CancellationTokenSource(); var cancellationToken = _watchCts.Token; foreach (var name in _knownNames) - WatchNameAsync(name); + _ = WatchNameAsync(name); - async void WatchNameAsync(string name) + async Task WatchNameAsync(string name) { try { @@ -99,10 +99,13 @@ namespace Avalonia.FreeDesktop.DBusIme OnOwnerChanged(name, owner); } } - catch (Exception e) when (!cancellationToken.IsCancellationRequested) + catch (Exception e) { - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform) - ?.Log(this, $"WatchNameOwner for '{name}' failed: {e}"); + if (!cancellationToken.IsCancellationRequested) + { + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform) + ?.Log(this, $"WatchNameOwner for '{name}' failed: {e}"); + } } } } From e00089a29fa6894f5271702bb6e3a3be9688360d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 11 Jun 2026 17:14:54 +0200 Subject: [PATCH 5/7] Fix caption button background in dark mode (#21555) --- src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml | 2 +- src/Avalonia.Themes.Simple/Accents/Base.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml index 80778a50db..f7d04ccd9b 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -1651,7 +1651,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index c81ba824d8..8b10f2735e 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -90,7 +90,7 @@ - + From 40033c104ffe1f5dc053224b4cc4f927261a0aee Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 12 Jun 2026 03:21:46 +0800 Subject: [PATCH 6/7] Expose placeholder text as the AT-SPI placeholder-text attribute (#21554) * Expose placeholder text to UIA and AT-SPI automation * Drop the UIA PlaceholderText property; placeholder is already exposed via HelpText on Windows --- .../Handlers/AtSpiAccessibleHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs b/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs index 4e674a9ed5..ecca97de79 100644 --- a/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs +++ b/src/Avalonia.FreeDesktop.AtSpi/Handlers/AtSpiAccessibleHandler.cs @@ -150,6 +150,10 @@ namespace Avalonia.FreeDesktop.AtSpi.Handlers if (!string.IsNullOrEmpty(accessKey)) attrs["access-key"] = accessKey; + var placeholderText = node.Peer.GetPlaceholderText(); + if (!string.IsNullOrEmpty(placeholderText)) + attrs["placeholder-text"] = placeholderText; + return ValueTask.FromResult(attrs); } From baa1e0127723f8ffccd01b935c14a3b3dd670f5d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 12 Jun 2026 09:37:33 +0200 Subject: [PATCH 7/7] Fix system font caching when retrieved with a different name (#21557) * Add failing cache test for font retrieved with different name * Fix system font caching when retrieved with a different name --- .../Media/Fonts/SystemFontCollection.cs | 4 + .../Media/FontManagerTests.cs | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index c7b013729c..390f4f4719 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -67,6 +67,10 @@ namespace Avalonia.Media.Fonts //Add to cache with platform typeface family name first TryAddGlyphTypeface(platformTypeface.FamilyName, key, glyphTypeface); + + // Then the requested family name + if (familyName != platformTypeface.FamilyName) + TryAddGlyphTypeface(familyName, key, glyphTypeface); //Add to cache if (!TryAddGlyphTypeface(glyphTypeface)) diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index b0d6e1bfd1..4f171b29ad 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; using Avalonia.Fonts.Inter; using Avalonia.Logging; using Avalonia.Media; @@ -643,5 +646,108 @@ namespace Avalonia.Skia.UnitTests.Media Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, (FontWeight)650), out var nearestMatchTypeface)); Assert.Same(boldGlyphTypeface, nearestMatchTypeface); } + + [Fact] + public void TryGetGlyphTypeface_Should_Cache_Matched_GlyphTypeface_Under_Requested_FamilyName() + { + var fontManagerImpl = new FamilyRemappingFontManagerImpl("NotInstalled", "Noto Mono"); + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl)); + + // "NotInstalled" is not installed, so the platform substitutes it with a different + // family ("Noto Mono"), much like requesting "Arial" yields "Liberation Sans" on some Linux distributions. + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("NotInstalled"), out var first)); + Assert.Equal("Noto Mono", first.FamilyName); + + // The substitute should now be cached under the requested "NotInstalled" name, so a second + // lookup must resolve from the cache instead of asking the platform again. + Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("NotInstalled"), out var second)); + Assert.Same(first, second); + Assert.Equal(1, fontManagerImpl.RequestedFamilyCreateCount); + } + + /// + /// A font manager whose every by-name lookup resolves to a single matched font whose family name + /// differs from the requested one. + /// + private sealed class FamilyRemappingFontManagerImpl(string requestedFamilyName, string matchedFamilyName) + : IFontManagerImpl, IDisposable + { + public int RequestedFamilyCreateCount { get; private set; } + + public string GetDefaultFontFamilyName() + => matchedFamilyName; + + public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false) + => [matchedFamilyName]; + + public bool TryCreateGlyphTypeface( + string familyName, + FontStyle style, + FontWeight weight, + FontStretch stretch, + [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + if (string.Equals(familyName, requestedFamilyName, StringComparison.OrdinalIgnoreCase)) + RequestedFamilyCreateCount++; + + platformTypeface = new SkiaTypeface(CreateMatchedTypeface(), FontSimulations.None); + return true; + } + + public bool TryCreateGlyphTypeface( + Stream stream, + FontSimulations fontSimulations, + [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = new SkiaTypeface(SKTypeface.FromStream(stream), fontSimulations); + return true; + } + + public bool TryMatchCharacter( + int codepoint, + FontStyle fontStyle, + FontWeight fontWeight, + FontStretch fontStretch, + string? familyName, + CultureInfo? culture, + [NotNullWhen(true)] out IPlatformTypeface? platformTypeface) + { + platformTypeface = null; + return false; + } + + public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces) + { + familyTypefaces = null; + return false; + } + + private SKTypeface CreateMatchedTypeface() + { + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); + + // LoadFontAssets ignores the family fragment and returns every embedded font asset, + // so pick the one whose family name matches the substitute we want to return. + foreach (var fontAsset in FontFamilyLoader.LoadFontAssets(new Uri(s_fontUri))) + { + var stream = assetLoader.Open(fontAsset); + var typeface = SKTypeface.FromStream(stream); + + if (typeface is not null && + string.Equals(typeface.FamilyName, matchedFamilyName, StringComparison.OrdinalIgnoreCase)) + { + return typeface; + } + + typeface?.Dispose(); + } + + throw new InvalidOperationException($"Could not load the '{matchedFamilyName}' font asset."); + } + + public void Dispose() + { + } + } } }