Browse Source

Merge branch 'master' into net6-ios

pull/7565/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
9909c628fa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .editorconfig
  2. 5
      .ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject
  3. 5
      .ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject
  4. 5
      .ncrunch/IntegrationTestApp.v3.ncrunchproject
  5. 55
      Avalonia.sln
  6. 18
      build/XUnit.props
  7. 8
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  8. 1
      native/Avalonia.Native/src/OSX/AvnString.h
  9. 16
      native/Avalonia.Native/src/OSX/AvnString.mm
  10. 12
      native/Avalonia.Native/src/OSX/automation.h
  11. 496
      native/Avalonia.Native/src/OSX/automation.mm
  12. 1
      native/Avalonia.Native/src/OSX/common.h
  13. 12
      native/Avalonia.Native/src/OSX/main.mm
  14. 1
      native/Avalonia.Native/src/OSX/window.h
  15. 52
      native/Avalonia.Native/src/OSX/window.mm
  16. 7
      samples/IntegrationTestApp/App.axaml
  17. 24
      samples/IntegrationTestApp/App.axaml.cs
  18. 27
      samples/IntegrationTestApp/IntegrationTestApp.csproj
  19. 95
      samples/IntegrationTestApp/MainWindow.axaml
  20. 67
      samples/IntegrationTestApp/MainWindow.axaml.cs
  21. 22
      samples/IntegrationTestApp/Program.cs
  22. 5
      samples/IntegrationTestApp/bundle.sh
  23. 11
      samples/IntegrationTestApp/nuget.config
  24. 28
      src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
  25. 28
      src/Avalonia.Controls/Automation/AutomationLiveSetting.cs
  26. 630
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  27. 11
      src/Avalonia.Controls/Automation/AutomationProperty.cs
  28. 21
      src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs
  29. 10
      src/Avalonia.Controls/Automation/ElementNotEnabledException.cs
  30. 15
      src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs
  31. 29
      src/Avalonia.Controls/Automation/ExpandCollapseState.cs
  32. 26
      src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs
  33. 263
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  34. 43
      src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
  35. 137
      src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
  36. 36
      src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs
  37. 221
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  38. 54
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  39. 82
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  40. 59
      src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
  41. 25
      src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs
  42. 48
      src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs
  43. 40
      src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs
  44. 34
      src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs
  45. 172
      src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs
  46. 84
      src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs
  47. 27
      src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
  48. 23
      src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
  49. 39
      src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs
  50. 24
      src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs
  51. 36
      src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs
  52. 78
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  53. 33
      src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs
  54. 15
      src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs
  55. 47
      src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs
  56. 14
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  57. 71
      src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs
  58. 35
      src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs
  59. 29
      src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs
  60. 40
      src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs
  61. 29
      src/Avalonia.Controls/Automation/Provider/IValueProvider.cs
  62. 30
      src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs
  63. 45
      src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs
  64. 25
      src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs
  65. 7
      src/Avalonia.Controls/Button.cs
  66. 6
      src/Avalonia.Controls/CheckBox.cs
  67. 6
      src/Avalonia.Controls/ComboBox.cs
  68. 7
      src/Avalonia.Controls/ComboBoxItem.cs
  69. 4
      src/Avalonia.Controls/ContextMenu.cs
  70. 20
      src/Avalonia.Controls/Control.cs
  71. 3
      src/Avalonia.Controls/Image.cs
  72. 6
      src/Avalonia.Controls/ItemsControl.cs
  73. 7
      src/Avalonia.Controls/ListBoxItem.cs
  74. 4
      src/Avalonia.Controls/Menu.cs
  75. 6
      src/Avalonia.Controls/MenuItem.cs
  76. 1
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  77. 18
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  78. 48
      src/Avalonia.Controls/Primitives/AccessText.cs
  79. 18
      src/Avalonia.Controls/Primitives/Popup.cs
  80. 6
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  81. 6
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  82. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  83. 8
      src/Avalonia.Controls/ScrollViewer.cs
  84. 3
      src/Avalonia.Controls/Slider.cs
  85. 3
      src/Avalonia.Controls/TabControl.cs
  86. 5
      src/Avalonia.Controls/TabItem.cs
  87. 14
      src/Avalonia.Controls/TextBlock.cs
  88. 141
      src/Avalonia.Controls/TextBox.cs
  89. 21
      src/Avalonia.Controls/Window.cs
  90. 1
      src/Avalonia.Controls/WindowBase.cs
  91. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  92. 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  93. 1
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  94. 32
      src/Avalonia.Input/KeyboardDevice.cs
  95. 4
      src/Avalonia.Native/Avalonia.Native.csproj
  96. 160
      src/Avalonia.Native/AvnAutomationPeer.cs
  97. 48
      src/Avalonia.Native/AvnString.cs
  98. 11
      src/Avalonia.Native/Helpers.cs
  99. 4
      src/Avalonia.Native/PopupImpl.cs
  100. 9
      src/Avalonia.Native/WindowImpl.cs

2
.editorconfig

@ -137,7 +137,7 @@ space_within_single_line_array_initializer_braces = true
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
[*.{xaml,axaml}]
indent_size = 2
# Xml project files

5
.ncrunch/Avalonia.IntegrationTests.Appium.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.IntegrationTests.Win32.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/IntegrationTestApp.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

55
Avalonia.sln

@ -216,6 +216,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Av
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
@ -2018,6 +2022,54 @@ Global
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhone.Build.0 = Release|Any CPU
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhone.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhone.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.ActiveCfg = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhone.Build.0 = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhone.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhone.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.ActiveCfg = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhone.Build.0 = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -2223,6 +2275,7 @@ Global
{29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E}
{39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
@ -2242,6 +2295,8 @@ Global
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}

18
build/XUnit.props

@ -1,14 +1,14 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="xunit" Version="2.3.0" />
<PackageReference Include="xunit.abstractions" Version="2.0.1" />
<PackageReference Include="xunit.assert" Version="2.3.0" />
<PackageReference Include="xunit.core" Version="2.3.0" />
<PackageReference Include="xunit.extensibility.core" Version="2.3.0" />
<PackageReference Include="xunit.extensibility.execution" Version="2.3.0" />
<PackageReference Include="xunit.runner.console" Version="2.3.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.0" />
<PackageReference Include="Xunit.SkippableFact" Version="1.3.6" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="xunit.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.console" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
</ItemGroup>
<PropertyGroup>

8
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -30,6 +30,8 @@
AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; };
AB661C202148286E00291242 /* window.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB661C1F2148286E00291242 /* window.mm */; };
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; };
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -64,6 +66,8 @@
AB661C212148288600291242 /* common.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = "<group>"; };
AB7A61EF2147C815003C5833 /* libAvalonia.Native.OSX.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libAvalonia.Native.OSX.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = "<group>"; };
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -97,6 +101,8 @@
AB7A61E62147C814003C5833 = {
isa = PBXGroup;
children = (
BC11A5BC2608D58F0017BAD0 /* automation.h */,
BC11A5BD2608D58F0017BAD0 /* automation.mm */,
1A1852DB23E05814008F0DED /* deadlock.mm */,
1A002B9D232135EE00021753 /* app.mm */,
37DDA9B121933371002E132B /* AvnString.h */,
@ -143,6 +149,7 @@
buildActionMask = 2147483647;
files = (
37155CE4233C00EB0034DCE9 /* menu.h in Headers */,
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -213,6 +220,7 @@
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */,
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,

1
native/Avalonia.Native/src/OSX/AvnString.h

@ -14,4 +14,5 @@ extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
extern NSString* GetNSStringAndRelease(IAvnString* s);
#endif /* AvnString_h */

16
native/Avalonia.Native/src/OSX/AvnString.mm

@ -153,3 +153,19 @@ IAvnString* CreateByteArray(void* data, int len)
{
return new AvnStringImpl(data, len);
}
NSString* GetNSStringAndRelease(IAvnString* s)
{
NSString* result = nil;
if (s != nullptr)
{
char* p;
if (s->Pointer((void**)&p) == S_OK && p != nullptr)
result = [NSString stringWithUTF8String:p];
s->Release();
}
return result;
}

12
native/Avalonia.Native/src/OSX/automation.h

@ -0,0 +1,12 @@
#import <Cocoa/Cocoa.h>
#include "window.h"
NS_ASSUME_NONNULL_BEGIN
class IAvnAutomationPeer;
@interface AvnAccessibilityElement : NSAccessibilityElement
+ (AvnAccessibilityElement *) acquire:(IAvnAutomationPeer *) peer;
@end
NS_ASSUME_NONNULL_END

496
native/Avalonia.Native/src/OSX/automation.mm

@ -0,0 +1,496 @@
#include "common.h"
#include "automation.h"
#include "AvnString.h"
#include "window.h"
@interface AvnAccessibilityElement (Events)
- (void) raiseChildrenChanged;
@end
@interface AvnRootAccessibilityElement : AvnAccessibilityElement
- (AvnView *) ownerView;
- (AvnRootAccessibilityElement *) initWithPeer:(IAvnAutomationPeer *) peer owner:(AvnView*) owner;
- (void) raiseFocusChanged;
@end
class AutomationNode : public ComSingleObject<IAvnAutomationNode, &IID_IAvnAutomationNode>
{
public:
FORWARD_IUNKNOWN()
AutomationNode(AvnAccessibilityElement* owner)
{
_owner = owner;
}
AvnAccessibilityElement* GetOwner()
{
return _owner;
}
virtual void Dispose() override
{
_owner = nil;
}
virtual void ChildrenChanged () override
{
[_owner raiseChildrenChanged];
}
virtual void PropertyChanged (AvnAutomationProperty property) override
{
}
virtual void FocusChanged () override
{
[(AvnRootAccessibilityElement*)_owner raiseFocusChanged];
}
private:
__strong AvnAccessibilityElement* _owner;
};
@implementation AvnAccessibilityElement
{
IAvnAutomationPeer* _peer;
AutomationNode* _node;
NSMutableArray* _children;
}
+ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
{
if (peer == nullptr)
return nil;
auto instance = peer->GetNode();
if (instance != nullptr)
return dynamic_cast<AutomationNode*>(instance)->GetOwner();
if (peer->IsRootProvider())
{
auto window = peer->RootProvider_GetWindow();
auto holder = dynamic_cast<INSWindowHolder*>(window);
auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
}
else
{
return [[AvnAccessibilityElement alloc] initWithPeer:peer];
}
}
- (AvnAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer
{
self = [super init];
_peer = peer;
_node = new AutomationNode(self);
_peer->SetNode(_node);
return self;
}
- (void)dealloc
{
if (_node)
delete _node;
_node = nullptr;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@ '%@' (%p)",
GetNSStringAndRelease(_peer->GetClassName()),
GetNSStringAndRelease(_peer->GetName()),
_peer];
}
- (IAvnAutomationPeer *)peer
{
return _peer;
}
- (BOOL)isAccessibilityElement
{
return _peer->IsControlElement();
}
- (NSAccessibilityRole)accessibilityRole
{
auto controlType = _peer->GetAutomationControlType();
switch (controlType) {
case AutomationButton: return NSAccessibilityButtonRole;
case AutomationCalendar: return NSAccessibilityGridRole;
case AutomationCheckBox: return NSAccessibilityCheckBoxRole;
case AutomationComboBox: return NSAccessibilityPopUpButtonRole;
case AutomationComboBoxItem: return NSAccessibilityMenuItemRole;
case AutomationEdit: return NSAccessibilityTextFieldRole;
case AutomationHyperlink: return NSAccessibilityLinkRole;
case AutomationImage: return NSAccessibilityImageRole;
case AutomationListItem: return NSAccessibilityRowRole;
case AutomationList: return NSAccessibilityTableRole;
case AutomationMenu: return NSAccessibilityMenuBarRole;
case AutomationMenuBar: return NSAccessibilityMenuBarRole;
case AutomationMenuItem: return NSAccessibilityMenuItemRole;
case AutomationProgressBar: return NSAccessibilityProgressIndicatorRole;
case AutomationRadioButton: return NSAccessibilityRadioButtonRole;
case AutomationScrollBar: return NSAccessibilityScrollBarRole;
case AutomationSlider: return NSAccessibilitySliderRole;
case AutomationSpinner: return NSAccessibilityIncrementorRole;
case AutomationStatusBar: return NSAccessibilityTableRole;
case AutomationTab: return NSAccessibilityTabGroupRole;
case AutomationTabItem: return NSAccessibilityRadioButtonRole;
case AutomationText: return NSAccessibilityStaticTextRole;
case AutomationToolBar: return NSAccessibilityToolbarRole;
case AutomationToolTip: return NSAccessibilityPopoverRole;
case AutomationTree: return NSAccessibilityOutlineRole;
case AutomationTreeItem: return NSAccessibilityCellRole;
case AutomationCustom: return NSAccessibilityUnknownRole;
case AutomationGroup: return NSAccessibilityGroupRole;
case AutomationThumb: return NSAccessibilityHandleRole;
case AutomationDataGrid: return NSAccessibilityGridRole;
case AutomationDataItem: return NSAccessibilityCellRole;
case AutomationDocument: return NSAccessibilityStaticTextRole;
case AutomationSplitButton: return NSAccessibilityPopUpButtonRole;
case AutomationWindow: return NSAccessibilityWindowRole;
case AutomationPane: return NSAccessibilityGroupRole;
case AutomationHeader: return NSAccessibilityGroupRole;
case AutomationHeaderItem: return NSAccessibilityButtonRole;
case AutomationTable: return NSAccessibilityTableRole;
case AutomationTitleBar: return NSAccessibilityGroupRole;
// Treat unknown roles as generic group container items. Returning
// NSAccessibilityUnknownRole is also possible but makes the screen
// reader focus on the item instead of passing focus to child items.
default: return NSAccessibilityGroupRole;
}
}
- (NSString *)accessibilityIdentifier
{
return GetNSStringAndRelease(_peer->GetAutomationId());
}
- (NSString *)accessibilityTitle
{
// StaticText exposes its text via the value property.
if (_peer->GetAutomationControlType() != AutomationText)
{
return GetNSStringAndRelease(_peer->GetName());
}
return [super accessibilityTitle];
}
- (id)accessibilityValue
{
if (_peer->IsRangeValueProvider())
{
return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetValue()];
}
else if (_peer->IsToggleProvider())
{
switch (_peer->ToggleProvider_GetToggleState()) {
case 0: return [NSNumber numberWithBool:NO];
case 1: return [NSNumber numberWithBool:YES];
default: return [NSNumber numberWithInt:2];
}
}
else if (_peer->IsValueProvider())
{
return GetNSStringAndRelease(_peer->ValueProvider_GetValue());
}
else if (_peer->GetAutomationControlType() == AutomationText)
{
return GetNSStringAndRelease(_peer->GetName());
}
return [super accessibilityValue];
}
- (id)accessibilityMinValue
{
if (_peer->IsRangeValueProvider())
{
return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMinimum()];
}
return [super accessibilityMinValue];
}
- (id)accessibilityMaxValue
{
if (_peer->IsRangeValueProvider())
{
return [NSNumber numberWithDouble:_peer->RangeValueProvider_GetMaximum()];
}
return [super accessibilityMaxValue];
}
- (BOOL)isAccessibilityEnabled
{
return _peer->IsEnabled();
}
- (BOOL)isAccessibilityFocused
{
return _peer->HasKeyboardFocus();
}
- (NSArray *)accessibilityChildren
{
if (_children == nullptr && _peer != nullptr)
[self recalculateChildren];
return _children;
}
- (NSRect)accessibilityFrame
{
id topLevel = [self accessibilityTopLevelUIElement];
auto result = NSZeroRect;
if ([topLevel isKindOfClass:[AvnRootAccessibilityElement class]])
{
auto root = (AvnRootAccessibilityElement*)topLevel;
auto view = [root ownerView];
if (view)
{
auto window = [view window];
auto bounds = ToNSRect(_peer->GetBoundingRectangle());
auto windowBounds = [view convertRect:bounds toView:nil];
auto screenBounds = [window convertRectToScreen:windowBounds];
result = screenBounds;
}
}
return result;
}
- (id)accessibilityParent
{
auto parentPeer = _peer->GetParent();
return parentPeer ? [AvnAccessibilityElement acquire:parentPeer] : [NSApplication sharedApplication];
}
- (id)accessibilityTopLevelUIElement
{
auto rootPeer = _peer->GetRootPeer();
return [AvnAccessibilityElement acquire:rootPeer];
}
- (id)accessibilityWindow
{
id topLevel = [self accessibilityTopLevelUIElement];
return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil;
}
- (BOOL)isAccessibilityExpanded
{
if (!_peer->IsExpandCollapseProvider())
return NO;
return _peer->ExpandCollapseProvider_GetIsExpanded();
}
- (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded
{
if (!_peer->IsExpandCollapseProvider())
return;
if (accessibilityExpanded)
_peer->ExpandCollapseProvider_Expand();
else
_peer->ExpandCollapseProvider_Collapse();
}
- (BOOL)accessibilityPerformPress
{
if (_peer->IsInvokeProvider())
{
_peer->InvokeProvider_Invoke();
}
else if (_peer->IsExpandCollapseProvider())
{
_peer->ExpandCollapseProvider_Expand();
}
else if (_peer->IsToggleProvider())
{
_peer->ToggleProvider_Toggle();
}
return YES;
}
- (BOOL)accessibilityPerformIncrement
{
if (!_peer->IsRangeValueProvider())
return NO;
auto value = _peer->RangeValueProvider_GetValue();
value += _peer->RangeValueProvider_GetSmallChange();
_peer->RangeValueProvider_SetValue(value);
return YES;
}
- (BOOL)accessibilityPerformDecrement
{
if (!_peer->IsRangeValueProvider())
return NO;
auto value = _peer->RangeValueProvider_GetValue();
value -= _peer->RangeValueProvider_GetSmallChange();
_peer->RangeValueProvider_SetValue(value);
return YES;
}
- (BOOL)accessibilityPerformShowMenu
{
if (!_peer->IsExpandCollapseProvider())
return NO;
_peer->ExpandCollapseProvider_Expand();
return YES;
}
- (BOOL)isAccessibilitySelected
{
if (_peer->IsSelectionItemProvider())
return _peer->SelectionItemProvider_IsSelected();
return NO;
}
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
if (selector == @selector(accessibilityPerformShowMenu))
{
return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_GetShowsMenu();
}
else if (selector == @selector(isAccessibilityExpanded))
{
return _peer->IsExpandCollapseProvider();
}
else if (selector == @selector(accessibilityPerformPress))
{
return _peer->IsInvokeProvider() || _peer->IsExpandCollapseProvider() || _peer->IsToggleProvider();
}
else if (selector == @selector(accessibilityPerformIncrement) ||
selector == @selector(accessibilityPerformDecrement) ||
selector == @selector(accessibilityMinValue) ||
selector == @selector(accessibilityMaxValue))
{
return _peer->IsRangeValueProvider();
}
return [super isAccessibilitySelectorAllowed:selector];
}
- (void)raiseChildrenChanged
{
auto changed = _children ? [NSMutableSet setWithArray:_children] : [NSMutableSet set];
[self recalculateChildren];
if (_children)
[changed addObjectsFromArray:_children];
NSAccessibilityPostNotificationWithUserInfo(
self,
NSAccessibilityLayoutChangedNotification,
@{ NSAccessibilityUIElementsKey: [changed allObjects]});
}
- (void)raisePropertyChanged
{
}
- (void)setAccessibilityFocused:(BOOL)accessibilityFocused
{
if (accessibilityFocused)
_peer->SetFocus();
}
- (void)recalculateChildren
{
auto childPeers = _peer->GetChildren();
auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0;
if (childCount > 0)
{
_children = [[NSMutableArray alloc] initWithCapacity:childCount];
for (int i = 0; i < childCount; ++i)
{
IAvnAutomationPeer* child;
if (childPeers->Get(i, &child) == S_OK)
{
auto element = [AvnAccessibilityElement acquire:child];
[_children addObject:element];
}
}
}
else
{
_children = nil;
}
}
@end
@implementation AvnRootAccessibilityElement
{
AvnView* _owner;
}
- (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:(AvnView *)owner
{
self = [super initWithPeer:peer];
_owner = owner;
// Seems we need to raise a focus changed notification here if we have focus
auto focusedPeer = [self peer]->RootProvider_GetFocus();
id focused = [AvnAccessibilityElement acquire:focusedPeer];
if (focused)
NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification);
return self;
}
- (AvnView *)ownerView
{
return _owner;
}
- (id)accessibilityFocusedUIElement
{
auto focusedPeer = [self peer]->RootProvider_GetFocus();
return [AvnAccessibilityElement acquire:focusedPeer];
}
- (id)accessibilityHitTest:(NSPoint)point
{
auto clientPoint = [[_owner window] convertPointFromScreen:point];
auto localPoint = [_owner translateLocalPoint:ToAvnPoint(clientPoint)];
auto hit = [self peer]->RootProvider_GetPeerFromPoint(localPoint);
return [AvnAccessibilityElement acquire:hit];
}
- (id)accessibilityParent
{
return _owner;
}
- (void)raiseFocusChanged
{
id focused = [self accessibilityFocusedUIElement];
NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification);
}
// Although this method is marked as deprecated we get runtime warnings if we don't handle it.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (void)accessibilityPerformAction:(NSAccessibilityActionName)action
{
[_owner accessibilityPerformAction:action];
}
#pragma clang diagnostic pop
@end

1
native/Avalonia.Native/src/OSX/common.h

@ -35,6 +35,7 @@ extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(IAvnApplicationEvents* events);
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);
extern AvnPoint ToAvnPoint (NSPoint p);
extern AvnPoint ConvertPointY (AvnPoint p);
extern CGFloat PrimaryDisplayHeight();

12
native/Avalonia.Native/src/OSX/main.mm

@ -1,6 +1,7 @@
//This file will contain actual IID structures
#define COM_GUIDS_MATERIALIZE
#include "common.h"
#include "window.h"
static NSString* s_appTitle = @"Avalonia";
@ -335,7 +336,7 @@ public:
return S_OK;
}
}
virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override
{
START_COM_CALL;
@ -400,6 +401,15 @@ NSPoint ToNSPoint (AvnPoint p)
return result;
}
NSRect ToNSRect (AvnRect r)
{
return NSRect
{
NSPoint { r.X, r.Y },
NSSize { r.Width, r.Height }
};
}
AvnPoint ToAvnPoint (NSPoint p)
{
AvnPoint result;

1
native/Avalonia.Native/src/OSX/window.h

@ -43,6 +43,7 @@ class WindowBaseImpl;
struct INSWindowHolder
{
virtual AvnWindow* _Nonnull GetNSWindow () = 0;
virtual AvnView* _Nonnull GetNSView () = 0;
};
struct IWindowStateChanged

52
native/Avalonia.Native/src/OSX/window.mm

@ -5,14 +5,22 @@
#include "menu.h"
#include <OpenGL/gl.h>
#include "rendertarget.h"
#include "AvnString.h"
#include "automation.h"
class WindowBaseImpl : public virtual ComSingleObject<IAvnWindowBase, &IID_IAvnWindowBase>, public INSWindowHolder
class WindowBaseImpl : public virtual ComObject,
public virtual IAvnWindowBase,
public INSWindowHolder
{
private:
NSCursor* cursor;
public:
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase)
END_INTERFACE_MAP()
virtual ~WindowBaseImpl()
{
View = NULL;
@ -115,7 +123,12 @@ public:
{
return Window;
}
virtual AvnView* GetNSView() override
{
return View;
}
virtual HRESULT Show(bool activate, bool isDialog) override
{
START_COM_CALL;
@ -1396,6 +1409,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
AvnPixelSize _lastPixelSize;
NSObject<IRenderTarget>* _renderTarget;
AvnPlatformResizeReason _resizeReason;
AvnAccessibilityElement* _accessibilityChild;
}
- (void)onClosed
@ -2050,6 +2064,37 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_resizeReason = reason;
}
- (AvnAccessibilityElement *) accessibilityChild
{
if (_accessibilityChild == nil)
{
auto peer = _parent->BaseEvents->GetAutomationPeer();
if (peer == nil)
return nil;
_accessibilityChild = [AvnAccessibilityElement acquire:peer];
}
return _accessibilityChild;
}
- (NSArray *)accessibilityChildren
{
auto child = [self accessibilityChild];
return NSAccessibilityUnignoredChildrenForOnlyChild(child);
}
- (id)accessibilityHitTest:(NSPoint)point
{
return [[self accessibilityChild] accessibilityHitTest:point];
}
- (id)accessibilityFocusedUIElement
{
return [[self accessibilityChild] accessibilityFocusedUIElement];
}
@end
@ -2062,6 +2107,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _isExtended;
AvnMenu* _menu;
double _lastScaling;
IAvnAutomationPeer* _automationPeer;
NSMutableArray* _automationChildren;
}
-(void) setIsExtended:(bool)value;
@ -2465,6 +2512,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
}
@end
class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup

7
samples/IntegrationTestApp/App.axaml

@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="IntegrationTestApp.App">
<Application.Styles>
<FluentTheme Mode="Light"/>
</Application.Styles>
</Application>

24
samples/IntegrationTestApp/App.axaml.cs

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace IntegrationTestApp
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
}

27
samples/IntegrationTestApp/IntegrationTestApp.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<CFBundleName>IntegrationTestApp</CFBundleName>
<CFBundleIdentifier>net.avaloniaui.avalonia.integrationtestapp</CFBundleIdentifier>
<NSHighResolutionCapable>true</NSHighResolutionCapable>
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dotnet.Bundle" Version="0.9.13" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
</Project>

95
samples/IntegrationTestApp/MainWindow.axaml

@ -0,0 +1,95 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Title="IntegrationTestApp">
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="File">
<NativeMenu>
<NativeMenuItem Header="Open..."/>
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="View">
<NativeMenu/>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>
<DockPanel>
<NativeMenuBar DockPanel.Dock="Top"/>
<TabControl TabStripPlacement="Left" Name="MainTabs">
<TabItem Header="Automation">
<StackPanel>
<TextBlock Name="TextBlockWithName">TextBlockWithName</TextBlock>
<TextBlock Name="NotTheAutomationId" AutomationProperties.AutomationId="TextBlockWithNameAndAutomationId">
TextBlockWithNameAndAutomationId
</TextBlock>
<TextBlock Name="TextBlockAsLabel">Label for TextBox</TextBlock>
<TextBox Name="LabeledByTextBox" AutomationProperties.LabeledBy="{Binding #TextBlockAsLabel}">
Foo
</TextBox>
</StackPanel>
</TabItem>
<TabItem Header="Button">
<StackPanel>
<Button Name="DisabledButton" IsEnabled="False">
Disabled Button
</Button>
<Button Name="BasicButton">
Basic Button
</Button>
<Button Name="ButtonWithTextBlock">
<TextBlock>Button with TextBlock</TextBlock>
</Button>
<Button Name="ButtonWithAcceleratorKey" HotKey="Ctrl+B">Button with Accelerator Key</Button>
</StackPanel>
</TabItem>
<TabItem Header="CheckBox">
<StackPanel>
<CheckBox Name="UncheckedCheckBox">Unchecked</CheckBox>
<CheckBox Name="CheckedCheckBox" IsChecked="True">Checked</CheckBox>
<CheckBox Name="ThreeStateCheckBox" IsThreeState="True" IsChecked="{x:Null}">ThreeState</CheckBox>
</StackPanel>
</TabItem>
<TabItem Header="ComboBox">
<StackPanel>
<ComboBox Name="BasicComboBox">
<ComboBoxItem>Item 0</ComboBoxItem>
<ComboBoxItem>Item 1</ComboBoxItem>
</ComboBox>
<Button Name="ComboBoxSelectionClear">Clear Selection</Button>
<Button Name="ComboBoxSelectFirst">Select First</Button>
</StackPanel>
</TabItem>
<TabItem Header="ListBox">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom">
<Button Name="ListBoxSelectionClear">Clear Selection</Button>
</StackPanel>
<ListBox Name="BasicListBox" Items="{Binding ListBoxItems}" SelectionMode="Multiple"/>
</DockPanel>
</TabItem>
<TabItem Header="Menu">
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Name="RootMenuItem" Header="_Root">
<MenuItem Name="Child1MenuItem" Header="_Child 1" InputGesture="Ctrl+O" Click="MenuClicked"/>
<MenuItem Name="Child2MenuItem" Header="_Child 1">
<MenuItem Name="GrandchildMenuItem" Header="_Grandchild" Click="MenuClicked"/>
</MenuItem>
</MenuItem>
</Menu>
<TextBlock Name="ClickedMenuItem">None</TextBlock>
</DockPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>

67
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace IntegrationTestApp
{
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitializeViewMenu();
this.AttachDevTools();
AddHandler(Button.ClickEvent, OnButtonClick);
ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList();
DataContext = this;
}
public List<string> ListBoxItems { get; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InitializeViewMenu()
{
var mainTabs = this.FindControl<TabControl>("MainTabs");
var viewMenu = (NativeMenuItem)NativeMenu.GetMenu(this).Items[1];
foreach (TabItem tabItem in mainTabs.Items)
{
var menuItem = new NativeMenuItem
{
Header = (string)tabItem.Header!,
IsChecked = tabItem.IsSelected,
ToggleType = NativeMenuItemToggleType.Radio,
};
menuItem.Click += (s, e) => tabItem.IsSelected = true;
viewMenu.Menu.Items.Add(menuItem);
}
}
private void MenuClicked(object? sender, RoutedEventArgs e)
{
var clickedMenuItemTextBlock = this.FindControl<TextBlock>("ClickedMenuItem");
clickedMenuItemTextBlock.Text = ((MenuItem)sender!).Header.ToString();
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
var source = e.Source as Button;
if (source?.Name == "ComboBoxSelectionClear")
this.FindControl<ComboBox>("BasicComboBox").SelectedIndex = -1;
if (source?.Name == "ComboBoxSelectFirst")
this.FindControl<ComboBox>("BasicComboBox").SelectedIndex = 0;
if (source?.Name == "ListBoxSelectionClear")
this.FindControl<ListBox>("BasicListBox").SelectedIndex = -1;
}
}
}

22
samples/IntegrationTestApp/Program.cs

@ -0,0 +1,22 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace IntegrationTestApp
{
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

5
samples/IntegrationTestApp/bundle.sh

@ -0,0 +1,5 @@
#!/usr/bin/env bash
cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
dotnet restore -r osx-arm64
dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false

11
samples/IntegrationTestApp/nuget.config

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
To use the Avalonia CI feed to get unstable packages, move this file to the root of your solution.
-->
<configuration>
<packageSources>
<add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" />
</packageSources>
</configuration>

28
src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs

@ -0,0 +1,28 @@
using Avalonia.Automation.Peers;
namespace Avalonia.Automation
{
/// <summary>
/// Contains values used as automation property identifiers by UI Automation providers.
/// </summary>
public static class AutomationElementIdentifiers
{
/// <summary>
/// Identifies the bounding rectangle automation property. The bounding rectangle property
/// value is returned by the <see cref="AutomationPeer.GetBoundingRectangle"/> method.
/// </summary>
public static AutomationProperty BoundingRectangleProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies the class name automation property. The class name property value is returned
/// by the <see cref="AutomationPeer.GetClassName"/> method.
/// </summary>
public static AutomationProperty ClassNameProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies the name automation property. The class name property value is returned
/// by the <see cref="AutomationPeer.GetName"/> method.
/// </summary>
public static AutomationProperty NameProperty { get; } = new AutomationProperty();
}
}

28
src/Avalonia.Controls/Automation/AutomationLiveSetting.cs

@ -0,0 +1,28 @@
namespace Avalonia.Automation
{
/// <summary>
/// Describes the notification characteristics of a particular live region
/// </summary>
public enum AutomationLiveSetting
{
/// <summary>
/// The element does not send notifications if the content of the live region has changed.
/// </summary>
Off = 0,
/// <summary>
/// The element sends non-interruptive notifications if the content of the live region has
/// changed. With this setting, UI Automation clients and assistive technologies are expected
/// to not interrupt the user to inform of changes to the live region.
/// </summary>
Polite = 1,
/// <summary>
/// The element sends interruptive notifications if the content of the live region has changed.
/// With this setting, UI Automation clients and assistive technologies are expected to interrupt
/// the user to inform of changes to the live region.
/// </summary>
Assertive = 2,
}
}

630
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -0,0 +1,630 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
namespace Avalonia.Automation
{
/// <summary>
/// Declares how a control should included in different views of the automation tree.
/// </summary>
public enum AccessibilityView
{
/// <summary>
/// The control is included in the Raw view of the automation tree.
/// </summary>
Raw,
/// <summary>
/// The control is included in the Control view of the automation tree.
/// </summary>
Control,
/// <summary>
/// The control is included in the Content view of the automation tree.
/// </summary>
Content,
}
public static class AutomationProperties
{
internal const int AutomationPositionInSetDefault = -1;
internal const int AutomationSizeOfSetDefault = -1;
/// <summary>
/// Defines the AutomationProperties.AcceleratorKey attached property.
/// </summary>
public static readonly AttachedProperty<string> AcceleratorKeyProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"AcceleratorKey",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.AccessibilityView attached property.
/// </summary>
public static readonly AttachedProperty<AccessibilityView> AccessibilityViewProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AccessibilityView>(
"AccessibilityView",
typeof(AutomationProperties),
defaultValue: AccessibilityView.Content);
/// <summary>
/// Defines the AutomationProperties.AccessKey attached property
/// </summary>
public static readonly AttachedProperty<string> AccessKeyProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"AccessKey",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.AutomationId attached property.
/// </summary>
public static readonly AttachedProperty<string> AutomationIdProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"AutomationId",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.ControlTypeOverride attached property.
/// </summary>
public static readonly AttachedProperty<AutomationControlType?> ControlTypeOverrideProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AutomationControlType?>(
"ControlTypeOverride",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.HelpText attached property.
/// </summary>
public static readonly AttachedProperty<string> HelpTextProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"HelpText",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.IsColumnHeader attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsColumnHeaderProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>(
"IsColumnHeader",
typeof(AutomationProperties),
false);
/// <summary>
/// Defines the AutomationProperties.IsRequiredForForm attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsRequiredForFormProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>(
"IsRequiredForForm",
typeof(AutomationProperties),
false);
/// <summary>
/// Defines the AutomationProperties.IsRowHeader attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsRowHeaderProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>(
"IsRowHeader",
typeof(AutomationProperties),
false);
/// <summary>
/// Defines the AutomationProperties.IsOffscreenBehavior attached property.
/// </summary>
public static readonly AttachedProperty<IsOffscreenBehavior> IsOffscreenBehaviorProperty =
AvaloniaProperty.RegisterAttached<StyledElement, IsOffscreenBehavior>(
"IsOffscreenBehavior",
typeof(AutomationProperties),
IsOffscreenBehavior.Default);
/// <summary>
/// Defines the AutomationProperties.ItemStatus attached property.
/// </summary>
public static readonly AttachedProperty<string> ItemStatusProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"ItemStatus",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.ItemType attached property.
/// </summary>
public static readonly AttachedProperty<string> ItemTypeProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"ItemType",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.LabeledBy attached property.
/// </summary>
public static readonly AttachedProperty<IControl> LabeledByProperty =
AvaloniaProperty.RegisterAttached<StyledElement, IControl>(
"LabeledBy",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.LiveSetting attached property.
/// </summary>
public static readonly AttachedProperty<AutomationLiveSetting> LiveSettingProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AutomationLiveSetting>(
"LiveSetting",
typeof(AutomationProperties),
AutomationLiveSetting.Off);
/// <summary>
/// Defines the AutomationProperties.Name attached attached property.
/// </summary>
public static readonly AttachedProperty<string> NameProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"Name",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.PositionInSet attached property.
/// </summary>
/// <remarks>
/// The PositionInSet property describes the ordinal location of the element within a set
/// of elements which are considered to be siblings. PositionInSet works in coordination
/// with the SizeOfSet property to describe the ordinal location in the set.
/// </remarks>
public static readonly AttachedProperty<int> PositionInSetProperty =
AvaloniaProperty.RegisterAttached<StyledElement, int>(
"PositionInSet",
typeof(AutomationProperties),
AutomationPositionInSetDefault);
/// <summary>
/// Defines the AutomationProperties.SizeOfSet attached property.
/// </summary>
/// <remarks>
/// The SizeOfSet property describes the count of automation elements in a group or set
/// that are considered to be siblings. SizeOfSet works in coordination with the PositionInSet
/// property to describe the count of items in the set.
/// </remarks>
public static readonly AttachedProperty<int> SizeOfSetProperty =
AvaloniaProperty.RegisterAttached<StyledElement, int>(
"SizeOfSet",
typeof(AutomationProperties),
AutomationSizeOfSetDefault);
/// <summary>
/// Helper for setting AcceleratorKey property on a StyledElement.
/// </summary>
public static void SetAcceleratorKey(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(AcceleratorKeyProperty, value);
}
/// <summary>
/// Helper for reading AcceleratorKey property from a StyledElement.
/// </summary>
public static string GetAcceleratorKey(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(AcceleratorKeyProperty));
}
/// <summary>
/// Helper for setting AccessibilityView property on a StyledElement.
/// </summary>
public static void SetAccessibilityView(StyledElement element, AccessibilityView value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(AccessibilityViewProperty, value);
}
/// <summary>
/// Helper for reading AccessibilityView property from a StyledElement.
/// </summary>
public static AccessibilityView GetAccessibilityView(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return element.GetValue(AccessibilityViewProperty);
}
/// <summary>
/// Helper for setting AccessKey property on a StyledElement.
/// </summary>
public static void SetAccessKey(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(AccessKeyProperty, value);
}
/// <summary>
/// Helper for reading AccessKey property from a StyledElement.
/// </summary>
public static string GetAccessKey(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(AccessKeyProperty));
}
/// <summary>
/// Helper for setting AutomationId property on a StyledElement.
/// </summary>
public static void SetAutomationId(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(AutomationIdProperty, value);
}
/// <summary>
/// Helper for reading AutomationId property from a StyledElement.
/// </summary>
public static string GetAutomationId(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return element.GetValue(AutomationIdProperty);
}
/// <summary>
/// Helper for setting ControlTypeOverride property on a StyledElement.
/// </summary>
public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(ControlTypeOverrideProperty, value);
}
/// <summary>
/// Helper for reading ControlTypeOverride property from a StyledElement.
/// </summary>
public static AutomationControlType? GetControlTypeOverride(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return element.GetValue(ControlTypeOverrideProperty);
}
/// <summary>
/// Helper for setting HelpText property on a StyledElement.
/// </summary>
public static void SetHelpText(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(HelpTextProperty, value);
}
/// <summary>
/// Helper for reading HelpText property from a StyledElement.
/// </summary>
public static string GetHelpText(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(HelpTextProperty));
}
/// <summary>
/// Helper for setting IsColumnHeader property on a StyledElement.
/// </summary>
public static void SetIsColumnHeader(StyledElement element, bool value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(IsColumnHeaderProperty, value);
}
/// <summary>
/// Helper for reading IsColumnHeader property from a StyledElement.
/// </summary>
public static bool GetIsColumnHeader(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((bool)element.GetValue(IsColumnHeaderProperty));
}
/// <summary>
/// Helper for setting IsRequiredForForm property on a StyledElement.
/// </summary>
public static void SetIsRequiredForForm(StyledElement element, bool value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(IsRequiredForFormProperty, value);
}
/// <summary>
/// Helper for reading IsRequiredForForm property from a StyledElement.
/// </summary>
public static bool GetIsRequiredForForm(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((bool)element.GetValue(IsRequiredForFormProperty));
}
/// <summary>
/// Helper for reading IsRowHeader property from a StyledElement.
/// </summary>
public static bool GetIsRowHeader(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((bool)element.GetValue(IsRowHeaderProperty));
}
/// <summary>
/// Helper for setting IsRowHeader property on a StyledElement.
/// </summary>
public static void SetIsRowHeader(StyledElement element, bool value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(IsRowHeaderProperty, value);
}
/// <summary>
/// Helper for setting IsOffscreenBehavior property on a StyledElement.
/// </summary>
public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(IsOffscreenBehaviorProperty, value);
}
/// <summary>
/// Helper for reading IsOffscreenBehavior property from a StyledElement.
/// </summary>
public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty));
}
/// <summary>
/// Helper for setting ItemStatus property on a StyledElement.
/// </summary>
public static void SetItemStatus(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(ItemStatusProperty, value);
}
/// <summary>
/// Helper for reading ItemStatus property from a StyledElement.
/// </summary>
public static string GetItemStatus(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(ItemStatusProperty));
}
/// <summary>
/// Helper for setting ItemType property on a StyledElement.
/// </summary>
public static void SetItemType(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(ItemTypeProperty, value);
}
/// <summary>
/// Helper for reading ItemType property from a StyledElement.
/// </summary>
public static string GetItemType(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(ItemTypeProperty));
}
/// <summary>
/// Helper for setting LabeledBy property on a StyledElement.
/// </summary>
public static void SetLabeledBy(StyledElement element, IControl value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(LabeledByProperty, value);
}
/// <summary>
/// Helper for reading LabeledBy property from a StyledElement.
/// </summary>
public static IControl GetLabeledBy(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return element.GetValue(LabeledByProperty);
}
/// <summary>
/// Helper for setting LiveSetting property on a StyledElement.
/// </summary>
public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(LiveSettingProperty, value);
}
/// <summary>
/// Helper for reading LiveSetting property from a StyledElement.
/// </summary>
public static AutomationLiveSetting GetLiveSetting(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty));
}
/// <summary>
/// Helper for setting Name property on a StyledElement.
/// </summary>
public static void SetName(StyledElement element, string value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(NameProperty, value);
}
/// <summary>
/// Helper for reading Name property from a StyledElement.
/// </summary>
public static string GetName(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((string)element.GetValue(NameProperty));
}
/// <summary>
/// Helper for setting PositionInSet property on a StyledElement.
/// </summary>
public static void SetPositionInSet(StyledElement element, int value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(PositionInSetProperty, value);
}
/// <summary>
/// Helper for reading PositionInSet property from a StyledElement.
/// </summary>
public static int GetPositionInSet(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((int)element.GetValue(PositionInSetProperty));
}
/// <summary>
/// Helper for setting SizeOfSet property on a StyledElement.
/// </summary>
public static void SetSizeOfSet(StyledElement element, int value)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
element.SetValue(SizeOfSetProperty, value);
}
/// <summary>
/// Helper for reading SizeOfSet property from a StyledElement.
/// </summary>
public static int GetSizeOfSet(StyledElement element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return ((int)element.GetValue(SizeOfSetProperty));
}
}
}

11
src/Avalonia.Controls/Automation/AutomationProperty.cs

@ -0,0 +1,11 @@
namespace Avalonia.Automation
{
/// <summary>
/// Identifies a property of <see cref="AutomationElementIdentifiers"/> or of a specific
/// control pattern.
/// </summary>
public sealed class AutomationProperty
{
internal AutomationProperty() { }
}
}

21
src/Avalonia.Controls/Automation/AutomationPropertyChangedEventArgs.cs

@ -0,0 +1,21 @@
using System;
namespace Avalonia.Automation
{
public class AutomationPropertyChangedEventArgs : EventArgs
{
public AutomationPropertyChangedEventArgs(
AutomationProperty property,
object? oldValue,
object? newValue)
{
Property = property;
OldValue = oldValue;
NewValue = newValue;
}
public AutomationProperty Property { get; }
public object? OldValue { get; }
public object? NewValue { get; }
}
}

10
src/Avalonia.Controls/Automation/ElementNotEnabledException.cs

@ -0,0 +1,10 @@
using System;
namespace Avalonia.Automation
{
public class ElementNotEnabledException : Exception
{
public ElementNotEnabledException() : base("Element not enabled.") { }
public ElementNotEnabledException(string message) : base(message) { }
}
}

15
src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs

@ -0,0 +1,15 @@
using Avalonia.Automation.Provider;
namespace Avalonia.Automation
{
/// <summary>
/// Contains values used as identifiers by <see cref="IExpandCollapseProvider"/>.
/// </summary>
public static class ExpandCollapsePatternIdentifiers
{
/// <summary>
/// Identifies <see cref="IExpandCollapseProvider.ExpandCollapseState"/> automation property.
/// </summary>
public static AutomationProperty ExpandCollapseStateProperty { get; } = new AutomationProperty();
}
}

29
src/Avalonia.Controls/Automation/ExpandCollapseState.cs

@ -0,0 +1,29 @@
namespace Avalonia.Automation
{
/// <summary>
/// Contains values that specify the <see cref="ExpandCollapseState"/> of a UI Automation element.
/// </summary>
public enum ExpandCollapseState
{
/// <summary>
/// No child nodes, controls, or content of the UI Automation element are displayed.
/// </summary>
Collapsed,
/// <summary>
/// All child nodes, controls or content of the UI Automation element are displayed.
/// </summary>
Expanded,
/// <summary>
/// The UI Automation element has no child nodes, controls, or content to display.
/// </summary>
LeafNode,
/// <summary>
/// Some, but not all, child nodes, controls, or content of the UI Automation element are
/// displayed.
/// </summary>
PartiallyExpanded
}
}

26
src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs

@ -0,0 +1,26 @@
namespace Avalonia.Automation
{
/// <summary>
/// This enum offers different ways of evaluating the IsOffscreen AutomationProperty
/// </summary>
public enum IsOffscreenBehavior
{
/// <summary>
/// The AutomationProperty IsOffscreen is calculated based on IsVisible.
/// </summary>
Default,
/// <summary>
/// The AutomationProperty IsOffscreen is false.
/// </summary>
Onscreen,
/// <summary>
/// The AutomationProperty IsOffscreen if true.
/// </summary>
Offscreen,
/// <summary>
/// The AutomationProperty IsOffscreen is calculated based on clip regions.
/// </summary>
FromClip,
}
}

263
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Automation.Peers
{
public enum AutomationControlType
{
None,
Button,
Calendar,
CheckBox,
ComboBox,
ComboBoxItem,
Edit,
Hyperlink,
Image,
ListItem,
List,
Menu,
MenuBar,
MenuItem,
ProgressBar,
RadioButton,
ScrollBar,
Slider,
Spinner,
StatusBar,
Tab,
TabItem,
Text,
ToolBar,
ToolTip,
Tree,
TreeItem,
Custom,
Group,
Thumb,
DataGrid,
DataItem,
Document,
SplitButton,
Window,
Pane,
Header,
HeaderItem,
Table,
TitleBar,
Separator,
}
/// <summary>
/// Provides a base class that exposes an element to UI Automation.
/// </summary>
public abstract class AutomationPeer
{
/// <summary>
/// Attempts to bring the element associated with the automation peer into view.
/// </summary>
public void BringIntoView() => BringIntoViewCore();
/// <summary>
/// Gets the accelerator key combinations for the element that is associated with the UI
/// Automation peer.
/// </summary>
public string? GetAcceleratorKey() => GetAcceleratorKeyCore();
/// <summary>
/// Gets the access key for the element that is associated with the automation peer.
/// </summary>
public string? GetAccessKey() => GetAccessKeyCore();
/// <summary>
/// Gets the control type for the element that is associated with the UI Automation peer.
/// </summary>
public AutomationControlType GetAutomationControlType() => GetControlTypeOverrideCore();
/// <summary>
/// Gets the automation ID of the element that is associated with the UI Automation peer.
/// </summary>
public string? GetAutomationId() => GetAutomationIdCore();
/// <summary>
/// Gets the bounding rectangle of the element that is associated with the automation peer
/// in top-level coordinates.
/// </summary>
public Rect GetBoundingRectangle() => GetBoundingRectangleCore();
/// <summary>
/// Gets the child automation peers.
/// </summary>
public IReadOnlyList<AutomationPeer> GetChildren() => GetOrCreateChildrenCore();
/// <summary>
/// Gets a string that describes the class of the element.
/// </summary>
public string GetClassName() => GetClassNameCore() ?? string.Empty;
/// <summary>
/// Gets the automation peer for the label that is targeted to the element.
/// </summary>
/// <returns></returns>
public AutomationPeer? GetLabeledBy() => GetLabeledByCore();
/// <summary>
/// Gets a human-readable localized string that represents the type of the control that is
/// associated with this automation peer.
/// </summary>
public string GetLocalizedControlType() => GetLocalizedControlTypeCore();
/// <summary>
/// Gets text that describes the element that is associated with this automation peer.
/// </summary>
public string GetName() => GetNameCore() ?? string.Empty;
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
/// </summary>
/// <returns></returns>
public AutomationPeer? GetParent() => GetParentCore();
/// <summary>
/// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus.
/// </summary>
public bool HasKeyboardFocus() => HasKeyboardFocusCore();
/// <summary>
/// Gets a value that indicates whether the element that is associated with this automation
/// peer contains data that is presented to the user.
/// </summary>
public bool IsContentElement() => IsControlElement() && IsContentElementCore();
/// <summary>
/// Gets a value that indicates whether the element is understood by the user as
/// interactive or as contributing to the logical structure of the control in the GUI.
/// </summary>
public bool IsControlElement() => IsControlElementCore();
/// <summary>
/// Gets a value indicating whether the control is enabled for user interaction.
/// </summary>
public bool IsEnabled() => IsEnabledCore();
/// <summary>
/// Gets a value that indicates whether the element can accept keyboard focus.
/// </summary>
/// <returns></returns>
public bool IsKeyboardFocusable() => IsKeyboardFocusableCore();
/// <summary>
/// Sets the keyboard focus on the element that is associated with this automation peer.
/// </summary>
public void SetFocus() => SetFocusCore();
/// <summary>
/// Shows the context menu for the element that is associated with this automation peer.
/// </summary>
/// <returns>true if a context menu is present for the element; otherwise false.</returns>
public bool ShowContextMenu() => ShowContextMenuCore();
/// <summary>
/// Tries to get a provider of the specified type from the peer.
/// </summary>
/// <typeparam name="T">The provider type.</typeparam>
/// <returns>The provider, or null if not implemented on this peer.</returns>
public T? GetProvider<T>() => (T?)GetProviderCore(typeof(T));
/// <summary>
/// Occurs when the children of the automation peer have changed.
/// </summary>
public event EventHandler? ChildrenChanged;
/// <summary>
/// Occurs when a property value of the automation peer has changed.
/// </summary>
public event EventHandler<AutomationPropertyChangedEventArgs>? PropertyChanged;
/// <summary>
/// Raises an event to notify the automation client the the children of the peer have changed.
/// </summary>
protected void RaiseChildrenChangedEvent() => ChildrenChanged?.Invoke(this, EventArgs.Empty);
/// <summary>
/// Raises an event to notify the automation client of a changed property value.
/// </summary>
/// <param name="property">The property that changed.</param>
/// <param name="oldValue">The previous value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
public void RaisePropertyChangedEvent(
AutomationProperty property,
object? oldValue,
object? newValue)
{
PropertyChanged?.Invoke(this, new AutomationPropertyChangedEventArgs(property, oldValue, newValue));
}
protected virtual string GetLocalizedControlTypeCore()
{
var controlType = GetAutomationControlType();
return controlType switch
{
AutomationControlType.CheckBox => "check box",
AutomationControlType.ComboBox => "combo box",
AutomationControlType.ListItem => "list item",
AutomationControlType.MenuBar => "menu bar",
AutomationControlType.MenuItem => "menu item",
AutomationControlType.ProgressBar => "progress bar",
AutomationControlType.RadioButton => "radio button",
AutomationControlType.ScrollBar => "scroll bar",
AutomationControlType.StatusBar => "status bar",
AutomationControlType.TabItem => "tab item",
AutomationControlType.ToolBar => "toolbar",
AutomationControlType.ToolTip => "tooltip",
AutomationControlType.TreeItem => "tree item",
AutomationControlType.Custom => "custom",
AutomationControlType.DataGrid => "data grid",
AutomationControlType.DataItem => "data item",
AutomationControlType.SplitButton => "split button",
AutomationControlType.HeaderItem => "header item",
AutomationControlType.TitleBar => "title bar",
_ => controlType.ToString().ToLowerInvariant(),
};
}
protected abstract void BringIntoViewCore();
protected abstract string? GetAcceleratorKeyCore();
protected abstract string? GetAccessKeyCore();
protected abstract AutomationControlType GetAutomationControlTypeCore();
protected abstract string? GetAutomationIdCore();
protected abstract Rect GetBoundingRectangleCore();
protected abstract IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore();
protected abstract string GetClassNameCore();
protected abstract AutomationPeer? GetLabeledByCore();
protected abstract string? GetNameCore();
protected abstract AutomationPeer? GetParentCore();
protected abstract bool HasKeyboardFocusCore();
protected abstract bool IsContentElementCore();
protected abstract bool IsControlElementCore();
protected abstract bool IsEnabledCore();
protected abstract bool IsKeyboardFocusableCore();
protected abstract void SetFocusCore();
protected abstract bool ShowContextMenuCore();
protected virtual AutomationControlType GetControlTypeOverrideCore()
{
return GetAutomationControlTypeCore();
}
protected virtual object? GetProviderCore(Type providerType)
{
return providerType.IsAssignableFrom(this.GetType()) ? this : null;
}
protected internal abstract bool TrySetParent(AutomationPeer? parent);
protected void EnsureEnabled()
{
if (!IsEnabled())
throw new ElementNotEnabledException();
}
}
}

43
src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs

@ -0,0 +1,43 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class ButtonAutomationPeer : ContentControlAutomationPeer,
IInvokeProvider
{
public ButtonAutomationPeer(Button owner)
: base(owner)
{
}
public new Button Owner => (Button)base.Owner;
public void Invoke()
{
EnsureEnabled();
(Owner as Button)?.PerformClick();
}
protected override string? GetAcceleratorKeyCore()
{
var result = base.GetAcceleratorKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
result = Owner.HotKey?.ToString();
}
return result;
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Button;
}
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
}
}

137
src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer,
IExpandCollapseProvider,
IValueProvider
{
private UnrealizedSelectionPeer[]? _selection;
public ComboBoxAutomationPeer(ComboBox owner)
: base(owner)
{
}
public new ComboBox Owner => (ComboBox)base.Owner;
public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen);
public bool ShowsMenu => true;
public void Collapse() => Owner.IsDropDownOpen = false;
public void Expand() => Owner.IsDropDownOpen = true;
bool IValueProvider.IsReadOnly => true;
string? IValueProvider.Value
{
get
{
var selection = GetSelection();
return selection.Count == 1 ? selection[0].GetName() : null;
}
}
void IValueProvider.SetValue(string? value) => throw new NotSupportedException();
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.ComboBox;
}
protected override IReadOnlyList<AutomationPeer>? GetSelectionCore()
{
if (ExpandCollapseState == ExpandCollapseState.Expanded)
return base.GetSelectionCore();
// If the combo box is not open then we won't have an ItemsPresenter so the default
// GetSelectionCore implementation won't work. For this case we create a separate
// peer to represent the unrealized item.
if (Owner.SelectedItem is object selection)
{
_selection ??= new[] { new UnrealizedSelectionPeer(this) };
_selection[0].Item = selection;
return _selection;
}
return null;
}
protected override void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
base.OwnerPropertyChanged(sender, e);
if (e.Property == ComboBox.IsDropDownOpenProperty)
{
RaisePropertyChangedEvent(
ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
ToState((bool)e.OldValue!),
ToState((bool)e.NewValue!));
}
}
private ExpandCollapseState ToState(bool value)
{
return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
}
private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer
{
private readonly ComboBoxAutomationPeer _owner;
private object? _item;
public UnrealizedSelectionPeer(ComboBoxAutomationPeer owner)
{
_owner = owner;
}
public object? Item
{
get => _item;
set
{
if (_item != value)
{
var oldValue = GetNameCore();
_item = value;
RaisePropertyChangedEvent(
AutomationElementIdentifiers.NameProperty,
oldValue,
GetNameCore());
}
}
}
protected override string? GetAcceleratorKeyCore() => null;
protected override string? GetAccessKeyCore() => null;
protected override string? GetAutomationIdCore() => null;
protected override string GetClassNameCore() => typeof(ComboBoxItem).Name;
protected override AutomationPeer? GetLabeledByCore() => null;
protected override AutomationPeer? GetParentCore() => _owner;
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem;
protected override string? GetNameCore()
{
if (_item is Control c)
{
var result = AutomationProperties.GetName(c);
if (result is null && c is ContentControl cc && cc.Presenter?.Child is TextBlock text)
{
result = text.Text;
}
if (result is null)
{
result = c.GetValue(ContentControl.ContentProperty)?.ToString();
}
return result;
}
return _item?.ToString();
}
}
}
}

36
src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs

@ -0,0 +1,36 @@
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class ContentControlAutomationPeer : ControlAutomationPeer
{
protected ContentControlAutomationPeer(ContentControl owner)
: base(owner)
{
}
public new ContentControl Owner => (ContentControl)base.Owner;
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane;
protected override string? GetNameCore()
{
var result = base.GetNameCore();
if (result is null && Owner.Presenter?.Child is TextBlock text)
{
result = text.Text;
}
if (result is null)
{
result = Owner.Content?.ToString();
}
return result;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
}
}

221
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Automation.Peers
{
/// <summary>
/// An automation peer which represents a <see cref="Control"/> element.
/// </summary>
public class ControlAutomationPeer : AutomationPeer
{
private IReadOnlyList<AutomationPeer>? _children;
private bool _childrenValid;
private AutomationPeer? _parent;
private bool _parentValid;
public ControlAutomationPeer(Control owner)
{
Owner = owner ?? throw new ArgumentNullException("owner");
Initialize();
}
public Control Owner { get; }
public AutomationPeer GetOrCreate(Control element)
{
if (element == Owner)
return this;
return CreatePeerForElement(element);
}
public static AutomationPeer CreatePeerForElement(Control element)
{
return element.GetOrCreateAutomationPeer();
}
protected override void BringIntoViewCore() => Owner.BringIntoView();
protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
{
var children = _children ?? Array.Empty<AutomationPeer>();
if (_childrenValid)
return children;
var newChildren = GetChildrenCore() ?? Array.Empty<AutomationPeer>();
foreach (var peer in children.Except(newChildren))
peer.TrySetParent(null);
foreach (var peer in newChildren)
peer.TrySetParent(this);
_childrenValid = true;
return _children = newChildren;
}
protected virtual IReadOnlyList<AutomationPeer>? GetChildrenCore()
{
var children = ((IVisual)Owner).VisualChildren;
if (children.Count == 0)
return null;
var result = new List<AutomationPeer>();
foreach (var child in children)
{
if (child is Control c && c.IsVisible)
{
result.Add(GetOrCreate(c));
}
}
return result;
}
protected override AutomationPeer? GetLabeledByCore()
{
var label = AutomationProperties.GetLabeledBy(Owner);
return label is Control c ? GetOrCreate(c) : null;
}
protected override string? GetNameCore()
{
var result = AutomationProperties.GetName(Owner);
if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy)
{
return labeledBy.GetName();
}
return null;
}
protected override AutomationPeer? GetParentCore()
{
EnsureConnected();
return _parent;
}
/// <summary>
/// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
/// </summary>
protected void InvalidateChildren()
{
_childrenValid = false;
RaiseChildrenChangedEvent();
}
/// <summary>
/// Invalidates the peer's parent.
/// </summary>
protected void InvalidateParent()
{
_parent = null;
_parentValid = false;
}
protected override bool ShowContextMenuCore()
{
var c = Owner;
while (c is object)
{
if (c.ContextMenu is object)
{
c.ContextMenu.Open(c);
return true;
}
c = c.Parent as Control;
}
return false;
}
protected internal override bool TrySetParent(AutomationPeer? parent)
{
_parent = parent;
return true;
}
protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner);
protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner);
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
protected override string GetClassNameCore() => Owner.GetType().Name;
protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
protected override bool IsContentElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Content;
protected override bool IsControlElementCore() => AutomationProperties.GetAccessibilityView(Owner) >= AccessibilityView.Control;
protected override bool IsEnabledCore() => Owner.IsEnabled;
protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
protected override void SetFocusCore() => Owner.Focus();
protected override AutomationControlType GetControlTypeOverrideCore()
{
return AutomationProperties.GetControlTypeOverride(Owner) ?? GetAutomationControlTypeCore();
}
private static Rect GetBounds(TransformedBounds? bounds)
{
return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default;
}
private void Initialize()
{
Owner.PropertyChanged += OwnerPropertyChanged;
var visualChildren = ((IVisual)Owner).VisualChildren;
visualChildren.CollectionChanged += VisualChildrenChanged;
}
private void VisualChildrenChanged(object? sender, EventArgs e) => InvalidateChildren();
private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Visual.IsVisibleProperty)
{
var parent = Owner.GetVisualParent();
if (parent is Control c)
(GetOrCreate(c) as ControlAutomationPeer)?.InvalidateChildren();
}
else if (e.Property == Visual.TransformedBoundsProperty)
{
RaisePropertyChangedEvent(
AutomationElementIdentifiers.BoundingRectangleProperty,
GetBounds((TransformedBounds?)e.OldValue),
GetBounds((TransformedBounds?)e.NewValue));
}
else if (e.Property == Visual.VisualParentProperty)
{
InvalidateParent();
}
}
private void EnsureConnected()
{
if (!_parentValid)
{
var parent = Owner.GetVisualParent();
while (parent is object)
{
if (parent is Control c)
{
var parentPeer = GetOrCreate(c);
parentPeer.GetChildren();
}
parent = parent.GetVisualParent();
}
_parentValid = true;
}
}
}
}

54
src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs

@ -0,0 +1,54 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider
{
private bool _searchedForScrollable;
private IScrollProvider? _scroller;
public ItemsControlAutomationPeer(ItemsControl owner)
: base(owner)
{
}
public new ItemsControl Owner => (ItemsControl)base.Owner;
public bool HorizontallyScrollable => _scroller?.HorizontallyScrollable ?? false;
public double HorizontalScrollPercent => _scroller?.HorizontalScrollPercent ?? -1;
public double HorizontalViewSize => _scroller?.HorizontalViewSize ?? 0;
public bool VerticallyScrollable => _scroller?.VerticallyScrollable ?? false;
public double VerticalScrollPercent => _scroller?.VerticalScrollPercent ?? -1;
public double VerticalViewSize => _scroller?.VerticalViewSize ?? 0;
protected virtual IScrollProvider? Scroller
{
get
{
if (!_searchedForScrollable)
{
if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
_scroller = GetOrCreate(scrollable) as IScrollProvider;
_searchedForScrollable = true;
}
return _scroller;
}
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.List;
}
public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
{
_scroller?.Scroll(horizontalAmount, verticalAmount);
}
public void SetScrollPercent(double horizontalPercent, double verticalPercent)
{
_scroller?.SetScrollPercent(horizontalPercent, verticalPercent);
}
}
}

82
src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

@ -0,0 +1,82 @@
using System;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
namespace Avalonia.Automation.Peers
{
public class ListItemAutomationPeer : ContentControlAutomationPeer,
ISelectionItemProvider
{
public ListItemAutomationPeer(ContentControl owner)
: base(owner)
{
}
public bool IsSelected => Owner.GetValue(ListBoxItem.IsSelectedProperty);
public ISelectionProvider? SelectionContainer
{
get
{
if (Owner.Parent is Control parent)
{
var parentPeer = GetOrCreate(parent);
return parentPeer as ISelectionProvider;
}
return null;
}
}
public void Select()
{
EnsureEnabled();
if (Owner.Parent is SelectingItemsControl parent)
{
var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
if (index != -1)
parent.SelectedIndex = index;
}
}
void ISelectionItemProvider.AddToSelection()
{
EnsureEnabled();
if (Owner.Parent is ItemsControl parent &&
parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
if (index != -1)
selectionModel.Select(index);
}
}
void ISelectionItemProvider.RemoveFromSelection()
{
EnsureEnabled();
if (Owner.Parent is ItemsControl parent &&
parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
{
var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
if (index != -1)
selectionModel.Deselect(index);
}
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.ListItem;
}
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
}
}

59
src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs

@ -0,0 +1,59 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public class MenuItemAutomationPeer : ControlAutomationPeer
{
public MenuItemAutomationPeer(MenuItem owner)
: base(owner)
{
}
public new MenuItem Owner => (MenuItem)base.Owner;
protected override string? GetAccessKeyCore()
{
var result = base.GetAccessKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
if (Owner.HeaderPresenter?.Child is AccessText accessText)
{
result = accessText.AccessKey.ToString();
}
}
return result;
}
protected override string? GetAcceleratorKeyCore()
{
var result = base.GetAcceleratorKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
result = Owner.InputGesture?.ToString();
}
return result;
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.MenuItem;
}
protected override string? GetNameCore()
{
var result = base.GetNameCore();
if (result is null && Owner.Header is string header)
{
result = AccessText.RemoveAccessKeyMarker(header);
}
return result;
}
}
}

25
src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs

@ -0,0 +1,25 @@
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
/// <summary>
/// An automation peer which represents an element that is exposed to automation as non-
/// interactive or as not contributing to the logical structure of the application.
/// </summary>
public class NoneAutomationPeer : ControlAutomationPeer
{
public NoneAutomationPeer(Control owner)
: base(owner)
{
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.None;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
}
}

48
src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public class PopupAutomationPeer : ControlAutomationPeer
{
public PopupAutomationPeer(Popup owner)
: base(owner)
{
owner.Opened += PopupOpenedClosed;
owner.Closed += PopupOpenedClosed;
}
protected override IReadOnlyList<AutomationPeer>? GetChildrenCore()
{
var host = (IPopupHostProvider)Owner;
return host.PopupHost is Control c ? new[] { GetOrCreate(c) } : null;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
private void PopupOpenedClosed(object? sender, EventArgs e)
{
// This is golden. We're following WPF's automation peer API here where the
// parent of a peer is set when another peer returns it as a child. We want to
// add the popup root as a child of the popup, so we need to return it as a
// child right? Yeah except invalidating children doesn't automatically cause
// UIA to re-read the children meaning that the parent doesn't get set. So the
// MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API
// IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the
// TrySetParent API internally to work around this. We're exposing it publicly
// to shame whoever came up with this abomination of an API.
GetPopupRoot()?.TrySetParent(this);
InvalidateChildren();
}
private AutomationPeer? GetPopupRoot()
{
var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control;
return popupRoot is object ? GetOrCreate(popupRoot) : null;
}
}
}

40
src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs

@ -0,0 +1,40 @@
using System;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public class PopupRootAutomationPeer : WindowBaseAutomationPeer
{
public PopupRootAutomationPeer(PopupRoot owner)
: base(owner)
{
if (owner.IsVisible)
StartTrackingFocus();
else
owner.Opened += OnOpened;
owner.Closed += OnClosed;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
protected override AutomationPeer? GetParentCore()
{
var parent = base.GetParentCore();
return parent;
}
private void OnOpened(object? sender, EventArgs e)
{
((PopupRoot)Owner).Opened -= OnOpened;
StartTrackingFocus();
}
private void OnClosed(object? sender, EventArgs e)
{
((PopupRoot)Owner).Closed -= OnClosed;
StopTrackingFocus();
}
}
}

34
src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs

@ -0,0 +1,34 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider
{
public RangeBaseAutomationPeer(RangeBase owner)
: base(owner)
{
owner.PropertyChanged += OwnerPropertyChanged;
}
public new RangeBase Owner => (RangeBase)base.Owner;
public virtual bool IsReadOnly => false;
public double Maximum => Owner.Maximum;
public double Minimum => Owner.Minimum;
public double Value => Owner.Value;
public double SmallChange => Owner.SmallChange;
public double LargeChange => Owner.LargeChange;
public void SetValue(double value) => Owner.Value = value;
protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == RangeBase.MinimumProperty)
RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue);
else if (e.Property == RangeBase.MaximumProperty)
RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MaximumProperty, e.OldValue, e.NewValue);
else if (e.Property == RangeBase.ValueProperty)
RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue);
}
}
}

172
src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs

@ -0,0 +1,172 @@
using System;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Utilities;
namespace Avalonia.Automation.Peers
{
public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider
{
public ScrollViewerAutomationPeer(ScrollViewer owner)
: base(owner)
{
}
public new ScrollViewer Owner => (ScrollViewer)base.Owner;
public bool HorizontallyScrollable
{
get => MathUtilities.GreaterThan(Owner.Extent.Width, Owner.Viewport.Width);
}
public double HorizontalScrollPercent
{
get
{
if (!HorizontallyScrollable)
return ScrollPatternIdentifiers.NoScroll;
return (double)(Owner.Offset.X * 100.0 / (Owner.Extent.Width - Owner.Viewport.Width));
}
}
public double HorizontalViewSize
{
get
{
if (MathUtilities.IsZero(Owner.Extent.Width))
return 100;
return Math.Min(100, Owner.Viewport.Width * 100.0 / Owner.Extent.Width);
}
}
public bool VerticallyScrollable
{
get => MathUtilities.GreaterThan(Owner.Extent.Height, Owner.Viewport.Height);
}
public double VerticalScrollPercent
{
get
{
if (!VerticallyScrollable)
return ScrollPatternIdentifiers.NoScroll;
return (double)(Owner.Offset.Y * 100.0 / (Owner.Extent.Height - Owner.Viewport.Height));
}
}
public double VerticalViewSize
{
get
{
if (MathUtilities.IsZero(Owner.Extent.Height))
return 100;
return Math.Min(100, Owner.Viewport.Height * 100.0 / Owner.Extent.Height);
}
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Pane;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore()
{
// Return false if the control is part of a control template.
return Owner.TemplatedParent is null && base.IsControlElementCore();
}
public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
{
if (!IsEnabled())
throw new ElementNotEnabledException();
var scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount;
var scrollVertically = verticalAmount != ScrollAmount.NoAmount;
if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable)
{
throw new InvalidOperationException("Operation cannot be performed");
}
switch (horizontalAmount)
{
case ScrollAmount.LargeDecrement:
Owner.PageLeft();
break;
case ScrollAmount.SmallDecrement:
Owner.LineLeft();
break;
case ScrollAmount.SmallIncrement:
Owner.LineRight();
break;
case ScrollAmount.LargeIncrement:
Owner.PageRight();
break;
case ScrollAmount.NoAmount:
break;
default:
throw new InvalidOperationException("Operation cannot be performed");
}
switch (verticalAmount)
{
case ScrollAmount.LargeDecrement:
Owner.PageUp();
break;
case ScrollAmount.SmallDecrement:
Owner.LineUp();
break;
case ScrollAmount.SmallIncrement:
Owner.LineDown();
break;
case ScrollAmount.LargeIncrement:
Owner.PageDown();
break;
case ScrollAmount.NoAmount:
break;
default:
throw new InvalidOperationException("Operation cannot be performed");
}
}
public void SetScrollPercent(double horizontalPercent, double verticalPercent)
{
if (!IsEnabled())
throw new ElementNotEnabledException();
var scrollHorizontally = horizontalPercent != ScrollPatternIdentifiers.NoScroll;
var scrollVertically = verticalPercent != ScrollPatternIdentifiers.NoScroll;
if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable)
{
throw new InvalidOperationException("Operation cannot be performed");
}
if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0))
{
throw new ArgumentOutOfRangeException("horizontalPercent");
}
if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0))
{
throw new ArgumentOutOfRangeException("verticalPercent");
}
var offset = Owner.Offset;
if (scrollHorizontally)
{
offset = offset.WithX((Owner.Extent.Width - Owner.Viewport.Width) * horizontalPercent * 0.01);
}
if (scrollVertically)
{
offset = offset.WithY((Owner.Extent.Height - Owner.Viewport.Height) * verticalPercent * 0.01);
}
Owner.Offset = offset;
}
}
}

84
src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.VisualTree;
namespace Avalonia.Automation.Peers
{
public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer,
ISelectionProvider
{
private ISelectionModel _selection;
protected SelectingItemsControlAutomationPeer(SelectingItemsControl owner)
: base(owner)
{
_selection = owner.GetValue(ListBox.SelectionProperty);
_selection.SelectionChanged += OwnerSelectionChanged;
owner.PropertyChanged += OwnerPropertyChanged;
}
public bool CanSelectMultiple => GetSelectionModeCore().HasAllFlags(SelectionMode.Multiple);
public bool IsSelectionRequired => GetSelectionModeCore().HasAllFlags(SelectionMode.AlwaysSelected);
public IReadOnlyList<AutomationPeer> GetSelection() => GetSelectionCore() ?? Array.Empty<AutomationPeer>();
protected virtual IReadOnlyList<AutomationPeer>? GetSelectionCore()
{
List<AutomationPeer>? result = null;
if (Owner is SelectingItemsControl owner)
{
var selection = Owner.GetValue(ListBox.SelectionProperty);
foreach (var i in selection.SelectedIndexes)
{
var container = owner.ItemContainerGenerator.ContainerFromIndex(i);
if (container is Control c && ((IVisual)c).IsAttachedToVisualTree)
{
var peer = GetOrCreate(c);
if (peer is object)
{
result ??= new List<AutomationPeer>();
result.Add(peer);
}
}
}
return result;
}
return result;
}
protected virtual SelectionMode GetSelectionModeCore()
{
return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single;
}
protected virtual void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ListBox.SelectionProperty)
{
_selection.SelectionChanged -= OwnerSelectionChanged;
_selection = Owner.GetValue(ListBox.SelectionProperty);
_selection.SelectionChanged += OwnerSelectionChanged;
RaiseSelectionChanged();
}
}
protected virtual void OwnerSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e)
{
RaiseSelectionChanged();
}
private void RaiseSelectionChanged()
{
RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null);
}
}
}

27
src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs

@ -0,0 +1,27 @@
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class TextBlockAutomationPeer : ControlAutomationPeer
{
public TextBlockAutomationPeer(TextBlock owner)
: base(owner)
{
}
public new TextBlock Owner => (TextBlock)base.Owner;
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Text;
}
protected override string? GetNameCore() => Owner.Text;
protected override bool IsControlElementCore()
{
// Return false if the control is part of a control template.
return Owner.TemplatedParent is null && base.IsControlElementCore();
}
}
}

23
src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs

@ -0,0 +1,23 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider
{
public TextBoxAutomationPeer(TextBox owner)
: base(owner)
{
}
public new TextBox Owner => (TextBox)base.Owner;
public bool IsReadOnly => Owner.IsReadOnly;
public string? Value => Owner.Text;
public void SetValue(string? value) => Owner.Text = value;
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Edit;
}
}
}

39
src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs

@ -0,0 +1,39 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
{
public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider
{
public ToggleButtonAutomationPeer(ToggleButton owner)
: base(owner)
{
}
public new ToggleButton Owner => (ToggleButton)base.Owner;
ToggleState IToggleProvider.ToggleState
{
get => Owner.IsChecked switch
{
true => ToggleState.On,
false => ToggleState.Off,
null => ToggleState.Indeterminate,
};
}
void IToggleProvider.Toggle()
{
EnsureEnabled();
Owner.PerformClick();
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Button;
}
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
}
}

24
src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Automation.Peers
{
/// <summary>
/// An automation peer which represents an unrealized element
/// </summary>
public abstract class UnrealizedElementAutomationPeer : AutomationPeer
{
public void SetParent(AutomationPeer? parent) => TrySetParent(parent);
protected override void BringIntoViewCore() => GetParent()?.BringIntoView();
protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default;
protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore() => Array.Empty<AutomationPeer>();
protected override bool HasKeyboardFocusCore() => false;
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
protected override bool IsEnabledCore() => true;
protected override bool IsKeyboardFocusableCore() => false;
protected override void SetFocusCore() { }
protected override bool ShowContextMenuCore() => false;
protected internal override bool TrySetParent(AutomationPeer? parent) => false;
}
}

36
src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs

@ -0,0 +1,36 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
public class WindowAutomationPeer : WindowBaseAutomationPeer
{
public WindowAutomationPeer(Window owner)
: base(owner)
{
if (owner.IsVisible)
StartTrackingFocus();
else
owner.Opened += OnOpened;
owner.Closed += OnClosed;
}
public new Window Owner => (Window)base.Owner;
protected override string? GetNameCore() => Owner.Title;
private void OnOpened(object? sender, EventArgs e)
{
Owner.Opened -= OnOpened;
StartTrackingFocus();
}
private void OnClosed(object? sender, EventArgs e)
{
Owner.Closed -= OnClosed;
StopTrackingFocus();
}
}
}

78
src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

@ -0,0 +1,78 @@
using System;
using System.ComponentModel;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Automation.Peers
{
public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider
{
private Control? _focus;
public WindowBaseAutomationPeer(WindowBase owner)
: base(owner)
{
}
public new WindowBase Owner => (WindowBase)base.Owner;
public ITopLevelImpl? PlatformImpl => Owner.PlatformImpl;
public event EventHandler? FocusChanged;
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Window;
}
public AutomationPeer? GetFocus() => _focus is object ? GetOrCreate(_focus) : null;
public AutomationPeer? GetPeerFromPoint(Point p)
{
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true);
return hit is object ? GetOrCreate(hit) : null;
}
protected void StartTrackingFocus()
{
if (KeyboardDevice.Instance is not null)
{
KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged;
OnFocusChanged(KeyboardDevice.Instance.FocusedElement);
}
}
protected void StopTrackingFocus()
{
if (KeyboardDevice.Instance is not null)
KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged;
}
private void OnFocusChanged(IInputElement? focus)
{
var oldFocus = _focus;
_focus = focus?.VisualRoot == Owner ? focus as Control : null;
if (_focus != oldFocus)
{
var peer = _focus is object ?
_focus == Owner ? this :
GetOrCreate(_focus) : null;
FocusChanged?.Invoke(this, EventArgs.Empty);
}
}
private void KeyboardDevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
{
OnFocusChanged(KeyboardDevice.Instance!.FocusedElement);
}
}
}
}

33
src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs

@ -0,0 +1,33 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support UI Automation client access to controls that
/// visually expand to display content and collapse to hide content.
/// </summary>
public interface IExpandCollapseProvider
{
/// <summary>
/// Gets the state, expanded or collapsed, of the control.
/// </summary>
ExpandCollapseState ExpandCollapseState { get; }
/// <summary>
/// Gets a value indicating whether expanding the element shows a menu of items to the user,
/// such as drop-down list.
/// </summary>
/// <remarks>
/// Used in OSX to enable the "Show Menu" action on the element.
/// </remarks>
bool ShowsMenu { get; }
/// <summary>
/// Displays all child nodes, controls, or content of the control.
/// </summary>
void Expand();
/// <summary>
/// Hides all nodes, controls, or content that are descendants of the control.
/// </summary>
void Collapse();
}
}

15
src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs

@ -0,0 +1,15 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support UI Automation client access to controls that
/// initiate or perform a single, unambiguous action and do not maintain state when
/// activated.
/// </summary>
public interface IInvokeProvider
{
/// <summary>
/// Sends a request to activate a control and initiate its single, unambiguous action.
/// </summary>
void Invoke();
}
}

47
src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs

@ -0,0 +1,47 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support access by a UI Automation client to controls
/// that can be set to a value within a range.
/// </summary>
public interface IRangeValueProvider
{
/// <summary>
/// Gets a value that indicates whether the value of a control is read-only.
/// </summary>
bool IsReadOnly { get; }
/// <summary>
/// Gets the minimum range value that is supported by the control.
/// </summary>
double Minimum { get; }
/// <summary>
/// Gets the maximum range value that is supported by the control.
/// </summary>
double Maximum { get; }
/// <summary>
/// Gets the value of the control.
/// </summary>
double Value { get; }
/// <summary>
/// Gets the value that is added to or subtracted from the Value property when a large
/// change is made, such as with the PAGE DOWN key.
/// </summary>
double LargeChange { get; }
/// <summary>
/// Gets the value that is added to or subtracted from the Value property when a small
/// change is made, such as with an arrow key.
/// </summary>
double SmallChange { get; }
/// <summary>
/// Sets the value of the control.
/// </summary>
/// <param name="value">The value to set.</param>
public void SetValue(double value);
}
}

14
src/Avalonia.Controls/Automation/Provider/IRootProvider.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Platform;
namespace Avalonia.Automation.Provider
{
public interface IRootProvider
{
ITopLevelImpl? PlatformImpl { get; }
AutomationPeer? GetFocus();
AutomationPeer? GetPeerFromPoint(Point p);
event EventHandler? FocusChanged;
}
}

71
src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs

@ -0,0 +1,71 @@
namespace Avalonia.Automation.Provider
{
public enum ScrollAmount
{
LargeDecrement,
SmallDecrement,
NoAmount,
LargeIncrement,
SmallIncrement,
}
/// <summary>
/// Exposes methods and properties to support access by a UI Automation client to a control
/// that acts as a scrollable container for a collection of child objects.
/// </summary>
public interface IScrollProvider
{
/// <summary>
/// Gets a value that indicates whether the control can scroll horizontally.
/// </summary>
bool HorizontallyScrollable { get; }
/// <summary>
/// Gets the current horizontal scroll position.
/// </summary>
double HorizontalScrollPercent { get; }
/// <summary>
/// Gets the current horizontal view size.
/// </summary>
double HorizontalViewSize { get; }
/// <summary>
/// Gets a value that indicates whether the control can scroll vertically.
/// </summary>
bool VerticallyScrollable { get; }
/// <summary>
/// Gets the current vertical scroll position.
/// </summary>
double VerticalScrollPercent { get; }
/// <summary>
/// Gets the vertical view size.
/// </summary>
double VerticalViewSize { get; }
/// <summary>
/// Scrolls the visible region of the content area horizontally and vertically.
/// </summary>
/// <param name="horizontalAmount">The horizontal increment specific to the control.</param>
/// <param name="verticalAmount">The vertical increment specific to the control.</param>
void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount);
/// <summary>
/// Sets the horizontal and vertical scroll position as a percentage of the total content
/// area within the control.
/// </summary>
/// <param name="horizontalPercent">
/// The horizontal position as a percentage of the content area's total range.
/// <see cref="ScrollPatternIdentifiers.NoScroll"/> should be passed in if the control
/// cannot be scrolled in this direction.
/// </param>
/// <param name="verticalPercent">
/// The vertical position as a percentage of the content area's total range.
/// <see cref="ScrollPatternIdentifiers.NoScroll"/> should be passed in if the control
/// cannot be scrolled in this direction.
/// </param>
void SetScrollPercent(double horizontalPercent, double verticalPercent);
}
}

35
src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs

@ -0,0 +1,35 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support access by a UI Automation client to individual,
/// selectable child controls of containers that implement <see cref="ISelectionProvider"/>.
/// </summary>
public interface ISelectionItemProvider
{
/// <summary>
/// Gets a value that indicates whether an item is selected.
/// </summary>
bool IsSelected { get; }
/// <summary>
/// Gets the UI Automation provider that implements <see cref="ISelectionProvider"/> and
/// acts as the container for the calling object.
/// </summary>
ISelectionProvider? SelectionContainer { get; }
/// <summary>
/// Adds the current element to the collection of selected items.
/// </summary>
void AddToSelection();
/// <summary>
/// Removes the current element from the collection of selected items.
/// </summary>
void RemoveFromSelection();
/// <summary>
/// Clears any existing selection and then selects the current element.
/// </summary>
void Select();
}
}

29
src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
using Avalonia.Automation.Peers;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support access by a UI Automation client to controls
/// that act as containers for a collection of individual, selectable child items.
/// </summary>
public interface ISelectionProvider
{
/// <summary>
/// Gets a value that indicates whether the provider allows more than one child element
/// to be selected concurrently.
/// </summary>
bool CanSelectMultiple { get; }
/// <summary>
/// Gets a value that indicates whether the provider requires at least one child element
/// to be selected.
/// </summary>
bool IsSelectionRequired { get; }
/// <summary>
/// Retrieves a provider for each child element that is selected.
/// </summary>
IReadOnlyList<AutomationPeer> GetSelection();
}
}

40
src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs

@ -0,0 +1,40 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Contains values that specify the toggle state of a UI Automation element.
/// </summary>
public enum ToggleState
{
/// <summary>
/// The UI Automation element isn't selected, checked, marked, or otherwise activated.
/// </summary>
Off,
/// <summary>
/// The UI Automation element is selected, checked, marked, or otherwise activated.
/// </summary>
On,
/// <summary>
/// The UI Automation element is in an indeterminate state.
/// </summary>
Indeterminate,
}
/// <summary>
/// Exposes methods and properties to support UI Automation client access to controls that can
/// cycle through a set of states and maintain a particular state.
/// </summary>
public interface IToggleProvider
{
/// <summary>
/// Gets the toggle state of the control.
/// </summary>
ToggleState ToggleState { get; }
/// <summary>
/// Cycles through the toggle states of a control.
/// </summary>
void Toggle();
}
}

29
src/Avalonia.Controls/Automation/Provider/IValueProvider.cs

@ -0,0 +1,29 @@
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support access by a UI Automation client to controls
/// that have an intrinsic value that does not span a range and that can be represented as
/// a string.
/// </summary>
public interface IValueProvider
{
/// <summary>
/// Gets a value that indicates whether the value of a control is read-only.
/// </summary>
bool IsReadOnly { get; }
/// <summary>
/// Gets the value of the control.
/// </summary>
public string? Value { get; }
/// <summary>
/// Sets the value of a control.
/// </summary>
/// <param name="value">
/// The value to set. The provider is responsible for converting the value to the
/// appropriate data type.
/// </param>
public void SetValue(string? value);
}
}

30
src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs

@ -0,0 +1,30 @@
using Avalonia.Automation.Provider;
namespace Avalonia.Automation
{
/// <summary>
/// Contains values used as identifiers by <see cref="IRangeValueProvider"/>.
/// </summary>
public static class RangeValuePatternIdentifiers
{
/// <summary>
/// Identifies <see cref="IRangeValueProvider.IsReadOnly"/> automation property.
/// </summary>
public static AutomationProperty IsReadOnlyProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IRangeValueProvider.Minimum"/> automation property.
/// </summary>
public static AutomationProperty MinimumProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IRangeValueProvider.Maximum"/> automation property.
/// </summary>
public static AutomationProperty MaximumProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IRangeValueProvider.Value"/> automation property.
/// </summary>
public static AutomationProperty ValueProperty { get; } = new AutomationProperty();
}
}

45
src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs

@ -0,0 +1,45 @@
using Avalonia.Automation.Provider;
namespace Avalonia.Automation
{
/// <summary>
/// Contains values used as identifiers by <see cref="IScrollProvider"/>.
/// </summary>
public static class ScrollPatternIdentifiers
{
/// <summary>
/// Specifies that scrolling should not be performed.
/// </summary>
public const double NoScroll = -1;
/// <summary>
/// Identifies <see cref="IScrollProvider.HorizontallyScrollable"/> automation property.
/// </summary>
public static AutomationProperty HorizontallyScrollableProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IScrollProvider.HorizontalScrollPercent"/> automation property.
/// </summary>
public static AutomationProperty HorizontalScrollPercentProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IScrollProvider.HorizontalViewSize"/> automation property.
/// </summary>
public static AutomationProperty HorizontalViewSizeProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IScrollProvider.VerticallyScrollable"/> automation property.
/// </summary>
public static AutomationProperty VerticallyScrollableProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IScrollProvider.VerticalScrollPercent"/> automation property.
/// </summary>
public static AutomationProperty VerticalScrollPercentProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="IScrollProvider.VerticalViewSize"/> automation property.
/// </summary>
public static AutomationProperty VerticalViewSizeProperty { get; } = new AutomationProperty();
}
}

25
src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs

@ -0,0 +1,25 @@
using Avalonia.Automation.Provider;
namespace Avalonia.Automation
{
/// <summary>
/// Contains values used as identifiers by <see cref="ISelectionProvider"/>.
/// </summary>
public static class SelectionPatternIdentifiers
{
/// <summary>
/// Identifies <see cref="ISelectionProvider.CanSelectMultiple"/> automation property.
/// </summary>
public static AutomationProperty CanSelectMultipleProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies <see cref="ISelectionProvider.IsSelectionRequired"/> automation property.
/// </summary>
public static AutomationProperty IsSelectionRequiredProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies the property that gets the selected items in a container.
/// </summary>
public static AutomationProperty SelectionProperty { get; } = new AutomationProperty();
}
}

7
src/Avalonia.Controls/Button.cs

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@ -237,7 +238,7 @@ namespace Avalonia.Controls
{
HotKey = _hotkey;
}
base.OnAttachedToLogicalTree(e);
if (Command != null)
@ -455,6 +456,8 @@ namespace Avalonia.Controls
}
}
protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this);
/// <inheritdoc/>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
@ -472,6 +475,8 @@ namespace Avalonia.Controls
}
}
internal void PerformClick() => OnClick();
/// <summary>
/// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
/// </summary>

6
src/Avalonia.Controls/CheckBox.cs

@ -1,3 +1,5 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
@ -7,5 +9,9 @@ namespace Avalonia.Controls
/// </summary>
public class CheckBox : ToggleButton
{
static CheckBox()
{
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<CheckBox>(AutomationControlType.CheckBox);
}
}
}

6
src/Avalonia.Controls/ComboBox.cs

@ -1,5 +1,6 @@
using System;
using System.Linq;
using Avalonia.Automation.Peers;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
@ -295,6 +296,11 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ComboBoxAutomationPeer(this);
}
internal void ItemFocused(ComboBoxItem dropDownItem)
{
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)

7
src/Avalonia.Controls/ComboBoxItem.cs

@ -1,5 +1,7 @@
using System;
using System.Reactive.Linq;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
namespace Avalonia.Controls
{
@ -13,5 +15,10 @@ namespace Avalonia.Controls
this.GetObservable(ComboBoxItem.IsFocusedProperty).Where(focused => focused)
.Subscribe(_ => (Parent as ComboBox)?.ItemFocused(this));
}
static ComboBoxItem()
{
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ComboBoxItem>(AutomationControlType.ComboBoxItem);
}
}
}

4
src/Avalonia.Controls/ContextMenu.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Automation.Peers;
using System.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Generators;
@ -13,6 +14,7 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.Automation;
namespace Avalonia.Controls
{
@ -107,6 +109,8 @@ namespace Avalonia.Controls
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
}
/// <summary>

20
src/Avalonia.Controls/Control.cs

@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
@ -69,6 +70,7 @@ namespace Avalonia.Controls
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
private AutomationPeer? _automationPeer;
/// <summary>
/// Gets or sets the control's focus adorner.
@ -242,6 +244,24 @@ namespace Avalonia.Controls
}
}
protected virtual AutomationPeer OnCreateAutomationPeer()
{
return new NoneAutomationPeer(this);
}
internal AutomationPeer GetOrCreateAutomationPeer()
{
VerifyAccess();
if (_automationPeer is object)
{
return _automationPeer;
}
_automationPeer = OnCreateAutomationPeer();
return _automationPeer;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);

3
src/Avalonia.Controls/Image.cs

@ -1,3 +1,5 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
@ -33,6 +35,7 @@ namespace Avalonia.Controls
{
AffectsRender<Image>(SourceProperty, StretchProperty, StretchDirectionProperty);
AffectsMeasure<Image>(SourceProperty, StretchProperty, StretchDirectionProperty);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Image>(AutomationControlType.Image);
}
/// <summary>

6
src/Avalonia.Controls/ItemsControl.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
@ -335,6 +336,11 @@ namespace Avalonia.Controls
base.OnKeyDown(e);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ItemsControlAutomationPeer(this);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);

7
src/Avalonia.Controls/ListBoxItem.cs

@ -1,6 +1,6 @@
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
namespace Avalonia.Controls
{
@ -34,5 +34,10 @@ namespace Avalonia.Controls
get { return GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ListItemAutomationPeer(this);
}
}
}

4
src/Avalonia.Controls/Menu.cs

@ -1,3 +1,5 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -35,6 +37,8 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<Menu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Menu>(AutomationControlType.Menu);
}
/// <inheritdoc/>

6
src/Avalonia.Controls/MenuItem.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
@ -494,6 +495,11 @@ namespace Avalonia.Controls
}
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new MenuItemAutomationPeer(this);
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.UpdateDataValidation(property, value);

1
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Automation.Peers;
namespace Avalonia.Platform
{

18
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -394,10 +394,14 @@ namespace Avalonia.Controls.Presenters
var x = Math.Floor(_caretBounds.X) + 0.5;
var y = Math.Floor(_caretBounds.Y) + 0.5;
var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];
if (x >= Bounds.Width)
if (_caretBounds.X > 0 && _caretBounds.X >= textLine.WidthIncludingTrailingWhitespace)
{
x = Math.Floor(_caretBounds.X - 1) + 0.5;
x -= 1;
}
return (new Point(x, y), new Point(x, b));
@ -468,8 +472,8 @@ namespace Avalonia.Controls.Presenters
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionStart = CoerceCaretIndex(SelectionStart);
var selectionEnd = CoerceCaretIndex(SelectionEnd);
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
@ -512,11 +516,9 @@ namespace Avalonia.Controls.Presenters
_textLayout = null;
InvalidateArrange();
var scale = LayoutHelper.GetLayoutScale(this);
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale);
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1);
return new Size(measuredSize.Width, measuredSize.Height);
}

48
src/Avalonia.Controls/Primitives/AccessText.cs

@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -80,7 +81,7 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string? text)
{
return base.CreateTextLayout(constraint, StripAccessKey(text));
return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text));
}
/// <inheritdoc/>
@ -107,29 +108,40 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Returns a string with the first underscore stripped.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>The text with the first underscore stripped.</returns>
[return: NotNullIfNotNull("text")]
private string? StripAccessKey(string? text)
protected override AutomationPeer OnCreateAutomationPeer()
{
if (text is null)
{
return null;
}
var position = text.IndexOf('_');
return new NoneAutomationPeer(this);
}
if (position == -1)
internal static string? RemoveAccessKeyMarker(string? text)
{
if (!string.IsNullOrEmpty(text))
{
return text;
var accessKeyMarker = "_";
var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker;
int index = FindAccessKeyMarker(text);
if (index >= 0 && index < text.Length - 1)
text = text.Remove(index, 1);
text = text.Replace(doubleAccessKeyMarker, accessKeyMarker);
}
else
return text;
}
private static int FindAccessKeyMarker(string text)
{
var length = text.Length;
var startIndex = 0;
while (startIndex < length)
{
return text.Substring(0, position) + text.Substring(position + 1);
int index = text.IndexOf('_', startIndex);
if (index == -1)
return -1;
if (index + 1 < length && text[index + 1] != '_')
return index;
startIndex = index + 2;
}
return -1;
}
/// <summary>

18
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
@ -560,6 +561,11 @@ namespace Avalonia.Controls.Primitives
}
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupAutomationPeer(this);
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
@ -651,7 +657,17 @@ namespace Avalonia.Controls.Primitives
{
if (PlacementTarget != null)
{
FocusManager.Instance?.Focus(PlacementTarget);
var e = (IControl?)PlacementTarget;
while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible))
{
e = e.Parent;
}
if (e is object)
{
FocusManager.Instance?.Focus(e);
}
}
else
{

6
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
@ -168,5 +169,10 @@ namespace Avalonia.Controls.Primitives
UpdatePosition();
return ClientSize;
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupRootAutomationPeer(this);
}
}
}

6
src/Avalonia.Controls/Primitives/ToggleButton.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Data;
using Avalonia.Interactivity;
@ -169,6 +170,11 @@ namespace Avalonia.Controls.Primitives
RaiseEvent(e);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ToggleButtonAutomationPeer(this);
}
private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e)
{
var newValue = (bool?)e.NewValue;

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Automation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")]

8
src/Avalonia.Controls/ScrollViewer.cs

@ -1,5 +1,6 @@
using System;
using System.Reactive.Linq;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
@ -8,7 +9,7 @@ using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// A control which scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
@ -762,6 +763,11 @@ namespace Avalonia.Controls
_scrollBarExpandSubscription = SubscribeToScrollBars(e);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ScrollViewerAutomationPeer(this);
}
private IDisposable? SubscribeToScrollBars(TemplateAppliedEventArgs e)
{
static IObservable<bool>? GetExpandedObservable(ScrollBar? scrollBar)

3
src/Avalonia.Controls/Slider.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Collections;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
@ -8,6 +9,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.Automation;
namespace Avalonia.Controls
{
@ -105,6 +107,7 @@ namespace Avalonia.Controls
RoutingStrategies.Bubble);
ValueProperty.OverrideMetadata<Slider>(new DirectPropertyMetadata<double>(enableDataValidation: true));
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Slider>(AutomationControlType.Slider);
}
/// <summary>

3
src/Avalonia.Controls/TabControl.cs

@ -1,6 +1,7 @@
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -9,6 +10,7 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Avalonia.Automation;
namespace Avalonia.Controls
{
@ -68,6 +70,7 @@ namespace Avalonia.Controls
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabControl>(AutomationControlType.Tab);
}
/// <summary>

5
src/Avalonia.Controls/TabItem.cs

@ -1,3 +1,5 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
@ -31,6 +33,7 @@ namespace Avalonia.Controls
PressedMixin.Attach<TabItem>();
FocusableProperty.OverrideDefaultValue(typeof(TabItem), true);
DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabItem>(AutomationControlType.TabItem);
}
/// <summary>
@ -53,6 +56,8 @@ namespace Avalonia.Controls
set { SetValue(IsSelectedProperty, value); }
}
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
if (Header == null)

14
src/Avalonia.Controls/TextBlock.cs

@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.LogicalTree;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Layout;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -550,10 +549,8 @@ namespace Avalonia.Controls
_textLayout = null;
InvalidateArrange();
var scale = LayoutHelper.GetLayoutScale(this);
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, scale);
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1);
return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding);
}
@ -572,6 +569,11 @@ namespace Avalonia.Controls
return finalSize;
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TextBlockAutomationPeer(this);
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;

141
src/Avalonia.Controls/TextBox.cs

@ -16,6 +16,7 @@ using Avalonia.Utilities;
using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
namespace Avalonia.Controls
{
@ -256,6 +257,8 @@ namespace Avalonia.Controls
UndoRedoState state;
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
SelectionStart = SelectionEnd = value;
}
}
@ -300,14 +303,15 @@ namespace Avalonia.Controls
{
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
if (changed)
{
UpdateCommandStates();
}
if (value == SelectionEnd)
if (SelectionEnd == value && CaretIndex != value)
{
CaretIndex = SelectionStart;
CaretIndex = value;
}
}
}
@ -328,8 +332,8 @@ namespace Avalonia.Controls
{
UpdateCommandStates();
}
if (value == SelectionStart)
if (SelectionStart == value && CaretIndex != value)
{
CaretIndex = value;
}
@ -351,10 +355,12 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value);
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{
@ -457,7 +463,7 @@ namespace Avalonia.Controls
/// </summary>
public void ClearSelection()
{
SelectionStart = SelectionEnd = CaretIndex;
CaretIndex = SelectionStart;
}
/// <summary>
@ -855,6 +861,7 @@ namespace Avalonia.Controls
movement = true;
selection = false;
handled = true;
CaretIndex = _presenter.CaretIndex;
}
else if (Match(keymap.MoveCursorToTheEndOfDocument))
{
@ -862,6 +869,7 @@ namespace Avalonia.Controls
movement = true;
selection = false;
handled = true;
CaretIndex = _presenter.CaretIndex;
}
else if (Match(keymap.MoveCursorToTheStartOfLine))
{
@ -869,7 +877,7 @@ namespace Avalonia.Controls
movement = true;
selection = false;
handled = true;
CaretIndex = _presenter.CaretIndex;
}
else if (Match(keymap.MoveCursorToTheEndOfLine))
{
@ -877,6 +885,7 @@ namespace Avalonia.Controls
movement = true;
selection = false;
handled = true;
CaretIndex = _presenter.CaretIndex;
}
else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection))
{
@ -949,7 +958,7 @@ namespace Avalonia.Controls
}
else
{
SelectionStart = SelectionEnd = _presenter.CaretIndex;
CaretIndex = _presenter.CaretIndex;
}
break;
@ -971,7 +980,7 @@ namespace Avalonia.Controls
}
else
{
SelectionStart = SelectionEnd = _presenter.CaretIndex;
CaretIndex = _presenter.CaretIndex;
}
break;
@ -995,6 +1004,8 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, length) +
text.Substring(caretIndex));
CaretIndex = _presenter.CaretIndex;
}
SnapshotUndoRedo();
@ -1069,8 +1080,6 @@ namespace Avalonia.Controls
{
e.Handled = true;
}
CaretIndex = _presenter.CaretIndex;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
@ -1189,6 +1198,11 @@ namespace Avalonia.Controls
e.Pointer.Capture(null);
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TextBoxAutomationPeer(this);
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty)
@ -1234,6 +1248,7 @@ namespace Avalonia.Controls
{
var text = Text ?? string.Empty;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
if (!wholeWord)
{
@ -1242,15 +1257,32 @@ namespace Avalonia.Controls
return;
}
_presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
if (isSelecting)
{
_presenter.MoveCaretToTextPosition(selectionEnd);
_presenter.MoveCaretHorizontal(direction > 0 ?
LogicalDirection.Forward :
LogicalDirection.Backward);
SelectionEnd = _presenter.CaretIndex;
}
else
{
SelectionStart = SelectionEnd = _presenter.CaretIndex;
if (selectionStart != selectionEnd)
{
_presenter.MoveCaretToTextPosition(direction > 0 ?
Math.Max(selectionStart, selectionEnd) :
Math.Min(selectionStart, selectionEnd));
}
else
{
_presenter.MoveCaretHorizontal(direction > 0 ?
LogicalDirection.Forward :
LogicalDirection.Backward);
}
CaretIndex = _presenter.CaretIndex;
}
}
else
@ -1259,14 +1291,19 @@ namespace Avalonia.Controls
if (direction > 0)
{
offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
offset = StringUtils.NextWord(text, selectionEnd) - selectionEnd;
}
else
{
offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd;
}
SelectionEnd = CaretIndex + offset;
SelectionEnd += offset;
if (!isSelecting)
{
CaretIndex = SelectionEnd;
}
}
}
@ -1310,10 +1347,18 @@ namespace Avalonia.Controls
else
{
var textLines = _presenter.TextLayout.TextLines;
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, true);
var textLine = textLines[lineIndex];
if (caretIndex == textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength &&
lineIndex + 1 < textLines.Count)
{
textLine = textLines[++lineIndex];
}
_presenter.MoveCaretToTextPosition(textLine.TextRange.Start + textLine.TextRange.Length, true);
var textPosition = textLine.TextRange.Start + textLine.TextRange.Length - textLine.NewLineLength;
_presenter.MoveCaretToTextPosition(textPosition, true);
}
}
@ -1324,50 +1369,56 @@ namespace Avalonia.Controls
{
SelectionStart = 0;
SelectionEnd = Text?.Length ?? 0;
CaretIndex = SelectionEnd;
}
private bool DeleteSelection(bool raiseTextChanged = true)
{
if (!IsReadOnly)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
if (IsReadOnly) return true;
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
if (selectionStart != selectionEnd)
{
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text!;
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
CaretIndex = start;
ClearSelection();
return true;
}
else
{
return false;
}
}
else
if (selectionStart != selectionEnd)
{
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text!;
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
_presenter?.MoveCaretToTextPosition(start);
CaretIndex= start;
ClearSelection();
return true;
}
CaretIndex = SelectionStart;
return false;
}
private string GetSelection()
{
var text = Text;
if (string.IsNullOrEmpty(text))
{
return "";
}
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
if (start == end || (Text?.Length ?? 0) < end)
{
return "";
}
return text.Substring(start, end - start);
}
@ -1393,9 +1444,11 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace()
{
SelectionStart = CaretIndex;
var selectionStart = CaretIndex;
MoveHorizontal(-1, true, false);
SelectionStart = selectionStart;
}
private void SetSelectionForControlDelete()
@ -1407,7 +1460,7 @@ namespace Avalonia.Controls
SelectionStart = CaretIndex;
MoveHorizontal(1, true, false);
MoveHorizontal(1, true, true);
if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ')
{

21
src/Avalonia.Controls/Window.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -858,6 +859,17 @@ namespace Avalonia.Controls
private void SetWindowStartupLocation(IWindowBaseImpl? owner = null)
{
var startupLocation = WindowStartupLocation;
if (startupLocation == WindowStartupLocation.CenterOwner &&
Owner is Window ownerWindow &&
ownerWindow.WindowState == WindowState.Minimized)
{
// If startup location is CenterOwner, but owner is minimized then fall back
// to CenterScreen. This behavior is consistent with WPF.
startupLocation = WindowStartupLocation.CenterScreen;
}
var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1;
// TODO: We really need non-client size here.
@ -865,7 +877,7 @@ namespace Avalonia.Controls
PixelPoint.Origin,
PixelSize.FromSize(ClientSize, scaling));
if (WindowStartupLocation == WindowStartupLocation.CenterScreen)
if (startupLocation == WindowStartupLocation.CenterScreen)
{
var screen = Screens.ScreenFromPoint(owner?.Position ?? Position);
@ -874,7 +886,7 @@ namespace Avalonia.Controls
Position = screen.WorkingArea.CenterRect(rect).Position;
}
}
else if (WindowStartupLocation == WindowStartupLocation.CenterOwner)
else if (startupLocation == WindowStartupLocation.CenterOwner)
{
if (owner != null)
{
@ -1016,5 +1028,10 @@ namespace Avalonia.Controls
}
}
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new WindowAutomationPeer(this);
}
}
}

1
src/Avalonia.Controls/WindowBase.cs

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;

1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -1,5 +1,4 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Controls.Remote.Server;
using Avalonia.Input;

1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;

1
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Controls.Primitives.PopupPositioning;

32
src/Avalonia.Input/KeyboardDevice.cs

@ -24,30 +24,7 @@ namespace Avalonia.Input
// the source of truth about the input focus is in KeyboardDevice
private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager();
public IInputElement? FocusedElement
{
get
{
return _focusedElement;
}
private set
{
_focusedElement = value;
if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree)
{
_focusedRoot = _focusedElement.VisualRoot as IInputRoot;
}
else
{
_focusedRoot = null;
}
RaisePropertyChanged();
_textInputManager.SetFocusedElement(value);
}
}
public IInputElement? FocusedElement => _focusedElement;
private void ClearFocusWithinAncestors(IInputElement? element)
{
@ -162,8 +139,8 @@ namespace Avalonia.Input
}
SetIsFocusWithin(FocusedElement, element);
FocusedElement = element;
_focusedElement = element;
_focusedRoot = _focusedElement?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs
{
@ -178,6 +155,9 @@ namespace Avalonia.Input
NavigationMethod = method,
KeyModifiers = keyModifiers,
});
_textInputManager.SetFocusedElement(element);
RaisePropertyChanged(nameof(FocusedElement));
}
}

4
src/Avalonia.Native/Avalonia.Native.csproj

@ -17,6 +17,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />

160
src/Avalonia.Native/AvnAutomationPeer.cs

@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Native.Interop;
#nullable enable
namespace Avalonia.Native
{
internal class AvnAutomationPeer : NativeCallbackBase, IAvnAutomationPeer
{
private static readonly ConditionalWeakTable<AutomationPeer, AvnAutomationPeer> s_wrappers = new();
private readonly AutomationPeer _inner;
private AvnAutomationPeer(AutomationPeer inner)
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
if (inner is WindowBaseAutomationPeer window)
window.FocusChanged += (_, _) => Node?.FocusChanged();
}
~AvnAutomationPeer() => Node?.Dispose();
public IAvnAutomationNode? Node { get; private set; }
public IAvnString? AcceleratorKey => _inner.GetAcceleratorKey().ToAvnString();
public IAvnString? AccessKey => _inner.GetAccessKey().ToAvnString();
public AvnAutomationControlType AutomationControlType => (AvnAutomationControlType)_inner.GetAutomationControlType();
public IAvnString? AutomationId => _inner.GetAutomationId().ToAvnString();
public AvnRect BoundingRectangle => _inner.GetBoundingRectangle().ToAvnRect();
public IAvnAutomationPeerArray Children => new AvnAutomationPeerArray(_inner.GetChildren());
public IAvnString ClassName => _inner.GetClassName().ToAvnString();
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
public int IsControlElement() => _inner.IsControlElement().AsComBool();
public int IsEnabled() => _inner.IsEnabled().AsComBool();
public int IsKeyboardFocusable() => _inner.IsKeyboardFocusable().AsComBool();
public void SetFocus() => _inner.SetFocus();
public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool();
public IAvnAutomationPeer? RootPeer
{
get
{
var peer = _inner;
var parent = peer.GetParent();
while (peer is not IRootProvider && parent is not null)
{
peer = parent;
parent = peer.GetParent();
}
return Wrap(peer);
}
}
public void SetNode(IAvnAutomationNode node)
{
if (Node is not null)
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
Node = node;
}
public int IsRootProvider() => (_inner is IRootProvider).AsComBool();
public IAvnWindowBase RootProvider_GetWindow()
{
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
return ((WindowBaseImpl)window.PlatformImpl!).Native;
}
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
{
var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
// The OSX accessibility APIs expect non-ignored elements when hit-testing.
while (!result.IsControlElement())
{
var parent = result.GetParent();
if (parent is not null)
result = parent;
else
break;
}
return Wrap(result);
}
public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch
{
ExpandCollapseState.Expanded => 1,
ExpandCollapseState.PartiallyExpanded => 1,
_ => 0,
};
public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand();
public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse();
public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool();
public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke();
public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool();
public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value;
public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum;
public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum;
public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange;
public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange;
public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value);
public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool();
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
[return: NotNullIfNotNull("peer")]
public static AvnAutomationPeer? Wrap(AutomationPeer? peer)
{
return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer));
}
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray
{
private readonly AvnAutomationPeer[] _items;
public AvnAutomationPeerArray(IReadOnlyList<AutomationPeer> items)
{
_items = items.Select(x => AvnAutomationPeer.Wrap(x)).ToArray();
}
public uint Count => (uint)_items.Length;
public IAvnAutomationPeer Get(uint index) => _items[index];
}
}

48
src/Avalonia.Native/AvnString.cs

@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Avalonia.Native.Interop
{
@ -13,6 +14,53 @@ namespace Avalonia.Native.Interop
{
string[] ToStringArray();
}
internal class AvnString : NativeCallbackBase, IAvnString
{
private IntPtr _native;
private int _nativeLen;
public AvnString(string s) => String = s;
public string String { get; }
public byte[] Bytes => Encoding.UTF8.GetBytes(String);
public unsafe void* Pointer()
{
EnsureNative();
return _native.ToPointer();
}
public int Length()
{
EnsureNative();
return _nativeLen;
}
protected override void Destroyed()
{
if (_native != IntPtr.Zero)
{
Marshal.FreeHGlobal(_native);
_native = IntPtr.Zero;
}
}
private unsafe void EnsureNative()
{
if (string.IsNullOrEmpty(String))
return;
if (_native == IntPtr.Zero)
{
_nativeLen = Encoding.UTF8.GetByteCount(String);
_native = Marshal.AllocHGlobal(_nativeLen + 1);
var ptr = (byte*)_native.ToPointer();
fixed (char* chars = String)
Encoding.UTF8.GetBytes(chars, String.Length, ptr, _nativeLen);
ptr[_nativeLen] = 0;
}
}
}
}
namespace Avalonia.Native.Interop.Impl
{

11
src/Avalonia.Native/Helpers.cs

@ -1,4 +1,5 @@
using Avalonia.Native.Interop;
using JetBrains.Annotations;
namespace Avalonia.Native
{
@ -24,11 +25,21 @@ namespace Avalonia.Native
return new AvnPoint { X = pt.X, Y = pt.Y };
}
public static AvnRect ToAvnRect (this Rect rect)
{
return new AvnRect() { X = rect.X, Y= rect.Y, Height = rect.Height, Width = rect.Width };
}
public static AvnSize ToAvnSize (this Size size)
{
return new AvnSize { Height = size.Height, Width = size.Width };
}
public static IAvnString ToAvnString(this string s)
{
return s != null ? new AvnString(s) : null;
}
public static Size ToAvaloniaSize (this AvnSize size)
{
return new Size(size.Width, size.Height);

4
src/Avalonia.Native/PopupImpl.cs

@ -7,7 +7,6 @@ namespace Avalonia.Native
{
class PopupImpl : WindowBaseImpl, IPopupImpl
{
private readonly IAvaloniaNativeFactory _factory;
private readonly AvaloniaNativePlatformOptions _opts;
private readonly AvaloniaNativePlatformOpenGlInterface _glFeature;
private readonly IWindowBaseImpl _parent;
@ -15,9 +14,8 @@ namespace Avalonia.Native
public PopupImpl(IAvaloniaNativeFactory factory,
AvaloniaNativePlatformOptions opts,
AvaloniaNativePlatformOpenGlInterface glFeature,
IWindowBaseImpl parent) : base(opts, glFeature)
IWindowBaseImpl parent) : base(factory, opts, glFeature)
{
_factory = factory;
_opts = opts;
_glFeature = glFeature;
_parent = parent;

9
src/Avalonia.Native/WindowImpl.cs

@ -13,7 +13,6 @@ namespace Avalonia.Native
{
internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter
{
private readonly IAvaloniaNativeFactory _factory;
private readonly AvaloniaNativePlatformOptions _opts;
private readonly AvaloniaNativePlatformOpenGlInterface _glFeature;
IAvnWindow _native;
@ -22,9 +21,8 @@ namespace Avalonia.Native
internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts,
AvaloniaNativePlatformOpenGlInterface glFeature) : base(opts, glFeature)
AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature)
{
_factory = factory;
_opts = opts;
_glFeature = glFeature;
_doubleClickHelper = new DoubleClickHelper();
@ -87,7 +85,10 @@ namespace Avalonia.Native
_native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B });
}
public void SetTitle(string title) => _native.SetTitle(title);
public void SetTitle(string title)
{
_native.SetTitle(title ?? "");
}
public WindowState WindowState
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save