Skip to content

Commit 95a009e

Browse files
ionite34lykos-code
andcommitted
feat: accessible names for icon-only controls and --overlay-popups flag
UI Automation (and screen readers) saw icon-only buttons as their content's ToString - "FluentAvalonia.UI.Controls.SymbolIcon" - making them indistinguishable. Adds AutomationProperties.Name to the package card action buttons, the DocsHelpButton theme, and the NavigationView pane toggle. Adds --overlay-popups to render popups inside the window on Windows (already the default on Linux/macOS), so window captures and the window's automation subtree include open flyouts and dropdowns. Platform options are now composed into one instance per platform before applying: .With<T>() replaces the registered options wholesale, so the per-flag instances were silently dropping earlier settings (X11 OverlayPopups under --disable-gpu-rendering, macOS OverlayPopups under any rendering flag). Co-Authored-By: Lykos (Fable 5) <noreply@lykos.ai>
1 parent 79b61f5 commit 95a009e

6 files changed

Lines changed: 90 additions & 38 deletions

File tree

StabilityMatrix.Avalonia/Models/AppArgs.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public class AppArgs
7474
[Option("vulkan", HelpText = "Prefer Vulkan rendering")]
7575
public bool UseVulkanRendering { get; set; }
7676

77+
/// <summary>
78+
/// Flag to render popups (flyouts, dropdowns, menus) inside the window instead of as
79+
/// separate OS windows, so window captures and UI automation can see them.
80+
/// Popups are clipped to the window bounds in this mode.
81+
/// </summary>
82+
[Option("overlay-popups", HelpText = "Render popups inside the window (for UI automation / capture)")]
83+
public bool UseOverlayPopups { get; set; }
84+
7785
/// <summary>
7886
/// Override global app home directory
7987
/// Defaults to (%APPDATA%|~/.config)/StabilityMatrix

StabilityMatrix.Avalonia/Program.cs

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -236,56 +236,31 @@ public static AppBuilder BuildAvaloniaApp()
236236

237237
var app = AppBuilder.Configure<App>().UsePlatformDetect().WithInterFont().LogToTrace();
238238

239-
if (Compat.IsLinux)
240-
{
241-
app = app.With(new X11PlatformOptions { OverlayPopups = true, WmClass = "stabilitymatrix" });
242-
}
243-
else if (Compat.IsMacOS)
244-
{
245-
app = app.With(new AvaloniaNativePlatformOptions { OverlayPopups = true });
246-
}
239+
// .With<T>() replaces the registered options instance wholesale, so each platform's
240+
// options must be composed into a single object before applying.
241+
var win32Options = new Win32PlatformOptions { OverlayPopups = Args.UseOverlayPopups };
242+
var x11Options = new X11PlatformOptions { OverlayPopups = true, WmClass = "stabilitymatrix" };
243+
var macOptions = new AvaloniaNativePlatformOptions { OverlayPopups = true };
247244

248245
if (Args.UseOpenGlRendering)
249246
{
250-
app = app.With(
251-
new Win32PlatformOptions
252-
{
253-
RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software],
254-
}
255-
);
247+
win32Options.RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software];
256248
}
257249

258250
if (Args.UseVulkanRendering)
259251
{
260-
app = app.With(
261-
new X11PlatformOptions
262-
{
263-
RenderingMode = [X11RenderingMode.Vulkan],
264-
WmClass = "stabilitymatrix",
265-
}
266-
)
267-
.With(new Win32PlatformOptions { RenderingMode = [Win32RenderingMode.Vulkan] });
252+
win32Options.RenderingMode = [Win32RenderingMode.Vulkan];
253+
x11Options.RenderingMode = [X11RenderingMode.Vulkan];
268254
}
269255

270256
if (Args.DisableGpuRendering)
271257
{
272-
app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } })
273-
.With(
274-
new X11PlatformOptions
275-
{
276-
RenderingMode = new[] { X11RenderingMode.Software },
277-
WmClass = "stabilitymatrix",
278-
}
279-
)
280-
.With(
281-
new AvaloniaNativePlatformOptions
282-
{
283-
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software },
284-
}
285-
);
258+
win32Options.RenderingMode = [Win32RenderingMode.Software];
259+
x11Options.RenderingMode = [X11RenderingMode.Software];
260+
macOptions.RenderingMode = [AvaloniaNativeRenderingMode.Software];
286261
}
287262

288-
return app;
263+
return app.With(win32Options).With(x11Options).With(macOptions);
289264
}
290265

291266
private static void HandleUpdateReplacement()

StabilityMatrix.Avalonia/Styles/ControlThemes/DocsHelpButtonStyles.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Setter Property="VerticalAlignment" Value="Center" />
2626
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
2727
<Setter Property="ToolTip.Tip" Value="{x:Static lang:Resources.Label_ViewDocumentation}" />
28+
<Setter Property="AutomationProperties.Name" Value="{x:Static lang:Resources.Label_ViewDocumentation}" />
2829

2930
<Setter Property="Template">
3031
<ControlTemplate>

StabilityMatrix.Avalonia/Views/MainWindow.axaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
<KeyBinding Command="{Binding OpenDocumentationCommand}" Gesture="F1" />
3939
</controls:AppWindowBase.KeyBindings>
4040

41+
<controls:AppWindowBase.Styles>
42+
<!-- FluentAvalonia's template gives this icon-only button no accessible name -->
43+
<Style Selector="ui|NavigationView /template/ Button#TogglePaneButton">
44+
<Setter Property="AutomationProperties.Name" Value="Toggle Navigation" />
45+
</Style>
46+
</controls:AppWindowBase.Styles>
47+
4148
<Grid RowDefinitions="Auto,Auto,Auto,*">
4249
<Grid
4350
Name="TitleBarHost"

StabilityMatrix.Avalonia/Views/PackageManager/MainPackageManagerView.axaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@
113113
VerticalAlignment="Top"
114114
HorizontalContentAlignment="Right"
115115
VerticalContentAlignment="Top"
116+
AutomationProperties.Name="More Options"
116117
BorderThickness="0"
117-
Classes="transparent">
118+
Classes="transparent"
119+
ToolTip.Tip="More Options">
118120
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
119121
<Button.Flyout>
120122
<MenuFlyout Placement="BottomEdgeAlignedLeft">
@@ -347,6 +349,7 @@
347349
<Button
348350
Margin="4,4,4,0"
349351
Padding="6"
352+
AutomationProperties.Name="{x:Static lang:Resources.Action_CheckForUpdates}"
350353
Classes="transparent"
351354
Command="{Binding OnLoadedAsync}"
352355
ToolTip.Tip="{x:Static lang:Resources.Action_CheckForUpdates}">
@@ -363,6 +366,7 @@
363366
<Button
364367
Margin="4,4,4,0"
365368
Padding="4"
369+
AutomationProperties.Name="{x:Static lang:Resources.Action_Update}"
366370
Classes="borderless-success"
367371
Command="{Binding Update}"
368372
IsEnabled="{Binding !IsRunning}"
@@ -414,6 +418,7 @@
414418
<Button
415419
Margin="4,4,4,0"
416420
Padding="6"
421+
AutomationProperties.Name="Launch Options"
417422
Classes="transparent"
418423
Command="{Binding ShowLaunchOptionsCommand}"
419424
IsVisible="{Binding !IsUnknownPackage}"
@@ -423,6 +428,7 @@
423428
<Button
424429
Margin="4,4,4,0"
425430
Padding="6"
431+
AutomationProperties.Name="Extensions"
426432
Classes="transparent"
427433
Command="{Binding OpenExtensionsDialogCommand}"
428434
IsVisible="{Binding CanUseExtensions}"
@@ -444,6 +450,7 @@
444450
Name="LaunchButton"
445451
HorizontalAlignment="Stretch"
446452
VerticalAlignment="Bottom"
453+
AutomationProperties.Name="{x:Static lang:Resources.Action_Launch}"
447454
Classes="accent"
448455
Command="{Binding Launch}">
449456
<Button.IsVisible>
@@ -465,6 +472,7 @@
465472
Name="SplitLaunchButton"
466473
HorizontalAlignment="Stretch"
467474
VerticalAlignment="Bottom"
475+
AutomationProperties.Name="{x:Static lang:Resources.Action_Launch}"
468476
Classes="accent"
469477
Command="{Binding Launch}">
470478
<Button.IsVisible>
@@ -503,6 +511,7 @@
503511
<Button
504512
HorizontalAlignment="Stretch"
505513
VerticalAlignment="Bottom"
514+
AutomationProperties.Name="{x:Static lang:Resources.Action_Stop}"
506515
Classes="borderless-danger"
507516
Command="{Binding StopCommand}"
508517
IsVisible="{Binding IsRunning}">
@@ -517,6 +526,7 @@
517526
<Button
518527
HorizontalAlignment="Stretch"
519528
VerticalAlignment="Bottom"
529+
AutomationProperties.Name="{x:Static lang:Resources.Action_Restart}"
520530
BorderBrush="Transparent"
521531
Classes="borderless-info"
522532
Command="{Binding RestartCommand}"
@@ -532,6 +542,7 @@
532542
<Button
533543
HorizontalAlignment="Stretch"
534544
VerticalAlignment="Bottom"
545+
AutomationProperties.Name="{x:Static lang:Resources.Label_Console}"
535546
Classes="accent"
536547
Command="{Binding NavToConsole}"
537548
IsVisible="{Binding IsRunning}">
@@ -546,6 +557,7 @@
546557
<Button
547558
HorizontalAlignment="Stretch"
548559
VerticalAlignment="Bottom"
560+
AutomationProperties.Name="{x:Static lang:Resources.Label_WebUi}"
549561
Classes="accent"
550562
Command="{Binding LaunchWebUi}"
551563
IsVisible="{Binding ShowWebUiButton}">
@@ -560,6 +572,7 @@
560572
<Button
561573
HorizontalAlignment="Stretch"
562574
VerticalAlignment="Bottom"
575+
AutomationProperties.Name="{x:Static lang:Resources.Action_Import}"
563576
Classes="transparent-info"
564577
Command="{Binding Import}"
565578
IsVisible="{Binding IsUnknownPackage}">
@@ -641,6 +654,7 @@
641654
Margin="0,8,0,0"
642655
HorizontalAlignment="Stretch"
643656
VerticalAlignment="Bottom"
657+
AutomationProperties.Name="{x:Static lang:Resources.Action_AddPackage}"
644658
Classes="transparent"
645659
Command="{Binding ShowInstallDialog}">
646660
<StackPanel Margin="8" Orientation="Horizontal">
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Avalonia.Automation;
2+
using Avalonia.Controls;
3+
using Avalonia.VisualTree;
4+
using FluentAvalonia.UI.Controls;
5+
using StabilityMatrix.Avalonia.Controls;
6+
using StabilityMatrix.Avalonia.Languages;
7+
using StabilityMatrix.Core.Models.Documentation;
8+
9+
namespace StabilityMatrix.UITests;
10+
11+
[Collection("TempDir")]
12+
public class AutomationNameTests : TestBase
13+
{
14+
/// <summary>
15+
/// The pane toggle is named via a /template/ style selector in MainWindow.axaml, which
16+
/// silently matches nothing if FluentAvalonia renames the template part.
17+
/// </summary>
18+
[AvaloniaFact]
19+
public async Task NavigationTogglePaneButton_ShouldHaveAutomationName()
20+
{
21+
var (window, _) = GetMainWindow();
22+
await DoInitialSetup();
23+
24+
var toggleButton = window
25+
.FindDescendantOfType<NavigationView>()!
26+
.GetVisualDescendants()
27+
.OfType<Button>()
28+
.FirstOrDefault(b => b.Name == "TogglePaneButton");
29+
30+
Assert.NotNull(toggleButton);
31+
Assert.Equal("Toggle Navigation", AutomationProperties.GetName(toggleButton));
32+
}
33+
34+
/// <summary>
35+
/// Icon-only control: the accessible name comes from the control theme, not content.
36+
/// </summary>
37+
[AvaloniaFact]
38+
public void DocsHelpButton_ShouldHaveAutomationName()
39+
{
40+
var button = new DocsHelpButton { DocsPath = DocumentationPages.EnvironmentVariables };
41+
var window = new Window { Content = button };
42+
43+
window.Show();
44+
45+
Assert.Equal(Resources.Label_ViewDocumentation, AutomationProperties.GetName(button));
46+
}
47+
}

0 commit comments

Comments
 (0)