Browse Source

Merge branch 'master' into colorpicker-updates-7

pull/11264/head
robloo 3 years ago
parent
commit
7ae025933e
  1. 4
      .editorconfig
  2. 4
      .nuke/build.schema.json
  3. 1
      Avalonia.Desktop.slnf
  4. 53
      Avalonia.sln
  5. 5
      dirs.proj
  6. 13
      nukebuild/Build.cs
  7. 7
      samples/AppWithoutLifetime/App.axaml
  8. 12
      samples/AppWithoutLifetime/App.axaml.cs
  9. 21
      samples/AppWithoutLifetime/AppWithoutLifetime.csproj
  10. 13
      samples/AppWithoutLifetime/MainWindow.axaml
  11. 30
      samples/AppWithoutLifetime/MainWindow.axaml.cs
  12. 28
      samples/AppWithoutLifetime/Program.cs
  13. 9
      samples/AppWithoutLifetime/Sub.axaml
  14. 24
      samples/AppWithoutLifetime/Sub.axaml.cs
  15. 18
      samples/AppWithoutLifetime/app.manifest
  16. 4
      samples/IntegrationTestApp/MainWindow.axaml.cs
  17. 7
      src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
  18. 7
      src/Avalonia.Base/Input/FocusManager.cs
  19. 4
      src/Avalonia.Base/Input/InputElement.cs
  20. 2
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  21. 3
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  22. 2
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  23. 39
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  24. 8
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  25. 1
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  26. 14
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  27. 1
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  28. 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  29. 17
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  30. 8
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  31. 1
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  32. 2
      src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs
  33. 21
      src/Avalonia.Controls/AppBuilder.cs
  34. 13
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  35. 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  36. 11
      src/Avalonia.Controls/ComboBox.cs
  37. 4
      src/Avalonia.Controls/Control.cs
  38. 1
      src/Avalonia.Controls/DesktopApplicationExtensions.cs
  39. 10
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  40. 101
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  41. 58
      src/Avalonia.Controls/ItemsControl.cs
  42. 14
      src/Avalonia.Controls/ListBox.cs
  43. 3
      src/Avalonia.Controls/Menu.cs
  44. 18
      src/Avalonia.Controls/MenuBase.cs
  45. 18
      src/Avalonia.Controls/MenuItem.cs
  46. 7
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  47. 8
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  48. 15
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  49. 11
      src/Avalonia.Controls/Primitives/TabStrip.cs
  50. 35
      src/Avalonia.Controls/TabControl.cs
  51. 11
      src/Avalonia.Controls/TreeView.cs
  52. 8
      src/Avalonia.Controls/TreeViewItem.cs
  53. 97
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  54. 112
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  55. 5
      src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
  56. 41
      src/Avalonia.Diagnostics/Diagnostics/Controls/TopLevelGroup.cs
  57. 143
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  58. 56
      src/Avalonia.Diagnostics/Diagnostics/IDevToolsTopLevelGroup.cs
  59. 87
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
  60. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  61. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  62. 53
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  63. 2
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  64. 9
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  65. 21
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  66. 9
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  67. 6
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  68. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  69. 8
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  70. 3
      src/Browser/Avalonia.Browser/webapp/build.js
  71. 9
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  72. 7
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  73. 23
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  74. 78
      src/Browser/Avalonia.Browser/webapp/modules/sw.ts
  75. 19
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  76. 25
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs
  77. 116
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
  78. 24
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs
  79. 8
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  80. 35
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  81. 126
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  82. 47
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  83. 98
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  84. 8
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  85. 21
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  86. 61
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  87. 37
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
  88. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  89. 9
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  90. 17
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  91. 27
      src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs
  92. 99
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  93. 109
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  94. 214
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  95. 21
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  96. 50
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  97. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
  98. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  99. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  100. 4
      src/Windows/Avalonia.Win32/TrayIconImpl.cs

4
.editorconfig

@ -177,7 +177,9 @@ dotnet_diagnostic.CA1828.severity = warning
dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CACA2211:Non-constant fields should not be visible
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences

4
.nuke/build.schema.json

@ -101,10 +101,6 @@
"type": "boolean",
"description": "skip-tests"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",

1
Avalonia.Desktop.slnf

@ -3,6 +3,7 @@
"path": "Avalonia.sln",
"projects": [
"packages\\Avalonia\\Avalonia.csproj",
"samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj",
"samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
"samples\\ControlCatalog\\ControlCatalog.csproj",
"samples\\GpuInterop\\GpuInterop.csproj",

53
Avalonia.sln

@ -246,12 +246,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
@ -264,7 +263,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -597,18 +602,22 @@ Global
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -629,14 +638,26 @@ Global
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.Build.0 = Release|Any CPU
{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.Build.0 = Release|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -706,15 +727,21 @@ Global
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7}
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

5
dirs.proj

@ -9,10 +9,11 @@
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<!-- Exclude iOS, Android and Web samples from build -->
<!-- Exclude iOS, Android and Browser samples from build -->
<ProjectReference Remove="samples/*.iOS/*.csproj" />
<ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Web/*.csproj" />
<ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/*.Blazor/*.csproj" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">
<ProjectReference Remove="src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj" />

13
nukebuild/Build.cs

@ -35,8 +35,6 @@ using MicroCom.CodeGenerator;
partial class Build : NukeBuild
{
[Solution("Avalonia.sln")] readonly Solution Solution;
BuildParameters Parameters { get; set; }
protected override void OnBuildInitialized()
{
@ -143,10 +141,12 @@ partial class Build : NukeBuild
void RunCoreTest(string projectName)
{
Information($"Running tests from {projectName}");
var project = Solution.GetProject(projectName).NotNull("project != null");
var project = RootDirectory.GlobFiles(@$"**\{projectName}.csproj").FirstOrDefault()
?? throw new InvalidOperationException($"Project {projectName} doesn't exist");
// Nuke and MSBuild tools have build-in helpers to get target frameworks from the project.
// Unfortunately, it gets broken with every second SDK update, so we had to do it manually.
var fileXml = XDocument.Parse(File.ReadAllText(project.Path));
var fileXml = XDocument.Parse(File.ReadAllText(project));
var targetFrameworks = fileXml.Descendants("TargetFrameworks")
.FirstOrDefault()?.Value.Split(';').Select(f => f.Trim());
if (targetFrameworks is null)
@ -212,7 +212,8 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.Headless.UnitTests");
RunCoreTest("Avalonia.Headless.NUnit.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.UnitTests");
});
Target RunRenderTests => _ => _
@ -311,7 +312,7 @@ partial class Build : NukeBuild
public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Execute<Build>(x => x.Package)
? Execute<Build>(x => x.RunToolsTests)
: Execute<Build>(x => x.RunTests);
}

7
samples/AppWithoutLifetime/App.axaml

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

12
samples/AppWithoutLifetime/App.axaml.cs

@ -0,0 +1,12 @@
using Avalonia;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}

21
samples/AppWithoutLifetime/AppWithoutLifetime.csproj

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

13
samples/AppWithoutLifetime/MainWindow.axaml

@ -0,0 +1,13 @@
<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="AppWithoutLifetime.MainWindow"
Title="AppWithoutLifetime">
<StackPanel>
<TextBlock Text="Welcome to Avalonia!"/>
<CheckBox Content="Welcome to Avalonia!"/>
<Button Click="Open" Content="Open"/>
</StackPanel>
</Window>

30
samples/AppWithoutLifetime/MainWindow.axaml.cs

@ -0,0 +1,30 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
this.AttachDevTools();
base.OnLoaded();
}
public void Open(object sender, RoutedEventArgs e)
{
new Sub().Show(this);
}
}

28
samples/AppWithoutLifetime/Program.cs

@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls;
using System;
namespace AppWithoutLifetime;
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.
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().Start(AppMain, args);
}
private static void AppMain(Application app, string[] args)
{
app.Run(new MainWindow());
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}

9
samples/AppWithoutLifetime/Sub.axaml

@ -0,0 +1,9 @@
<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="AppWithoutLifetime.Sub"
Title="Window1">
Welcome to Avalonia Sub!
</Window>

24
samples/AppWithoutLifetime/Sub.axaml.cs

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace AppWithoutLifetime;
public partial class Sub : Window
{
public Sub()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
this.AttachDevTools();
base.OnLoaded();
}
}

18
samples/AppWithoutLifetime/app.manifest

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AppWithoutLifetime"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

4
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -202,7 +202,7 @@ namespace IntegrationTestApp
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows)
foreach (var window in lifetime.Windows.ToArray())
{
window.Activate();
}
@ -212,7 +212,7 @@ namespace IntegrationTestApp
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows)
foreach (var window in lifetime.Windows.ToArray())
{
window.Show();
if (window.WindowState == WindowState.Minimized)

7
src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs

@ -35,7 +35,12 @@ namespace Avalonia.Data.Converters
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
return string.Format(culture, Format, value);
var format = Format!;
if (!format.Contains('{'))
{
format = $"{{0:{format}}}";
}
return string.Format(culture, format, value);
}
/// <inheritdoc/>

7
src/Avalonia.Base/Input/FocusManager.cs

@ -122,6 +122,11 @@ namespace Avalonia.Input
{
scope = scope ?? throw new ArgumentNullException(nameof(scope));
if (element is not null && !CanFocus(element))
{
return;
}
if (_focusScopes.TryGetValue(scope, out var existingElement))
{
if (element != existingElement)
@ -242,6 +247,6 @@ namespace Avalonia.Input
}
}
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true;
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true;
}
}

4
src/Avalonia.Base/Input/InputElement.cs

@ -647,6 +647,10 @@ namespace Avalonia.Input
{
PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
}
else if (change.Property == IsVisibleProperty && !change.GetNewValue<bool>() && IsFocused)
{
FocusManager.Instance?.Focus(null);
}
}
/// <summary>

2
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput
}
_transformTracker.SetVisual(_client?.TextViewVisual);
UpdateCursorRect();
_im?.SetClient(_client);
UpdateCursorRect();
}
else
{

3
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -50,7 +50,8 @@ internal static class StorageProviderHelpers
}
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
[return: NotNullIfNotNull(nameof(path))]
public static string? NameWithExtension(string? path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))

2
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@ -243,7 +243,7 @@ namespace Avalonia.Rendering.Composition.Expressions
else if (ch == '-')
{
if (len != 0)
return false;
break;
len = c + 1;
}
else

39
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -729,6 +729,8 @@ namespace Avalonia.Controls
RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsTemplateChanged(e));
RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAutoGenerateColumnsChanged(e));
FocusableProperty.OverrideDefaultValue<DataGrid>(true);
}
/// <summary>
@ -2478,7 +2480,7 @@ namespace Avalonia.Controls
if (_hScrollBar != null)
{
//_hScrollBar.IsTabStop = false;
_hScrollBar.IsTabStop = false;
_hScrollBar.Maximum = 0.0;
_hScrollBar.Orientation = Orientation.Horizontal;
_hScrollBar.IsVisible = false;
@ -2494,7 +2496,7 @@ namespace Avalonia.Controls
if (_vScrollBar != null)
{
//_vScrollBar.IsTabStop = false;
_vScrollBar.IsTabStop = false;
_vScrollBar.Maximum = 0.0;
_vScrollBar.Orientation = Orientation.Vertical;
_vScrollBar.IsVisible = false;
@ -3734,7 +3736,7 @@ namespace Avalonia.Controls
if (sender is Control editingElement)
{
editingElement.LostFocus -= EditingElement_LostFocus;
if (EditingRow != null && EditingColumnIndex != -1)
if (EditingRow != null && _editingColumnIndex != -1)
{
FocusEditingCell(true);
}
@ -4039,18 +4041,22 @@ namespace Avalonia.Controls
return true;
}
Debug.Assert(EditingRow != null);
var editingRow = EditingRow;
if (editingRow is null)
{
return true;
}
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
// Cache these to see if they change later
int currentSlot = CurrentSlot;
int currentColumnIndex = CurrentColumnIndex;
// We're ready to start ending, so raise the event
DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex];
DataGridCell editingCell = editingRow.Cells[_editingColumnIndex];
var editingElement = editingCell.Content as Control;
if (editingElement == null)
{
@ -4058,7 +4064,7 @@ namespace Avalonia.Controls
}
if (raiseEvents)
{
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction);
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction);
OnCellEditEnding(e);
if (e.Cancel)
{
@ -4112,7 +4118,7 @@ namespace Avalonia.Controls
}
else
{
if (EditingRow != null)
if (editingRow != null)
{
if (editingCell.IsValid)
{
@ -4120,10 +4126,10 @@ namespace Avalonia.Controls
editingCell.UpdatePseudoClasses();
}
if (EditingRow.IsValid)
if (editingRow.IsValid)
{
EditingRow.IsValid = false;
EditingRow.UpdatePseudoClasses();
editingRow.IsValid = false;
editingRow.UpdatePseudoClasses();
}
}
@ -4169,22 +4175,22 @@ namespace Avalonia.Controls
PopulateCellContent(
isCellEdited: !exitEditingMode,
dataGridColumn: CurrentColumn,
dataGridRow: EditingRow,
dataGridRow: editingRow,
dataGridCell: editingCell);
EditingRow.InvalidateDesiredHeight();
editingRow.InvalidateDesiredHeight();
var column = editingCell.OwningColumn;
if (column.Width.IsSizeToCells || column.Width.IsAuto)
{// Invalidate desired width and force recalculation
column.SetWidthDesiredValue(0);
EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
}
}
// We're done, so raise the CellEditEnded event
if (raiseEvents)
{
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction));
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction));
}
// There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
@ -4427,8 +4433,7 @@ namespace Avalonia.Controls
dataGridCell.Focus();
success = dataGridCell.ContainsFocusedElement();
}
//TODO Check
//success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus();
_focusEditingControl = !success;
}
return success;

8
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -33,6 +33,8 @@ namespace Avalonia.Controls
{
PointerPressedEvent.AddClassHandler<DataGridCell>(
(x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true);
FocusableProperty.OverrideDefaultValue<DataGridCell>(true);
IsTabStopProperty.OverrideDefaultValue<DataGridCell>(false);
}
public DataGridCell()
{ }
@ -169,8 +171,7 @@ namespace Avalonia.Controls
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
@ -190,8 +191,7 @@ namespace Avalonia.Controls
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

1
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -72,6 +72,7 @@ namespace Avalonia.Controls
{
AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
PressedMixin.Attach<DataGridColumnHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridColumnHeader>(false);
}
/// <summary>

14
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Reflection;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -489,7 +490,7 @@ namespace Avalonia.Controls
{
DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
if (finalWidth > totalColumnsWidth)
if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon)
{
fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
}
@ -971,6 +972,12 @@ namespace Avalonia.Controls
{
cx += _negHorizontalOffset;
_horizontalOffset -= _negHorizontalOffset;
if (_horizontalOffset < LayoutHelper.LayoutEpsilon)
{
// Snap to zero to avoid trying to partially scroll in first scrolled off column below
_horizontalOffset = 0;
}
_negHorizontalOffset = 0;
}
else
@ -979,6 +986,11 @@ namespace Avalonia.Controls
_negHorizontalOffset -= displayWidth - cx;
cx = displayWidth;
}
// Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset
// since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell
// called by DataGridCellsPresenter.MeasureOverride.
HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset);
}
// second try to scroll entire columns
if (cx < displayWidth && _horizontalOffset > 0)

1
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -128,6 +128,7 @@ namespace Avalonia.Controls
DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnDetailsTemplateChanged(e));
AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e));
PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
IsTabStopProperty.OverrideDefaultValue<DataGridRow>(false);
}
/// <summary>

4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -106,6 +106,7 @@ namespace Avalonia.Controls
{
SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>((x,e) => x.OnSublevelIndentChanged(e));
PressedMixin.Attach<DataGridRowGroupHeader>();
IsTabStopProperty.OverrideDefaultValue<DataGridRowGroupHeader>(false);
}
/// <summary>
@ -301,8 +302,7 @@ namespace Avalonia.Controls
}
else
{
//if (!e.Handled && OwningGrid.IsTabStop)
if (!e.Handled)
if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}

17
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -1589,6 +1589,23 @@ namespace Avalonia.Controls
CorrectSlotsAfterDeletion(slot, isRow);
OnRemovedElement(slot, item);
// Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem
// and CurrentSlot with the currently edited cell, since OnRemovingElement called
// SetCurrentCellCore(-1, -1) to temporarily reset the current cell.
if (_temporarilyResetCurrentCell &&
_editingColumnIndex != -1 &&
_previousCurrentItem != null &&
EditingRow != null &&
EditingRow.Slot != -1)
{
ProcessSelectionAndCurrency(
columnIndex: _editingColumnIndex,
item: _previousCurrentItem,
backupSlot: this.EditingRow.Slot,
action: DataGridSelectionAction.None,
scrollIntoView: false);
}
}
private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot)

8
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -82,7 +82,6 @@
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="CellBorder"
@ -157,7 +156,6 @@
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="12,0,0,0" />
<Setter Property="FontSize" Value="12" />
@ -268,7 +266,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
@ -310,7 +307,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
<Setter Property="Template">
<ControlTemplate>
@ -408,7 +404,6 @@
</ControlTheme>
<ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
@ -433,7 +428,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Focusable="False"
IsTabStop="False"
Foreground="{TemplateBinding Foreground}" />
<StackPanel Grid.Column="3"
@ -503,6 +498,7 @@
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"

1
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -126,7 +126,6 @@
<ControlTheme x:Key="{x:Type DataGridRowHeader}"
TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"

2
src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs

@ -54,7 +54,7 @@ namespace Avalonia.Controls.Utils
/// <returns>True if the currently focused element is within the visual tree of the parent</returns>
internal static bool ContainsFocusedElement(this Visual element)
{
return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current as Visual);
return element is InputElement { IsKeyboardFocusWithin: true };
}
}
}

21
src/Avalonia.Controls/AppBuilder.cs

@ -288,17 +288,26 @@ namespace Avalonia
}
s_setupWasAlreadyCalled = true;
SetupUnsafe();
}
/// <summary>
/// Setup method that doesn't check for input initalizers being set.
/// Nor
/// </summary>
internal void SetupUnsafe()
{
_optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer();
RenderingSubsystemInitializer();
WindowingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self);
Instance = _appFactory();
RuntimePlatformServicesInitializer?.Invoke();
RenderingSubsystemInitializer?.Invoke();
WindowingSubsystemInitializer?.Invoke();
AfterPlatformServicesSetupCallback?.Invoke(Self);
Instance = _appFactory!();
Instance.ApplicationLifetime = _lifetime;
AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
AfterSetupCallback?.Invoke(Self);
Instance.OnFrameworkInitializationCompleted();
}
}

13
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
@ -16,7 +17,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
private int _exitCode;
private CancellationTokenSource? _cts;
private bool _isShuttingDown;
private readonly HashSet<Window> _windows = new();
private readonly AvaloniaList<Window> _windows = new();
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
@ -35,7 +36,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
private static void OnWindowOpened(object? sender, RoutedEventArgs e)
{
s_activeLifetime?._windows.Add((Window)sender!);
var window = (Window)sender!;
if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window))
{
s_activeLifetime._windows.Add(window);
}
}
public ClassicDesktopStyleApplicationLifetime()
@ -67,7 +72,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
public Window? MainWindow { get; set; }
/// <inheritdoc />
public IReadOnlyList<Window> Windows => _windows.ToArray();
public IReadOnlyList<Window> Windows => _windows;
private void HandleWindowClosed(Window? window)
{
@ -161,7 +166,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
foreach (var w in Windows.ToArray())
{
if (w.Owner is null)
{

2
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -15,9 +15,9 @@
<InternalsVisibleTo Include="Avalonia.Controls.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless.XUnit, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />

11
src/Avalonia.Controls/ComboBox.cs

@ -170,8 +170,15 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ComboBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ComboBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

4
src/Avalonia.Controls/Control.cs

@ -403,7 +403,9 @@ namespace Avalonia.Controls
{
if (_focusAdorner == null)
{
var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
var template = IsSet(FocusAdornerProperty)
? GetValue(FocusAdornerProperty)
: adornerLayer.DefaultFocusAdorner;
if (template != null)
{

1
src/Avalonia.Controls/DesktopApplicationExtensions.cs

@ -1,6 +1,5 @@
using System;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;

10
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -24,9 +24,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
@ -59,8 +58,8 @@ namespace Avalonia.Controls
/// </summary>
public IDataTemplate? ItemTemplate
{
get => _itemTemplate;
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
@ -82,7 +81,6 @@ namespace Avalonia.Controls
}
private Classes? _classes;
private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter()
{

101
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -10,34 +10,44 @@ namespace Avalonia.Controls.Generators
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// process should be followed:
///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
/// derived from the <see cref="Control"/> class. If this method returns true then the
/// item itself should be used as the container.
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
/// <see cref="CreateContainer"/> should be called to create a new container.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should first be called to
/// determine whether the item needs a container. This method will return true if the item
/// should be wrapped in a container control, or false if the item itself can be used as a
/// container.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> returns true then the
/// <see cref="CreateContainer"/> method should be called to create a new container, passing
/// the recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>.
/// - If the panel supports recycling and the recycle key is non-null then the recycle key
/// should be recorded for the container (e.g. in an attached property or the realized
/// container list).
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
///
/// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> in the first step above returns true
/// then the above steps should be carried out a single time; the first time the item is
/// displayed. Otherwise the steps should be carried out each time a new container is realized
/// for an item.
/// NOTE: If <see cref="NeedsContainer(object, int, out object?)"/> in the first step above
/// returns false then the above steps should be carried out a single time: the first time the
/// item is displayed. Otherwise the steps should be carried out each time a new container is
/// realized for an item.
///
/// When unrealizing a container, the following process should be followed:
///
/// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item
/// cannot be unrealized or recycled.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
/// then the item cannot be unrealized or recycled.
/// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container
/// - If recycling is supported then the container should be added to a recycle pool.
/// - It is assumed that recyclable containers will not be removed from the panel but instead
/// hidden from view using e.g. `container.IsVisible = false`.
/// - If recycling is supported by the panel and the container then the container should be
/// added to a recycle pool keyed on the recycle key returned from
/// <see cref="NeedsContainer(object, int, out object?)"/>. It is assumed that recycled
/// containers will not be removed from the panel but instead hidden from view using
/// e.g. `container.IsVisible = false`.
/// - If recycling is not supported then the container should be removed from the panel.
///
/// When recycling an unrealized container, the following process should be followed:
///
/// - An element should be taken from the recycle pool.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should be called to determine
/// whether the item needs a container, and if so, the recycle key.
/// - A container should be taken from the recycle pool keyed on the returned recycle key.
/// - The container should be made visible.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
@ -55,28 +65,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Creates a new container control.
/// Determines whether the specified item needs to be wrapped in a container control.
/// </summary>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerForItemOverride();
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
public bool NeedsContainer(object? item, int index, out object? recycleKey) =>
_owner.NeedsContainerOverride(item, index, out recycleKey);
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Creates a new container control.
/// </summary>
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// The recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>
/// </param>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// Before calling this method, <see cref="NeedsContainer(object, int, out object?)"/>
/// should be called to determine whether the item itself should be used as a container.
/// After calling this method, <see cref="PrepareItemContainer(Control, object, int)"/>
/// must be called to prepare the container to display the specified item.
///
/// If the panel supports recycling then the returned recycle key should be stored alongside
/// the container and when container becomes eligible for recycling the container should
/// be placed in a recycle pool using this key. If the returned recycle key is null then
/// the container cannot be recycled.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
public Control CreateContainer(object? item, int index, object? recycleKey)
=> _owner.CreateContainerForItemOverride(item, index, recycleKey);
/// <summary>
/// Prepares the specified element as the container for the corresponding item.
@ -85,10 +110,10 @@ namespace Avalonia.Controls.Generators
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// If <see cref="NeedsContainer(object, int, out object?)"/> is false for an
/// item, then this method must only be called a single time; otherwise this method must
/// be called after the container is created, and each subsequent time the container is
/// recycled to display a new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
@ -104,8 +129,8 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
/// must be called only a single time.
/// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
/// false then must be called only a single time.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -128,7 +153,7 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// <see cref="NeedsContainer(object, int, out object?)"/> returned false for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);

58
src/Avalonia.Controls/ItemsControl.cs

@ -307,6 +307,12 @@ namespace Avalonia.Controls
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary>
/// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports
/// a single container type.
/// </summary>
protected static object DefaultRecycleKey { get; } = new object();
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
@ -362,7 +368,10 @@ namespace Avalonia.Controls
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContentPresenter();
}
/// <summary>
/// Prepares the specified element to display the specified item.
@ -495,11 +504,52 @@ namespace Avalonia.Controls
}
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Determines whether the specified item can be its own container.
/// </summary>
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// If the item is its own container then by definition it cannot be recycled, so
/// <paramref name="recycleKey"/> shoud be set to null.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<Control>(item, out recycleKey);
}
/// <summary>
/// A default implementation of <see cref="NeedsContainerOverride(object, int, out object?)"/>
/// that returns true and sets the recycle key to <see cref="DefaultRecycleKey"/> if the item
/// is not a <typeparamref name="T"/> .
/// </summary>
/// <typeparam name="T">The container type.</typeparam>
/// <param name="item">The item.</param>
/// <param name="recycleKey">
/// When the method returns, contains <see cref="DefaultRecycleKey"/> if
/// <paramref name="item"/> is not of type <typeparamref name="T"/>; otherwise null.
/// </param>
/// <returns>
/// true if <paramref name="item"/> is of type <typeparamref name="T"/>; otherwise false.
/// </returns>
protected bool NeedsContainer<T>(object? item, out object? recycleKey) where T : Control
{
if (item is T)
{
recycleKey = null;
return false;
}
else
{
recycleKey = DefaultRecycleKey;
return true;
}
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

14
src/Avalonia.Controls/ListBox.cs

@ -58,6 +58,9 @@ namespace Avalonia.Controls
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ListBox),
KeyboardNavigationMode.Once);
}
/// <summary>
@ -108,8 +111,15 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ListBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ListBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

3
src/Avalonia.Controls/Menu.cs

@ -35,6 +35,9 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(Menu),
KeyboardNavigationMode.Once);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<Menu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Menu>(AutomationControlType.Menu);
}

18
src/Avalonia.Controls/MenuBase.cs

@ -133,8 +133,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

18
src/Avalonia.Controls/MenuItem.cs

@ -339,8 +339,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{

7
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -52,13 +52,6 @@ namespace Avalonia.Controls.Presenters
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
static ItemsPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
}
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;

8
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
if (generator.NeedsContainer(item, index, out var recycleKey))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
container = generator.CreateContainer(item, index, recycleKey);
}
else
{
container = generator.CreateContainer();
container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
}
generator.PrepareItemContainer(container, item, index);

15
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -5,6 +5,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Xml.Linq;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data;
@ -528,13 +529,6 @@ namespace Avalonia.Controls.Primitives
protected internal override void ClearContainerForItemOverride(Control element)
{
base.ClearContainerForItemOverride(element);
if (Presenter?.Panel is InputElement panel &&
KeyboardNavigation.GetTabOnceActiveElement(panel) == element)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
}
element.ClearValue(IsSelectedProperty);
}
@ -834,12 +828,6 @@ namespace Avalonia.Controls.Primitives
Selection.Clear();
Selection.Select(index);
}
if (Presenter?.Panel is { } panel)
{
var container = ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(panel, container);
}
}
/// <summary>
@ -928,6 +916,7 @@ namespace Avalonia.Controls.Primitives
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{
_hasScrolledToSelectedItem = false;
KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(Selection.AnchorIndex));
AutoScrollToSelectedItemIfNecessary();
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)

11
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected internal override Control CreateContainerForItemOverride() => new TabStripItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabStripItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabStripItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

35
src/Avalonia.Controls/TabControl.cs

@ -148,8 +148,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabItem>(item, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
@ -218,6 +225,20 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart?.ApplyTemplate();
// Set TabNavigation to Once on the panel if not already set and
// forward the TabOnceActiveElement to the panel.
if (ItemsPresenterPart?.Panel is { } panel)
{
if (!panel.IsSet(KeyboardNavigation.TabNavigationProperty))
panel.SetCurrentValue(
KeyboardNavigation.TabNavigationProperty,
KeyboardNavigationMode.Once);
KeyboardNavigation.SetTabOnceActiveElement(
panel,
KeyboardNavigation.GetTabOnceActiveElement(this));
}
}
/// <inheritdoc/>
@ -261,7 +282,17 @@ namespace Avalonia.Controls
base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty)
{
RefreshContainers();
}
else if (change.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
ItemsPresenterPart?.Panel is { } panel)
{
// Forward TabOnceActiveElement to the panel.
KeyboardNavigation.SetTabOnceActiveElement(
panel,
change.GetNewValue<IInputElement?>());
}
}
}
}

11
src/Avalonia.Controls/TreeView.cs

@ -486,8 +486,15 @@ namespace Avalonia.Controls
return (false, null);
}
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TreeViewItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TreeViewItem>(item, out recycleKey);
}
protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
{

8
src/Avalonia.Controls/TreeViewItem.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
internal TreeView? TreeViewOwner => _treeView;
protected internal override Control CreateContainerForItemOverride()
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return EnsureTreeView().CreateContainerForItemOverride();
return EnsureTreeView().CreateContainerForItemOverride(item, index, recycleKey);
}
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return EnsureTreeView().IsItemItsOwnContainerOverride(item);
return EnsureTreeView().NeedsContainerOverride(item, index, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)

97
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -15,13 +15,14 @@ namespace Avalonia.Controls
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly object s_itemIsItsOwnContainer = new object();
private Size _extent;
private Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
@ -172,7 +173,7 @@ namespace Avalonia.Controls
return null;
if (index == _realizedIndex)
return _realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -246,10 +247,27 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
return GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
private Control? GetRealizedElement(int index)
@ -257,44 +275,37 @@ namespace Avalonia.Controls
return _realizedIndex == index ? _realized : null;
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var item = items[index];
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (item is Control controlItem)
if (!controlItem.IsSet(RecycleKeyProperty))
{
var generator = ItemContainerGenerator;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -304,14 +315,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -323,7 +334,10 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -331,7 +345,14 @@ namespace Avalonia.Controls
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
element.IsVisible = false;
}
}

112
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -52,10 +52,11 @@ namespace Avalonia.Controls
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private static readonly object s_itemIsItsOwnContainer = new object();
private readonly Action<Control, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex;
@ -68,7 +69,7 @@ namespace Avalonia.Controls
private RealizedStackElements? _realizedElements;
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
@ -331,7 +332,7 @@ namespace Avalonia.Controls
return null;
if (_realizedElements?.GetElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -492,8 +493,7 @@ namespace Avalonia.Controls
c = c?.GetVisualParent();
}
return viewport;
return viewport.Intersect(new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity));
}
private void RealizeElements(
@ -561,10 +561,26 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
var e = GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
@ -575,38 +591,33 @@ namespace Avalonia.Controls
return _realizedElements?.GetElement(index);
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
var item = items[index];
Debug.Assert(ItemContainerGenerator is not null);
if (item is Control controlItem)
{
var generator = ItemContainerGenerator!;
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
if (!controlItem.IsSet(RecycleKeyProperty))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
var item = items[index];
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{
@ -617,9 +628,9 @@ namespace Avalonia.Controls
return element;
}
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -629,14 +640,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!;
var item = items[index];
var container = generator.CreateContainer();
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -650,7 +661,10 @@ namespace Avalonia.Controls
_scrollViewer?.UnregisterAnchorCandidate(element);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -663,8 +677,7 @@ namespace Avalonia.Controls
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
@ -673,19 +686,34 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
RemoveInternalChild(element);
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
private void PushToRecyclePool(object recycleKey, Control element)
{
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
}
private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
{
Debug.Assert(ItemContainerGenerator is not null);

5
src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs

@ -5,7 +5,7 @@ using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Diagnostics.Controls
{
class Application : AvaloniaObject
internal class Application : TopLevelGroup
, Input.ICloseable, IDisposable
{
@ -16,7 +16,8 @@ namespace Avalonia.Diagnostics.Controls
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
public Application(Avalonia.Application application)
public Application(ClassicDesktopStyleApplicationLifetimeTopLevelGroup group, Avalonia.Application application)
: base(group)
{
_application = application;

41
src/Avalonia.Diagnostics/Diagnostics/Controls/TopLevelGroup.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Controls;
namespace Avalonia.Diagnostics.Controls;
internal class TopLevelGroup : AvaloniaObject
{
public TopLevelGroup(IDevToolsTopLevelGroup group)
{
Group = group;
if (Group.Items is INotifyCollectionChanged incc)
{
incc.CollectionChanged += (_, args) =>
{
if (args.OldItems is not null)
{
foreach (TopLevel oldItem in args.OldItems)
{
Removed?.Invoke(this, oldItem);
}
}
if (args.NewItems is not null)
{
foreach (TopLevel newItem in args.NewItems)
{
Added?.Invoke(this, newItem);
}
}
};
}
}
public IDevToolsTopLevelGroup Group { get; }
public IReadOnlyList<TopLevel> Items => Group.Items;
public event EventHandler<TopLevel>? Added;
public event EventHandler<TopLevel>? Removed;
}

143
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
@ -9,12 +11,9 @@ using Avalonia.Reactive;
namespace Avalonia.Diagnostics
{
public static class DevTools
internal static class DevTools
{
private static readonly Dictionary<AvaloniaObject, MainWindow> s_open =
new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
private static readonly Dictionary<IDevToolsTopLevelGroup, MainWindow> s_open = new();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
@ -26,11 +25,6 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options)
{
if (s_attachedToApplication == true)
{
throw new ArgumentException("DevTools already attached to application", nameof(root));
}
void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (options.Gesture.Matches(e))
@ -39,109 +33,116 @@ namespace Avalonia.Diagnostics
}
}
return root.AddDisposableHandler(
return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler(
InputElement.KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);
}
public static IDisposable Open(TopLevel root) =>
Open(Application.Current,new DevToolsOptions(),root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(new SingleViewTopLevelGroup(root), options, root as Window, null);
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
if (window.Root is Controls.Application host)
{
s_open.Remove(host.Instance);
}
else
{
s_open.Remove(window.Root!);
}
}
internal static IDisposable Open(IDevToolsTopLevelGroup group, DevToolsOptions options) =>
Open(group, options, null, null);
internal static IDisposable Attach(Application? application, DevToolsOptions options, Window? owner = null)
internal static IDisposable Attach(Application application, DevToolsOptions options)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
var openedDisposable = new SerialDisposableValue();
var result = new CompositeDisposable(2);
result.Add(openedDisposable);
// Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode
&& !s_attachedToApplication)
if (!Design.IsDesignMode)
{
var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime;
as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime;
if (lifeTime is null)
{
throw new ArgumentNullException(nameof(Application.ApplicationLifetime));
throw new ArgumentNullException(nameof(application), "DevTools can only attach to applications that support IClassicDesktopStyleApplicationLifetime.");
}
if (application.InputManager is { })
if (application.InputManager is not null)
{
s_attachedToApplication = true;
result.Add(application.InputManager.PreProcess.Subscribe(e =>
{
var owner = lifeTime.MainWindow;
if (e is RawKeyEventArgs keyEventArgs
&& keyEventArgs.Type == RawKeyEventType.KeyUp
&& options.Gesture.Matches(keyEventArgs))
{
openedDisposable.Disposable = Open(application, options, owner);
openedDisposable.Disposable =
Open(new ClassicDesktopStyleApplicationLifetimeTopLevelGroup(lifeTime), options,
owner, application);
e.Handled = true;
}
}));
}
}
return result;
}
private static IDisposable Open(Application? application, DevToolsOptions options, Window? owner = default)
private static IDisposable Open(IDevToolsTopLevelGroup topLevelGroup, DevToolsOptions options,
Window? owner, Application? app)
{
var focussedControl = KeyboardDevice.Instance?.FocusedElement as Control;
if (application is null)
AvaloniaObject root = topLevelGroup switch
{
throw new ArgumentNullException(nameof(application));
}
if (s_open.TryGetValue(application, out var window))
ClassicDesktopStyleApplicationLifetimeTopLevelGroup gr => new Controls.Application(gr, app ?? Application.Current!),
SingleViewTopLevelGroup gr => gr.Items.First(),
_ => new Controls.TopLevelGroup(topLevelGroup)
};
// If single static toplevel is already visible in another devtools window, focus it.
if (s_open.TryGetValue(topLevelGroup, out var mainWindow))
{
window.Activate();
window.SelectedControl(focussedControl);
mainWindow.Activate();
mainWindow.SelectedControl(focussedControl);
return Disposable.Empty;
}
else
if (topLevelGroup.Items.Count == 1 && topLevelGroup.Items is not INotifyCollectionChanged)
{
window = new MainWindow
{
Root = new Controls.Application(application),
Width = options.Size.Width,
Height = options.Size.Height,
};
window.SetOptions(options);
window.SelectedControl(focussedControl);
window.Closed += DevToolsClosed;
s_open.Add(application, window);
if (options.ShowAsChildWindow && owner is { })
var singleTopLevel = topLevelGroup.Items.First();
foreach (var group in s_open)
{
window.Show(owner);
}
else
{
window.Show();
if (group.Key.Items.Contains(singleTopLevel))
{
group.Value.Activate();
group.Value.SelectedControl(focussedControl);
return Disposable.Empty;
}
}
}
return Disposable.Create(() => window?.Close());
var window = new MainWindow
{
Root = root,
Width = options.Size.Width,
Height = options.Size.Height,
Tag = topLevelGroup
};
window.SetOptions(options);
window.SelectedControl(focussedControl);
window.Closed += DevToolsClosed;
s_open.Add(topLevelGroup, window);
if (options.ShowAsChildWindow && owner is not null)
{
window.Show(owner);
}
else
{
window.Show();
}
return Disposable.Create(() => window.Close());
}
private static void DevToolsClosed(object? sender, EventArgs e)
{
var window = (MainWindow)sender!;
window.Closed -= DevToolsClosed;
s_open.Remove((IDevToolsTopLevelGroup)window.Tag!);
}
}
}

56
src/Avalonia.Diagnostics/Diagnostics/IDevToolsTopLevelGroup.cs

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Diagnostics;
internal interface IDevToolsTopLevelGroup
{
IReadOnlyList<TopLevel> Items { get; }
}
internal class ClassicDesktopStyleApplicationLifetimeTopLevelGroup : IDevToolsTopLevelGroup
{
private readonly IClassicDesktopStyleApplicationLifetime _lifetime;
public ClassicDesktopStyleApplicationLifetimeTopLevelGroup(IClassicDesktopStyleApplicationLifetime lifetime)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
}
public IReadOnlyList<TopLevel> Items => _lifetime.Windows;
public override int GetHashCode()
{
return _lifetime.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is ClassicDesktopStyleApplicationLifetimeTopLevelGroup g && g._lifetime == _lifetime;
}
}
internal class SingleViewTopLevelGroup : IDevToolsTopLevelGroup
{
private readonly TopLevel _topLevel;
public SingleViewTopLevelGroup(TopLevel topLevel)
{
_topLevel = topLevel;
Items = new[] { topLevel ?? throw new ArgumentNullException(nameof(topLevel)) };
}
public IReadOnlyList<TopLevel> Items { get; }
public override int GetHashCode()
{
return _topLevel.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is SingleViewTopLevelGroup g && g._topLevel == _topLevel;
}
}

87
src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs

@ -16,7 +16,7 @@ namespace Avalonia.Diagnostics.ViewModels
Children = avaloniaObject switch
{
ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host),
Controls.TopLevelGroup host => new TopLevelGroupHostLogical(this, host),
_ => TreeNodeCollection.Empty
};
}
@ -55,70 +55,55 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
internal class ApplicationHostLogical : TreeNodeCollection
internal class TopLevelGroupHostLogical : TreeNodeCollection
{
readonly Controls.Application _application;
CompositeDisposable _subscriptions = new CompositeDisposable(2);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
private readonly Controls.TopLevelGroup _group;
private readonly CompositeDisposable _subscriptions = new(1);
public TopLevelGroupHostLogical(TreeNode owner, Controls.TopLevelGroup host) :
base(owner)
{
_application = host;
_group = host;
}
protected override void Initialize(AvaloniaList<TreeNode> nodes)
{
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single &&
single.MainView is not null)
for (var i = 0; i < _group.Items.Count; i++)
{
nodes.Add(new LogicalTreeNode(single.MainView, Owner));
var window = _group.Items[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
}
if (_application.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime classic)
void GroupOnAdded(object? sender, TopLevel e)
{
for (int i = 0; i < classic.Windows.Count; i++)
if (e is Views.MainWindow)
{
var window = classic.Windows[i];
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
return;
}
_subscriptions = new CompositeDisposable(2)
nodes.Add(new LogicalTreeNode(e, Owner));
}
void GroupOnRemoved(object? sender, TopLevel e)
{
if (e is Views.MainWindow)
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
nodes.Add(new LogicalTreeNode((AvaloniaObject)s!,Owner));
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (s,e)=>
{
if (s is Views.MainWindow)
{
return;
}
var item = nodes.FirstOrDefault(node=>object.ReferenceEquals(node.Visual,s));
if(!(item is null))
{
nodes.Remove(item);
}
if(nodes.Count == 0)
{
if (Avalonia.Application.Current?.ApplicationLifetime is Lifetimes.IControlledApplicationLifetime controller)
{
controller.Shutdown();
}
else
{
Environment.Exit(0);
}
}
}),
};
return;
}
nodes.Add(new LogicalTreeNode(e, Owner));
}
_group.Added += GroupOnAdded;
_group.Removed += GroupOnRemoved;
_subscriptions.Add(new Disposable.AnonymousDisposable(() =>
{
_group.Added -= GroupOnAdded;
_group.Removed -= GroupOnRemoved;
}));
}
public override void Dispose()

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -237,9 +237,9 @@ namespace Avalonia.Diagnostics.Views
else
{
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
if (_frozenPopupStates.ContainsKey(popup))
if (_frozenPopupStates.TryGetValue(popup, out var value))
{
_frozenPopupStates[popup].Dispose();
value.Dispose();
_frozenPopupStates.Remove(popup);
}
}

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.6" PrivateAssets="All" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.7" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

53
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -1,8 +1,8 @@
using System;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop
@ -22,39 +22,47 @@ namespace Avalonia.FreeDesktop
_settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValueAsync();
_ = TryGetInitialValuesAsync();
}
public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues();
private async Task TryGetInitialValueAsync()
private async Task TryGetInitialValuesAsync()
{
_themeVariant = await TryGetThemeVariantAsync();
_accentColor = await TryGetAccentColorAsync();
_lastColorValues = BuildPlatformColorValues();
if (_lastColorValues is not null)
OnColorValuesChanged(_lastColorValues);
}
private async Task<PlatformThemeVariant?> TryGetThemeVariantAsync()
{
try
{
var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
_themeVariant = ReadAsColorScheme(value);
return ToColorScheme(((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value);
}
catch (Exception ex)
catch (DBusException)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex);
return null;
}
}
private async Task<Color?> TryGetAccentColorAsync()
{
try
{
var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor");
_accentColor = ReadAsAccentColor(value);
return ToAccentColor(((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value);
}
catch (Exception ex)
catch (DBusException)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex);
return null;
}
_lastColorValues = BuildPlatformColorValues();
if (_lastColorValues is not null)
OnColorValuesChanged(_lastColorValues);
}
private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
private async void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
{
if (exception is not null)
return;
@ -62,12 +70,8 @@ namespace Avalonia.FreeDesktop
switch (valueTuple)
{
case ("org.freedesktop.appearance", "color-scheme", { } colorScheme):
_themeVariant = ReadAsColorScheme(colorScheme);
_lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!);
break;
case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor):
_accentColor = ReadAsAccentColor(accentColor);
_themeVariant = ToColorScheme((colorScheme.Value as DBusUInt32Item)!.Value);
_accentColor = await TryGetAccentColorAsync();
_lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!);
break;
@ -85,21 +89,20 @@ namespace Avalonia.FreeDesktop
return null;
}
private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value)
private static PlatformThemeVariant ToColorScheme(uint value)
{
/*
<member>0: No preference</member>
<member>1: Prefer dark appearance</member>
<member>2: Prefer light appearance</member>
*/
var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1;
var isDark = value == 1;
return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light;
}
private static Color ReadAsAccentColor(DBusVariantItem value)
private static Color ToAccentColor(string value)
{
var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value;
var rgb = colorStr.Split(',');
var rgb = value.Split(',');
return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2]));
}
}

2
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -21,7 +21,7 @@ namespace Avalonia.FreeDesktop
var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
try
{
await dbusFileChooser.GetVersionAsync();
await dbusFileChooser.GetVersionPropertyAsync();
}
catch
{

9
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -715,7 +715,8 @@ namespace Metsys.Bson
public MagicProperty FindProperty(string name)
{
return _properties.ContainsKey(name) ? _properties[name] : null;
_properties.TryGetValue(name, out var property);
return property;
}
public static TypeHelper GetHelperForType(Type type)
@ -1196,7 +1197,9 @@ namespace Metsys.Bson
}
object container = null;
var property = typeHelper.FindProperty(name);
var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object);
var propertyType = property?.Type
?? (_typeMap.TryGetValue(storageType, out var type1) ? type1 : null)
?? typeof(object);
if (property != null && property.Setter == null)
{
container = property.Getter(instance);
@ -1588,7 +1591,7 @@ namespace Metsys.Bson.Configuration
{
return property;
}
return map.ContainsKey(property) ? map[property] : property;
return map.TryGetValue(property, out var value) ? value : property;
}
public void AddIgnore<T>(string name)

21
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -11,6 +11,27 @@ public class BrowserPlatformOptions
/// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
/// <summary>
/// Defines if the service worker used by Avalonia should be registered.
/// If registered, service worker can work as a save file picker fallback on the browsers that don't support native implementation.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool RegisterAvaloniaServiceWorker { get; set; }
/// <summary>
/// If <see cref="RegisterAvaloniaServiceWorker"/> is enabled, it is possible to redefine scope for the worker.
/// By default, current domain root is used as a scope.
/// </summary>
public string? AvaloniaServiceWorkerScope { get; set; }
/// <summary>
/// Avalonia uses "native-file-system-adapter" polyfill for the file dialogs.
/// If native implementation is available, by default it is used.
/// This property forces polyfill to be always used.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool PreferFileDialogPolyfill { get; set; }
}
public static class BrowserAppBuilder

9
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -25,6 +25,15 @@ internal static partial class AvaloniaModule
public static Task ImportStorage() => s_importStorage.Value;
public static string ResolveServiceWorkerPath()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return options.FrameworkAssetPathResolver!("sw.js");
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
[JSImport("registerServiceWorker", AvaloniaModule.MainModuleName)]
public static partial void RegisterServiceWorker(string path, string? scope);
}

6
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -9,15 +9,15 @@ internal static partial class StorageHelper
public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn, bool preferPolyfill);
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageItem.createWellKnownDirectory", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateWellKnownDirectory(string wellKnownDirectory);

16
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -6,20 +6,22 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Browser.Storage;
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
internal class BrowserStorageProvider : IStorageProvider
{
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanSave => true;
public bool CanPickFolder => true;
private bool PreferPolyfill =>
AvaloniaLocator.Current.GetService<BrowserPlatformOptions>()?.PreferFileDialogPolyfill ?? false;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await AvaloniaModule.ImportStorage();
@ -29,7 +31,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll);
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll, PreferPolyfill);
if (items is null)
{
return Array.Empty<IStorageFile>();
@ -63,7 +65,9 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll);
var suggestedName =
StorageProviderHelpers.NameWithExtension(options.SuggestedFileName, options.DefaultExtension, null);
var item = await StorageHelper.SaveFileDialog(startIn, suggestedName, types, excludeAll, PreferPolyfill);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
@ -89,7 +93,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SelectFolderDialog(startIn);
var item = await StorageHelper.SelectFolderDialog(startIn, PreferPolyfill);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))

8
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,5 +1,6 @@
using System;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -46,6 +47,13 @@ namespace Avalonia.Browser
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)
{
var swPath = AvaloniaModule.ResolveServiceWorkerPath();
AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

3
src/Browser/Avalonia.Browser/webapp/build.js

@ -1,7 +1,8 @@
require("esbuild").build({
entryPoints: [
"./modules/avalonia.ts",
"./modules/storage.ts"
"./modules/storage.ts",
"./modules/sw.ts"
],
outdir: "../wwwroot",
bundle: true,

9
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -7,6 +7,12 @@ import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
await globalThis.navigator.serviceWorker.register(path, scope ? { scope } : undefined);
}
}
export {
Caniuse,
Canvas,
@ -17,5 +23,6 @@ export {
StreamHelper,
NativeControlHost,
NavigationHelper,
GeneralHelpers
GeneralHelpers,
registerServiceWorker
};

7
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@ -18,12 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
const data = {
type: "write",
data: array
};
return await stream.write(data);
return await stream.write(array);
}
public static byteLength(stream: Blob) {

23
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -1,6 +1,6 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
import { showOpenFilePicker, showDirectoryPicker, showSaveFilePicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
@ -12,10 +12,12 @@ declare global {
export class StorageProvider {
public static async selectFolderDialog(
startIn: StorageItem | null): Promise<StorageItem> {
startIn: StorageItem | null,
preferPolyfill: boolean): Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
_preferPolyfill: preferPolyfill
};
const handle = await showDirectoryPicker(options as any);
@ -24,12 +26,14 @@ export class StorageProvider {
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItems> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
const handles = await showOpenFilePicker(options);
@ -38,16 +42,17 @@ export class StorageProvider {
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItem> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
const handle = await showSaveFilePicker(options);
return StorageItem.createFromHandle(handle);
}

78
src/Browser/Avalonia.Browser/webapp/modules/sw.ts

@ -0,0 +1,78 @@
const WRITE = 0;
const PULL = 0;
const ERROR = 1;
const ABORT = 1;
const CLOSE = 2;
class MessagePortSource implements UnderlyingSource {
private controller?: ReadableStreamController<any>;
constructor (private readonly port: MessagePort) {
this.port.onmessage = evt => this.onMessage(evt.data);
}
start (controller: ReadableStreamController<any>) {
this.controller = controller;
}
cancel (reason: Error) {
// Firefox can notify a cancel event, chrome can't
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
this.port.postMessage({ type: ERROR, reason: reason.message });
this.port.close();
}
onMessage (message: { type: number; chunk: Uint8Array; reason: any }) {
// enqueue() will call pull() if needed when there's no backpressure
if (!this.controller) {
return;
}
if (message.type === WRITE) {
this.controller.enqueue(message.chunk);
this.port.postMessage({ type: PULL });
}
if (message.type === ABORT) {
this.controller.error(message.reason);
this.port.close();
}
if (message.type === CLOSE) {
this.controller.close();
this.port.close();
}
}
}
self.addEventListener("install", () => {
(self as any).skipWaiting();
});
self.addEventListener("activate", event /* ExtendableEvent */ => {
(event as any).waitUntil((self as any).clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
globalThis.addEventListener("message", evt => {
const data = evt.data;
if (data.url && data.readablePort) {
data.rs = new ReadableStream(
new MessagePortSource(evt.data.readablePort),
new CountQueuingStrategy({ highWaterMark: 4 })
);
map.set(data.url, data);
}
});
globalThis.addEventListener("fetch", evt => {
const url = (evt as any).request.url;
const data = map.get(url);
if (!data) return null;
map.delete(url);
(evt as any).respondWith(new Response(data.rs, {
headers: data.headers
}));
});
export {};

19
src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
</PropertyGroup>
<ItemGroup>
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="NUnit" Version="3.13.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
</Project>

25
src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs

@ -0,0 +1,25 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal.Commands;
namespace Avalonia.Headless.NUnit;
/// <summary>
/// Identifies a nunit test that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown
{
public TestCommand Wrap(TestCommand command)
{
var session =
HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly);
return AvaloniaTestMethodCommand.ProcessCommand(session, command);
}
}

116
src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Threading;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using NUnit.Framework.Internal.Commands;
namespace Avalonia.Headless.NUnit;
internal class AvaloniaTestMethodCommand : TestCommand
{
private readonly HeadlessUnitTestSession _session;
private readonly TestCommand _innerCommand;
private readonly List<Action> _beforeTest;
private readonly List<Action> _afterTest;
// There are multiple problems with NUnit integration at the moment when we wrote this integration.
// NUnit doesn't have extensibility API for running on custom dispatcher/sync-context.
// See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774
// To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands.
// Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit.
// Also, we need to push BeforeTest/AfterTest callbacks to the very same session call.
// I hope there will be a better solution without reflection, but for now that's it.
private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand)
.GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static FieldInfo s_beforeTest = typeof(BeforeAndAfterTestCommand)
.GetField("BeforeTest", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand)
.GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!;
private AvaloniaTestMethodCommand(
HeadlessUnitTestSession session,
TestCommand innerCommand,
List<Action> beforeTest,
List<Action> afterTest)
: base(innerCommand.Test)
{
_session = session;
_innerCommand = innerCommand;
_beforeTest = beforeTest;
_afterTest = afterTest;
}
public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command)
{
return ProcessCommand(session, command, new List<Action>(), new List<Action>());
}
private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List<Action> before, List<Action> after)
{
if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand)
{
if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest)
{
Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c));
s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
}
if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest)
{
Action<TestExecutionContext> afterAction = c => after.Add(() => afterTest(c));
s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction);
}
}
if (command is DelegatingTestCommand delegatingTestCommand
&& s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner)
{
s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner, before, after));
}
else if (command is TestMethodCommand methodCommand)
{
return new AvaloniaTestMethodCommand(session, methodCommand, before, after);
}
return command;
}
public override TestResult Execute(TestExecutionContext context)
{
return _session.Dispatch(() => ExecuteTestMethod(context), default).GetAwaiter().GetResult();
}
// Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work.
private async Task<TestResult> ExecuteTestMethod(TestExecutionContext context)
{
_beforeTest.ForEach(a => a());
var testMethod = _innerCommand.Test.Method;
var methodInfo = testMethod!.MethodInfo;
var result = methodInfo.Invoke(context.TestObject, _innerCommand.Test.Arguments);
// Only Task, non generic ValueTask are supported in async context. No ValueTask<> nor F# tasks.
if (result is Task task)
{
await task;
}
else if (result is ValueTask valueTask)
{
await valueTask;
}
context.CurrentResult.SetResult(ResultState.Success);
if (context.CurrentResult.AssertionResults.Count > 0)
context.CurrentResult.RecordTestCompletion();
if (context.ExecutionStatus != TestExecutionStatus.AbortRequested)
{
_afterTest.ForEach(a => a());
}
return context.CurrentResult;
}
}

24
src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs

@ -0,0 +1,24 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal.Commands;
namespace Avalonia.Headless.NUnit;
/// <summary>
/// Identifies a nunit theory that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class AvaloniaTheoryAttribute : TheoryAttribute, IWrapSetUpTearDown
{
public TestCommand Wrap(TestCommand command)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly);
return AvaloniaTestMethodCommand.ProcessCommand(session, command);
}
}

8
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<IsTestProject>false</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.core" Version="2.4.2" />
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="xunit.core" Version="2.4.0" />
</ItemGroup>
<ItemGroup>

35
src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs

@ -0,0 +1,35 @@
using System;
using System.ComponentModel;
using System.Threading;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
/// <summary>
/// Identifies an xunit test that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer", "Avalonia.Headless.XUnit")]
public sealed class AvaloniaFactAttribute : FactAttribute
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaUIFactDiscoverer : FactDiscoverer
{
private readonly IMessageSink diagnosticMessageSink;
public AvaloniaUIFactDiscoverer(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
this.diagnosticMessageSink = diagnosticMessageSink;
}
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
{
return new AvaloniaTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod);
}
}

126
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner
{
private HeadlessUnitTestSession? _session;
public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
executionMessageSink, executionOptions)
{
}
protected override void SetupSyncContext(int maxParallelThreads)
{
_session = HeadlessUnitTestSession.GetOrStartForAssembly(
Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name)));
base.SetupSyncContext(1);
}
public override void Dispose()
{
_session?.Dispose();
base.Dispose();
}
protected override Task<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
{
return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus,
TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner
{
private readonly HeadlessUnitTestSession _session;
public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session,
ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection,
testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
_session = session;
}
protected override Task<RunSummary> RunTestClassAsync(
ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> testCases)
{
return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus,
TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource,
CollectionFixtureMappings).RunAsync();
}
}
private class AvaloniaTestClassRunner : XunitTestClassRunner
{
private readonly HeadlessUnitTestSession _session;
public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) :
base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
cancellationTokenSource, collectionFixtureMappings)
{
_session = session;
}
protected override Task<RunSummary> RunTestMethodAsync(
ITestMethod testMethod,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> testCases,
object[] constructorArguments)
{
return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink,
MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource,
constructorArguments).RunAsync();
}
}
private class AvaloniaTestMethodRunner : XunitTestMethodRunner
{
private readonly HeadlessUnitTestSession _session;
private readonly IMessageBus _messageBus;
private readonly ExceptionAggregator _aggregator;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly object[] _constructorArguments;
public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod,
IReflectionTypeInfo @class,
IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink,
IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource,
object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink,
messageBus, aggregator, cancellationTokenSource, constructorArguments)
{
_session = session;
_messageBus = messageBus;
_aggregator = aggregator;
_cancellationTokenSource = cancellationTokenSource;
_constructorArguments = constructorArguments;
}
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
{
return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason,
_constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator,
_cancellationTokenSource);
}
}
}

47
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs

@ -0,0 +1,47 @@
using System;
using System.ComponentModel;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCase : XunitTestCase
{
public AvaloniaTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay defaultMethodDisplay,
ITestMethod testMethod,
object?[]? testMethodArguments = null)
: base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments)
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public AvaloniaTestCase()
{
}
public override Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
IMessageBus messageBus,
object[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
// We need to block the XUnit thread to ensure its concurrency throttle is effective.
// See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
var runSummary = AvaloniaTestCaseRunner
.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource)
.GetAwaiter().GetResult();
return Task.FromResult(runSummary);
}
}

98
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
{
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestCaseRunner(
Action? onAfterTestInvoked,
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments,
testMethodArguments, messageBus, aggregator, cancellationTokenSource)
{
_onAfterTestInvoked = onAfterTestInvoked;
}
public static Task<RunSummary> RunTest(HeadlessUnitTestSession session,
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var afterTest = () => Dispatcher.UIThread.RunJobs();
return session.Dispatch(async () =>
{
var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName,
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource);
return await runner.RunAsync();
}, cancellationTokenSource.Token);
}
protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass,
object[] constructorArguments,
MethodInfo testMethod, object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{
return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
}
private class AvaloniaTestRunner : XunitTestRunner
{
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestRunner(
Action? onAfterTestInvoked,
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
{
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
{
return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments,
TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync();
}
}
private class AvaloniaTestInvoker : XunitTestInvoker
{
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestInvoker(
Action? onAfterTestInvoked,
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
object[] testMethodArguments, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus,
testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator,
cancellationTokenSource)
{
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override async Task AfterTestMethodInvokedAsync()
{
await base.AfterTestMethodInvokedAsync();
// Only here we can execute random code after the test, where exception will be properly handled by the XUnit.
if (_onAfterTestInvoked is not null)
{
Aggregator.Run(_onAfterTestInvoked);
}
}
}
}

8
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs

@ -1,10 +1,11 @@
using System.Reflection;
using System.Collections.Generic;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
internal class AvaloniaTestFramework : XunitTestFramework
{
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{
@ -26,8 +27,7 @@ internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
executionOptions.SetValue("xunit.execution.DisableParallelization", false);
using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>(
using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}

21
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs

@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Xunit.Abstractions;
using Xunit.Sdk;
@ -7,20 +9,13 @@ namespace Avalonia.Headless.XUnit;
/// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary>
/// <remarks>
/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method.
/// </remarks>
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkAttribute"/>.
/// </summary>
/// <param name="appBuilderEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public AvaloniaTestFrameworkAttribute(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type appBuilderEntryPointType) { }
}
/// <summary>
@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
/// <inheritdoc/>
public Type GetTestFrameworkType(IAttributeInfo attribute)
{
var builderType = attribute.GetConstructorArguments().First() as Type
?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute.");
return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType);
return typeof(AvaloniaTestFramework);
}
}

61
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs

@ -1,61 +0,0 @@
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestRunner<TAppBuilderEntry> : XunitTestAssemblyRunner
{
private CancellationTokenSource? _cancellationTokenSource;
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
executionMessageSink, executionOptions)
{
}
protected override void SetupSyncContext(int maxParallelThreads)
{
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result);
}
public override void Dispose()
{
_cancellationTokenSource?.Cancel();
base.Dispose();
}
internal static Task<SynchronizationContext> InitNewApplicationContext(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<SynchronizationContext>();
new Thread(() =>
{
try
{
var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry));
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
if (appBuilder.WindowingSubsystemName != "Headless")
{
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions());
}
appBuilder.SetupWithoutStarting();
tcs.SetResult(SynchronizationContext.Current!);
}
catch (Exception e)
{
tcs.SetException(e);
}
Dispatcher.UIThread.MainLoop(cancellationToken);
}) { IsBackground = true }.Start();
return tcs.Task;
}
}

37
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
/// <summary>
/// Identifies an xunit theory that starts on Avalonia Dispatcher
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer", "Avalonia.Headless.XUnit")]
public sealed class AvaloniaTheoryAttribute : TheoryAttribute
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaTheoryDiscoverer : TheoryDiscoverer
{
public AvaloniaTheoryDiscoverer(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
{
yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow);
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
{
yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod);
}
}

31
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs

@ -0,0 +1,31 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTheoryTestCase : XunitTheoryTestCase
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public AvaloniaTheoryTestCase()
{
}
public AvaloniaTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod)
: base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod)
{
}
public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
return AvaloniaTestCaseRunner
.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
}
}

9
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -12,7 +12,16 @@
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Compile Remove="..\..\Shared\ModuleInitializer.cs" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

17
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -21,20 +21,21 @@ namespace Avalonia.Headless
private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> tick)
{
bool cancelled = false;
var st = Stopwatch.StartNew();
_forceTick = () => tick(st.Elapsed);
DispatcherTimer.Run(() =>
var timer = new DispatcherTimer(DispatcherPriority.Render)
{
if (cancelled)
return false;
tick(st.Elapsed);
return !cancelled;
}, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render);
Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
Tag = "HeadlessRenderTimer"
};
timer.Tick += (s, e) => tick(st.Elapsed);
timer.Start();
return Disposable.Create(() =>
{
_forceTick = null;
cancelled = true;
timer.Stop();
});
}

27
src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs

@ -0,0 +1,27 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Headless;
/// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestApplicationAttribute : Attribute
{
public Type AppBuilderEntryPointType { get; }
/// <summary>
/// Creates instance of <see cref="AvaloniaTestApplicationAttribute"/>.
/// </summary>
/// <param name="appBuilderEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public AvaloniaTestApplicationAttribute(
[DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)]
Type appBuilderEntryPointType)
{
AppBuilderEntryPointType = appBuilderEntryPointType;
}
}

99
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -18,7 +18,8 @@ namespace Avalonia.Headless
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub());
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub())
.Bind<ITextShaperImpl>().ToConstant(new HeadlessTextShaperStub());
}
public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" };
@ -128,18 +129,30 @@ namespace Avalonia.Headless
Point baselineOrigin,
Rect bounds)
{
return new HeadlessGlyphRunStub();
return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
}
private class HeadlessGlyphRunStub : IGlyphRunImpl
internal class HeadlessGlyphRunStub : IGlyphRunImpl
{
public Rect Bounds => new Rect(new Size(8, 12));
public HeadlessGlyphRunStub(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
Point baselineOrigin,
Rect bounds)
{
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BaselineOrigin = baselineOrigin;
Bounds =bounds;
}
public Point BaselineOrigin => new Point(0, 8);
public Rect Bounds { get; }
public IGlyphTypeface GlyphTypeface => new HeadlessGlyphTypefaceImpl();
public Point BaselineOrigin { get; }
public double FontRenderingEmSize => 12;
public IGlyphTypeface GlyphTypeface { get; }
public double FontRenderingEmSize { get; }
public void Dispose()
{
@ -234,8 +247,11 @@ namespace Avalonia.Headless
private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
private HeadlessStreamingGeometryContextStub _context;
public HeadlessStreamingGeometryStub() : base(default)
{
_context = new HeadlessStreamingGeometryContextStub(this);
}
public IStreamGeometryImpl Clone()
@ -245,13 +261,18 @@ namespace Avalonia.Headless
public IStreamGeometryContextImpl Open()
{
return new HeadlessStreamingGeometryContextStub(this);
return _context;
}
public override bool FillContains(Point point)
{
return _context.FillContains(point);
}
private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl
{
private readonly HeadlessStreamingGeometryStub _parent;
private double _x1, _y1, _x2, _y2;
private List<Point> points = new List<Point>();
public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent)
{
_parent = parent;
@ -259,19 +280,30 @@ namespace Avalonia.Headless
private void Track(Point pt)
{
if (_x1 > pt.X)
_x1 = pt.X;
if (_x2 < pt.X)
_x2 = pt.X;
if (_y1 > pt.Y)
_y1 = pt.Y;
if (_y2 < pt.Y)
_y2 = pt.Y;
points.Add(pt);
}
public Rect CalculateBounds()
{
var left = double.MaxValue;
var right = double.MinValue;
var top = double.MaxValue;
var bottom = double.MinValue;
foreach (var p in points)
{
left = Math.Min(p.X, left);
right = Math.Max(p.X, right);
top = Math.Min(p.Y, top);
bottom = Math.Max(p.Y, bottom);
}
return new Rect(new Point(left, top), new Point(right, bottom));
}
public void Dispose()
{
_parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1);
_parent.Bounds = CalculateBounds();
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
@ -303,6 +335,35 @@ namespace Avalonia.Headless
{
}
public bool FillContains(Point point)
{
// Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
// to determine if the point is in the geometry (since it will always be convex in this situation)
for (int i = 0; i < points.Count; i++)
{
var a = points[i];
var b = points[(i + 1) % points.Count];
var c = points[(i + 2) % points.Count];
Vector v0 = c - a;
Vector v1 = b - a;
Vector v2 = point - a;
var dot00 = v0 * v0;
var dot01 = v0 * v1;
var dot02 = v0 * v2;
var dot11 = v1 * v1;
var dot12 = v1 * v2;
var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
}
return false;
}
}
}
@ -368,7 +429,7 @@ namespace Avalonia.Headless
}
}
private class HeadlessDrawingContextStub : IDrawingContextImpl
internal class HeadlessDrawingContextStub : IDrawingContextImpl
{
public void Dispose()
{

109
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
@ -11,6 +13,7 @@ using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
@ -82,22 +85,22 @@ namespace Avalonia.Headless
{
public FontMetrics Metrics => new FontMetrics
{
DesignEmHeight = 1,
Ascent = 8,
Descent = 4,
DesignEmHeight = 10,
Ascent = 2,
Descent = 10,
IsFixedPitch = true,
LineGap = 0,
UnderlinePosition = 2,
UnderlineThickness = 1,
StrikethroughPosition = 2,
StrikethroughThickness = 1,
IsFixedPitch = true
StrikethroughThickness = 1
};
public int GlyphCount => 1337;
public FontSimulations FontSimulations { get; }
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName => "Arial";
public string FamilyName => "$Default";
public FontWeight Weight => FontWeight.Normal;
@ -111,24 +114,31 @@ namespace Avalonia.Headless
public ushort GetGlyph(uint codepoint)
{
return 1;
return (ushort)codepoint;
}
public bool TryGetGlyph(uint codepoint, out ushort glyph)
{
glyph = 1;
glyph = 8;
return true;
}
public int GetGlyphAdvance(ushort glyph)
{
return 12;
return 8;
}
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
return glyphs.ToArray().Select(x => (int)x).ToArray();
var advances = new int[glyphs.Length];
for (var i = 0; i < advances.Length; i++)
{
advances[i] = 8;
}
return advances;
}
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
@ -146,8 +156,8 @@ namespace Avalonia.Headless
{
metrics = new GlyphMetrics
{
Height = 10,
Width = 8
Width = 10,
Height = 10
};
return true;
@ -161,40 +171,81 @@ namespace Avalonia.Headless
var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel;
var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
var textSpan = text.Span;
var textStartIndex = TextTestHelper.GetStartCharIndex(text);
for (var i = 0; i < shapedBuffer.Length;)
{
var glyphCluster = i + textStartIndex;
var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
var glyphIndex = typeface.GetGlyph(codepoint);
return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
for (var j = 0; j < count; ++j)
{
shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
}
i += count;
}
return shapedBuffer;
}
}
internal class HeadlessFontManagerStub : IFontManagerImpl
{
private readonly string _defaultFamilyName;
public HeadlessFontManagerStub(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public int TryCreateGlyphTypefaceCount { get; private set; }
public string GetDefaultFontFamilyName()
{
return "Arial";
return _defaultFamilyName;
}
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return new string[] { "Arial" };
return new[] { _defaultFamilyName };
}
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
CultureInfo? culture, out Typeface fontKey)
{
glyphTypeface= new HeadlessGlyphTypefaceImpl();
fontKey = new Typeface(_defaultFamilyName);
return true;
return false;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl();
glyphTypeface = null;
TryCreateGlyphTypefaceCount++;
if (familyName == "Unknown")
{
return false;
}
glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface)
public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch);
glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
}
@ -249,4 +300,14 @@ namespace Avalonia.Headless
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
internal static class TextTestHelper
{
public static int GetStartCharIndex(ReadOnlyMemory<char> text)
{
if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
throw new InvalidOperationException("text memory should have been a string");
return start;
}
}
}

214
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -0,0 +1,214 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Headless;
/// <summary>
/// Headless unit test session that needs to be used by the actual testing framework.
/// All UI tests are supposed to be executed from one of the <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
/// Disposing unit test session stops internal dispatcher loop.
/// </summary>
[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")]
public sealed class HeadlessUnitTestSession : IDisposable
{
private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new();
private readonly AppBuilder _appBuilder;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly BlockingCollection<Action> _queue;
private readonly Task _dispatchTask;
internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
BlockingCollection<Action> queue, Task dispatchTask)
{
_appBuilder = appBuilder;
_cancellationTokenSource = cancellationTokenSource;
_queue = queue;
_dispatchTask = dispatchTask;
}
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task Dispatch(Action action, CancellationToken cancellationToken)
{
return Dispatch(() =>
{
action();
return Task.FromResult(0);
}, cancellationToken);
}
/// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken)
{
return Dispatch(() => Task.FromResult(action()), cancellationToken);
}
/// <summary>
/// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
/// setting app avalonia services, and runs <see cref="action"/> parameter.
/// </summary>
/// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
/// <param name="cancellationToken">Cancellation token to cancel execution.</param>
/// <exception cref="ObjectDisposedException">
/// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
/// </exception>
public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken)
{
if (_cancellationTokenSource.IsCancellationRequested)
{
throw new ObjectDisposedException("Session was already disposed.");
}
var token = _cancellationTokenSource.Token;
var tcs = new TaskCompletionSource<TResult>();
_queue.Add(() =>
{
using var application = EnsureApplication();
var cts = new CancellationTokenSource();
using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
try
{
var task = action();
task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts,
TaskScheduler.FromCurrentSynchronizationContext());
if (cts.IsCancellationRequested)
{
return;
}
var frame = new DispatcherFrame();
using var innerCts = cts.Token.Register(() => frame.Continue = false, true);
Dispatcher.UIThread.PushFrame(frame);
var result = task.GetAwaiter().GetResult();
tcs.TrySetResult(result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
private IDisposable EnsureApplication()
{
var scope = AvaloniaLocator.EnterScope();
try
{
Dispatcher.ResetForUnitTests();
_appBuilder.SetupUnsafe();
}
catch
{
scope.Dispose();
throw;
}
return Disposable.Create(() =>
{
scope.Dispose();
Dispatcher.ResetForUnitTests();
});
}
public void Dispose()
{
_cancellationTokenSource.Cancel();
_queue.CompleteAdding();
_dispatchTask.Wait();
_cancellationTokenSource.Dispose();
}
/// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary>
/// <param name="entryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public static HeadlessUnitTestSession StartNew(
[DynamicallyAccessedMembers(DynamicallyAccessed)]
Type entryPointType)
{
var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
var cancellationTokenSource = new CancellationTokenSource();
var queue = new BlockingCollection<Action>();
Task? task = null;
task = Task.Run(() =>
{
try
{
var appBuilder = AppBuilder.Configure(entryPointType);
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
if (appBuilder.WindowingSubsystemName != "Headless")
{
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions());
}
// ReSharper disable once AccessToModifiedClosure
tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!));
}
catch (Exception e)
{
tcs.SetException(e);
return;
}
while (!cancellationTokenSource.IsCancellationRequested)
{
try
{
var action = queue.Take(cancellationTokenSource.Token);
action();
}
catch (OperationCanceledException)
{
}
}
});
return tcs.Task.GetAwaiter().GetResult();
}
/// <summary>
/// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
/// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL2072",
Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")]
public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly)
{
return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a =>
{
var appBuilderEntryPointType = a.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
?.AppBuilderEntryPointType;
return appBuilderEntryPointType is not null ?
StartNew(appBuilderEntryPointType) :
StartNew(typeof(Application));
});
}
}

21
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -44,53 +44,56 @@ public static class HeadlessWindowExtensions
/// Simulates keyboard press on the headless window/toplevel.
/// </summary>
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers);
RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers));
/// <summary>
/// Simulates keyboard release on the headless window/toplevel.
/// </summary>
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers);
RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers));
/// <summary>
/// Simulates mouse down on the headless window/toplevel.
/// </summary>
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers);
RunJobsOnImpl(topLevel, w => w.MouseDown(point, button, modifiers));
/// <summary>
/// Simulates mouse move on the headless window/toplevel.
/// </summary>
public static void MouseMove(this TopLevel topLevel, Point point,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers);
RunJobsOnImpl(topLevel, w => w.MouseMove(point, modifiers));
/// <summary>
/// Simulates mouse up on the headless window/toplevel.
/// </summary>
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers);
RunJobsOnImpl(topLevel, w => w.MouseUp(point, button, modifiers));
/// <summary>
/// Simulates mouse wheel on the headless window/toplevel.
/// </summary>
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers);
RunJobsOnImpl(topLevel, w => w.MouseWheel(point, delta, modifiers));
/// <summary>
/// Simulates drag'n'drop target on the headless window/toplevel.
/// </summary>
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data,
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers);
RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers));
private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel)
private static void RunJobsOnImpl(this TopLevel topLevel, Action<IHeadlessWindow> action)
{
Dispatcher.UIThread.RunJobs();
return GetImpl(topLevel);
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
action(GetImpl(topLevel));
Dispatcher.UIThread.RunJobs();
}
private static IHeadlessWindow GetImpl(this TopLevel topLevel)

50
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@ -36,13 +36,29 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
}
if (valueNode.Manipulation is not XamlObjectInitializationNode
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
$"Invalid \"{nodeTypeName}\" node initialization.", valueNode);
}
var additionalProperties = new List<IXamlAstManipulationNode>();
if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
{
if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup
&& manipulationGroup.Children.OfType<XamlPropertyAssignmentNode>()
.FirstOrDefault(p => p.Property.Name == "Source") is { } sourceProperty2)
{
sourceProperty = sourceProperty2;
// We need to copy some additional properties from ResourceInclude to ResourceDictionary except the Source one.
// If there is any missing properties, then XAML compiler will throw an error in the emitter code.
additionalProperties = manipulationGroup.Children.Where(c => c != sourceProperty2).ToList();
}
else
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
}
}
var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{
if (targetDocument.BuildMethod is not null)
{
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
if (targetDocument.ClassType is not null)
{
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null)
{
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
else if (assetAssembly.FindType(fullTypeName) is { } type)
{
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(type))
{
@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode);
}
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
var sp = context.Configuration.TypeMappings.ServiceProvider;
return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) });
return new XamlValueWithManipulationNode(li,
new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) }),
new XamlManipulationGroupNode(li, manipulationNodes));
}
internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude(

10
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs

@ -10,7 +10,8 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
var type = context.GetAvaloniaTypes().IThemeVariantProvider;
var avTypes = context.GetAvaloniaTypes();
var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType())))
return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null)
return node;
var themeDictionariesColl = avTypes.IDictionaryT.MakeGenericType(avTypes.ThemeVariant, avTypes.IThemeVariantProvider);
if (context.ParentNodes().FirstOrDefault() is not XamlAstXamlPropertyValueNode propertyValueNode
|| !themeDictionariesColl.IsAssignableFrom(propertyValueNode.Property.GetClrProperty().Getter.ReturnType))
{
return node;
}
var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration),

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -65,6 +65,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType Int { get; }
public IXamlType Long { get; }
public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -701,7 +701,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
.GetConstructor(new() { objectType, context.Configuration.TypeSystem.GetType("System.IntPtr") }));
}
if (_dependsOnProperties is { Count:> 1 })
if (_dependsOnProperties is { Count:> 0 })
{
using var dependsOnPropertiesArray = context.GetLocalOfType(context.Configuration.WellKnownTypes.String.MakeArrayType(1));
codeGen

4
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -41,9 +41,9 @@ namespace Avalonia.Win32
internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32()))
if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.TryGetValue(wParam.ToInt32(), out var value))
{
s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
value.WndProc(hWnd, msg, wParam, lParam);
}
if (msg == WM_TASKBARCREATED)

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

Loading…
Cancel
Save