Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ButtonTestApp-Linux-Properties-Fixed.zip
Binary file not shown.
105 changes: 105 additions & 0 deletions FINAL-AT-SPI-IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# LinuxWindowAutomationPeer AT-SPI Implementation - Final Version

## What We Fixed

The core issue was that **Windows were not getting AT-SPI root support**. Previous implementations had:

1. `LinuxControlAutomationPeer` handling both windows and child controls
2. Windows getting basic `WindowAutomationPeer` instead of Linux AT-SPI peers
3. Child controls couldn't access AT-SPI root context because windows didn't provide it

## New Architecture

### 1. LinuxWindowAutomationPeer.cs
- **Purpose**: Dedicated AT-SPI root provider for Window controls
- **Key Features**:
- Inherits from `WindowAutomationPeer` for proper window behavior
- Registers with `AtspiRoot.RegisterRoot()` to become the AT-SPI application root
- Provides AT-SPI root context that child controls can access
- Can wrap existing `WindowAutomationPeer` instances for compatibility

### 2. Updated LinuxAutomationPeerFactory.cs
- **Window Handling**: Detects `Window` controls and creates `LinuxWindowAutomationPeer`
- **Child Control Handling**: Creates `LinuxControlAutomationPeer` for non-window controls
- **Strategy**: Windows get AT-SPI root support, child controls connect to existing root

### 3. Updated LinuxControlAutomationPeer.cs
- **Simplified Logic**: No longer tries to register as AT-SPI root for windows
- **Root Access**: Uses `AtspiRoot.Current` to connect to window-provided root
- **Error Handling**: Clear logging when AT-SPI root is not available

## How It Works

1. **Application Startup**:
- `LinuxAutomationPeerFactory` is registered as the automation peer factory
- When `MainWindow` creates automation peer, factory detects it's a `Window`
- Factory creates `LinuxWindowAutomationPeer` which registers as AT-SPI root

2. **Child Control Registration**:
- Child controls (Button, TextBlock, etc.) create automation peers via factory
- Factory creates `LinuxControlAutomationPeer` for non-window controls
- Child peers access the AT-SPI root created by the window peer
- Each child control gets its own AT-SPI context via `AtspiRoot.Current.CreateAutomationContext()`

3. **AT-SPI Tree Structure**:
```
AtSpiTestApp (LinuxWindowAutomationPeer - AT-SPI root)
├── StackPanel (LinuxControlAutomationPeer)
├── TextBlock "Hello AT-SPI!" (LinuxControlAutomationPeer)
└── Button "Click Me" (LinuxControlAutomationPeer)
```

## Expected Debug Output

```
=== LinuxAutomationPeerFactory.CreateAutomationPeer called for MainWindow ===
Creating Linux window automation peer for: MainWindow
Creating LinuxWindowAutomationPeer without wrapped peer
=== LinuxWindowAutomationPeer created for MainWindow ===
=== Initializing AT-SPI Root for Window ===
✅ Window owner confirmed, registering with AtspiRoot
✅ Window successfully registered with AT-SPI root

=== LinuxAutomationPeerFactory.CreateAutomationPeer called for StackPanel ===
Creating Linux control automation peer for: StackPanel
✅ AT-SPI root available, creating context for control...
✅ AT-SPI context created: True

=== LinuxAutomationPeerFactory.CreateAutomationPeer called for TextBlock ===
Creating Linux control automation peer for: TextBlock
✅ AT-SPI root available, creating context for control...
✅ AT-SPI context created: True

=== LinuxAutomationPeerFactory.CreateAutomationPeer called for Button ===
Creating Linux control automation peer for: Button
✅ AT-SPI root available, creating context for control...
✅ AT-SPI context created: True
```

## Testing

### Deploy to Linux:
1. Copy `AtSpiTestApp-WindowPeer-Final.zip` to Linux machine
2. Extract: `unzip AtSpiTestApp-WindowPeer-Final.zip`
3. Make executable: `chmod +x AtSpiTestApp`
4. Run: `./AtSpiTestApp`

### Verify AT-SPI Integration:
1. Check if app appears in AT-SPI tree: `accerciser`
2. Expected: "AtSpiTestApp" should appear alongside system applications
3. Expected: Child controls (TextBlock, Button) should be visible in the tree
4. Expected: Debug output should show successful AT-SPI root registration

### Key Success Indicators:
- ✅ Application appears in accerciser accessibility tree
- ✅ Window is registered as AT-SPI root (not child controls)
- ✅ Child controls can access AT-SPI root and create contexts
- ✅ No "No AT-SPI root available" errors for child controls
- ✅ Clear hierarchy: Window → Child Controls

## Files Modified:
- `src/Avalonia.FreeDesktop/LinuxWindowAutomationPeer.cs` (NEW)
- `src/Avalonia.FreeDesktop/IAutomationPeerFactory.cs` (UPDATED)
- `src/Avalonia.FreeDesktop/LinuxControlAutomationPeer.cs` (UPDATED)

This implementation follows the correct AT-SPI architecture where the application window serves as the root accessible object, and all child controls register as children of that root.
1 change: 1 addition & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<packageSources>
<clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="AvaloniaLocal" value="C:\Users\akhatri\Documents\Avalonia\artifacts\nuget" />
</packageSources>
</configuration>
2 changes: 1 addition & 1 deletion native/Avalonia.Native/generate-headers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd $SCRIPT_DIR/../..
dotnet run --project ./nukebuild/_build.csproj --target GenerateCppHeaders

testkey
25 changes: 25 additions & 0 deletions samples/ButtonTestApp/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Styling;

namespace ButtonTestApp;

public class App : Application
{
public override void Initialize()
{
// Pure code - no XAML
Styles.Add(new Avalonia.Themes.Fluent.FluentTheme());
}

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}

base.OnFrameworkInitializationCompleted();
}
}
17 changes: 17 additions & 0 deletions samples/ButtonTestApp/ButtonTestApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions samples/ButtonTestApp/MainWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using Avalonia.Controls;

namespace ButtonTestApp;

public class MainWindow : Window
{
public MainWindow()
{
Console.WriteLine("🏠 MainWindow constructor called");

Title = "Button Test App";
Width = 300;
Height = 200;

Console.WriteLine("🔧 Creating button content...");

// Create a single button - nothing else
var button = new Button
{
Content = "Test Button",
Width = 120,
Height = 40,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};

Console.WriteLine($"📋 Button created: {button.GetType().Name}");

// Set the button directly as window content
Content = button;

Console.WriteLine("✅ MainWindow setup complete");

// Hook into the Opened event to ensure proper AT-SPI initialization after window is fully shown
this.Opened += (sender, e) =>
{
Console.WriteLine("� Window Opened event - Starting AT-SPI initialization...");

// Force window automation peer creation through normal Avalonia pathway
try
{
// This should trigger the LinuxWindowAutomationPeer creation
var windowMethod = this.GetType().GetMethod("OnCreateAutomationPeer",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var windowPeer = windowMethod?.Invoke(this, null);
Console.WriteLine($"📋 Window automation peer: {windowPeer?.GetType().Name ?? "NULL"}");

// Give it time to establish AT-SPI root
System.Threading.Thread.Sleep(1000);

// Now force button automation peer creation
var buttonMethod = button.GetType().GetMethod("OnCreateAutomationPeer",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var buttonPeer = buttonMethod?.Invoke(button, null);
Console.WriteLine($"📋 Button automation peer: {buttonPeer?.GetType().Name ?? "NULL"}");

// Force the LinuxWindowAutomationPeer to reinitialize children if it exists
if (windowPeer != null && windowPeer.GetType().Name == "LinuxWindowAutomationPeer")
{
Console.WriteLine("� Forcing child control reinitialization...");
var reinitMethod = windowPeer.GetType().GetMethod("ForceReinitializeChildControls",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
reinitMethod?.Invoke(windowPeer, new object[] { this });
}

}
catch (Exception ex)
{
Console.WriteLine($"❌ AT-SPI initialization failed: {ex.Message}");
}
};
}
}
35 changes: 35 additions & 0 deletions samples/ButtonTestApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using Avalonia;

namespace ButtonTestApp;

class Program
{
[STAThread]
public static int Main(string[] args)
{
Console.WriteLine("🚀 ButtonTestApp starting...");
Console.WriteLine($" Args: {string.Join(" ", args)}");
Console.WriteLine($" Environment: {Environment.OSVersion}");
Console.WriteLine($" Current Directory: {Environment.CurrentDirectory}");

// Force check if automation peer factory exists
Console.WriteLine("🔍 Checking if LinuxAutomationPeerFactory is available...");
try
{
var factory = AvaloniaLocator.Current.GetService<Avalonia.Controls.Platform.IAutomationPeerFactory>();
Console.WriteLine($" Factory found: {factory?.GetType().Name ?? "NULL"}");
}
catch (Exception ex)
{
Console.WriteLine($" Factory check failed: {ex.Message}");
}

return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}

public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
33 changes: 33 additions & 0 deletions samples/ButtonTestApp/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="ButtonTestApp.Desktop"/>

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>

<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->

<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>

<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
11 changes: 11 additions & 0 deletions src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -555,5 +555,16 @@ protected void EnsureEnabled()
if (!IsEnabled())
throw new ElementNotEnabledException();
}

/// <summary>
/// Creates the platform-specific implementation for this automation peer.
/// This is called when the automation peer is first created to set up platform-specific
/// functionality like AT-SPI on Linux or UIA on Windows.
/// </summary>
internal virtual void CreatePlatformImpl()
{
// Default implementation does nothing
// Platform-specific automation implementations should override this
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using System;
using Avalonia.Controls;

namespace Avalonia.Automation.Peers
{
Expand All @@ -16,17 +17,24 @@ protected ContentControlAutomationPeer(ContentControl owner)
protected override string? GetNameCore()
{
var result = base.GetNameCore();
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] base result: '{result ?? "NULL"}'");
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] Owner: {Owner?.GetType().Name ?? "NULL"}");
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] Owner.Content: {Owner?.Content ?? "NULL"}");
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] Owner.Content type: {Owner?.Content?.GetType().Name ?? "NULL"}");

if (result is null && Owner.Presenter?.Child is TextBlock text)
if (result is null && Owner?.Presenter?.Child is TextBlock text)
{
result = text.Text;
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] From TextBlock: '{result ?? "NULL"}'");
}

if (result is null)
if (result is null && Owner?.Content is object content)
{
result = Owner.Content?.ToString();
result = content.ToString();
Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] From Content.ToString(): '{result ?? "NULL"}'");
}

Console.WriteLine($"[ContentControlAutomationPeer.GetNameCore] Final result: '{result ?? "NULL"}'");
return result;
}

Expand Down
18 changes: 17 additions & 1 deletion src/Avalonia.Controls/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
Expand Down Expand Up @@ -550,7 +551,22 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
}
}

protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this);
protected override AutomationPeer OnCreateAutomationPeer()
{
// Check if a platform-specific automation peer factory is available
var factory = AvaloniaLocator.Current.GetService<IAutomationPeerFactory>();
if (factory != null)
{
var platformPeer = factory.CreateAutomationPeer(this);
if (platformPeer != null)
{
return platformPeer;
}
}

// Fall back to the default ButtonAutomationPeer
return new ButtonAutomationPeer(this);
}

/// <inheritdoc/>
protected override void UpdateDataValidation(
Expand Down
Loading