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/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)
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/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/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);
}
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}");
+ }
}
}
}
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 @@
-
+
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 @@
+
+
+
+
-
-
-
-
-
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);
+ }
+}
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()
+ {
+ }
+ }
}
}