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 dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning 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 dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences # Wrapping preferences

4
.nuke/build.schema.json

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

1
Avalonia.Desktop.slnf

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

53
Avalonia.sln

@ -246,12 +246,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = 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 {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.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.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.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.ActiveCfg = Debug|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = 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 {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.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}.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -706,15 +727,21 @@ Global
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {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} {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} {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} {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {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} {4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {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} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

5
dirs.proj

@ -9,10 +9,11 @@
<ProjectReference Remove="**/*.shproj" /> <ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" /> <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*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/*.iOS/*.csproj" />
<ProjectReference Remove="samples/*.Android/*.csproj" /> <ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Web/*.csproj" /> <ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/*.Blazor/*.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'"> <ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">
<ProjectReference Remove="src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj" /> <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 partial class Build : NukeBuild
{ {
[Solution("Avalonia.sln")] readonly Solution Solution;
BuildParameters Parameters { get; set; } BuildParameters Parameters { get; set; }
protected override void OnBuildInitialized() protected override void OnBuildInitialized()
{ {
@ -143,10 +141,12 @@ partial class Build : NukeBuild
void RunCoreTest(string projectName) void RunCoreTest(string projectName)
{ {
Information($"Running tests from {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. // 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. // 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") var targetFrameworks = fileXml.Descendants("TargetFrameworks")
.FirstOrDefault()?.Value.Split(';').Select(f => f.Trim()); .FirstOrDefault()?.Value.Split(';').Select(f => f.Trim());
if (targetFrameworks is null) if (targetFrameworks is null)
@ -212,7 +212,8 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.Headless.UnitTests"); RunCoreTest("Avalonia.Headless.NUnit.UnitTests");
RunCoreTest("Avalonia.Headless.XUnit.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _
@ -311,7 +312,7 @@ partial class Build : NukeBuild
public static int Main() => public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Execute<Build>(x => x.Package) ? Execute<Build>(x => x.RunToolsTests)
: Execute<Build>(x => x.RunTests); : 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!; var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows) foreach (var window in lifetime.Windows.ToArray())
{ {
window.Activate(); window.Activate();
} }
@ -212,7 +212,7 @@ namespace IntegrationTestApp
{ {
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows) foreach (var window in lifetime.Windows.ToArray())
{ {
window.Show(); window.Show();
if (window.WindowState == WindowState.Minimized) 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) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
value = Inner?.Convert(value, targetType, parameter, culture) ?? value; 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/> /// <inheritdoc/>

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

@ -122,6 +122,11 @@ namespace Avalonia.Input
{ {
scope = scope ?? throw new ArgumentNullException(nameof(scope)); scope = scope ?? throw new ArgumentNullException(nameof(scope));
if (element is not null && !CanFocus(element))
{
return;
}
if (_focusScopes.TryGetValue(scope, out var existingElement)) if (_focusScopes.TryGetValue(scope, out var existingElement))
{ {
if (element != 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>()); PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
} }
else if (change.Property == IsVisibleProperty && !change.GetNewValue<bool>() && IsFocused)
{
FocusManager.Instance?.Focus(null);
}
} }
/// <summary> /// <summary>

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

@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput
} }
_transformTracker.SetVisual(_client?.TextViewVisual); _transformTracker.SetVisual(_client?.TextViewVisual);
UpdateCursorRect();
_im?.SetClient(_client); _im?.SetClient(_client);
UpdateCursorRect();
} }
else 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); var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name)) 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 == '-') else if (ch == '-')
{ {
if (len != 0) if (len != 0)
return false; break;
len = c + 1; len = c + 1;
} }
else else

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

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

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

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

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

@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using Avalonia.Layout;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -489,7 +490,7 @@ namespace Avalonia.Controls
{ {
DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn; DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth; double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
if (finalWidth > totalColumnsWidth) if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon)
{ {
fillerColumn.FillerWidth = finalWidth - totalColumnsWidth; fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
} }
@ -971,6 +972,12 @@ namespace Avalonia.Controls
{ {
cx += _negHorizontalOffset; cx += _negHorizontalOffset;
_horizontalOffset -= _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; _negHorizontalOffset = 0;
} }
else else
@ -979,6 +986,11 @@ namespace Avalonia.Controls
_negHorizontalOffset -= displayWidth - cx; _negHorizontalOffset -= displayWidth - cx;
cx = displayWidth; 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 // second try to scroll entire columns
if (cx < displayWidth && _horizontalOffset > 0) 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)); DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnDetailsTemplateChanged(e));
AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e)); AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e));
PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true); PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
IsTabStopProperty.OverrideDefaultValue<DataGridRow>(false);
} }
/// <summary> /// <summary>

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

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

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

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

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

@ -126,7 +126,6 @@
<ControlTheme x:Key="{x:Type DataGridRowHeader}" <ControlTheme x:Key="{x:Type DataGridRowHeader}"
TargetType="DataGridRowHeader"> TargetType="DataGridRowHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid x:Name="PART_Root" <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> /// <returns>True if the currently focused element is within the visual tree of the parent</returns>
internal static bool ContainsFocusedElement(this Visual element) 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; s_setupWasAlreadyCalled = true;
SetupUnsafe();
}
/// <summary>
/// Setup method that doesn't check for input initalizers being set.
/// Nor
/// </summary>
internal void SetupUnsafe()
{
_optionsInitializers?.Invoke(); _optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer(); RuntimePlatformServicesInitializer?.Invoke();
RenderingSubsystemInitializer(); RenderingSubsystemInitializer?.Invoke();
WindowingSubsystemInitializer(); WindowingSubsystemInitializer?.Invoke();
AfterPlatformServicesSetupCallback(Self); AfterPlatformServicesSetupCallback?.Invoke(Self);
Instance = _appFactory(); Instance = _appFactory!();
Instance.ApplicationLifetime = _lifetime; Instance.ApplicationLifetime = _lifetime;
AvaloniaLocator.CurrentMutable.BindToSelf(Instance); AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices(); Instance.RegisterServices();
Instance.Initialize(); Instance.Initialize();
AfterSetupCallback(Self); AfterSetupCallback?.Invoke(Self);
Instance.OnFrameworkInitializationCompleted(); Instance.OnFrameworkInitializationCompleted();
} }
} }

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -16,7 +17,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
private int _exitCode; private int _exitCode;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private bool _isShuttingDown; private bool _isShuttingDown;
private readonly HashSet<Window> _windows = new(); private readonly AvaloniaList<Window> _windows = new();
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime; private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
@ -35,7 +36,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
private static void OnWindowOpened(object? sender, RoutedEventArgs e) 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() public ClassicDesktopStyleApplicationLifetime()
@ -67,7 +72,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
public Window? MainWindow { get; set; } public Window? MainWindow { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<Window> Windows => _windows.ToArray(); public IReadOnlyList<Window> Windows => _windows;
private void HandleWindowClosed(Window? window) 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 // 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 // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners. // owners.
foreach (var w in Windows) foreach (var w in Windows.ToArray())
{ {
if (w.Owner is null) 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.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless.XUnit, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />

11
src/Avalonia.Controls/ComboBox.cs

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

4
src/Avalonia.Controls/Control.cs

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

1
src/Avalonia.Controls/DesktopApplicationExtensions.cs

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

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

@ -24,9 +24,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="ItemTemplate"/> property /// Defines the <see cref="ItemTemplate"/> property
/// </summary> /// </summary>
public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty = public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate), AvaloniaProperty.Register<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate));
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
/// <summary> /// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property. /// Defines the <see cref="ItemContainerTheme"/> property.
@ -59,8 +58,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IDataTemplate? ItemTemplate public IDataTemplate? ItemTemplate
{ {
get => _itemTemplate; get => GetValue(ItemTemplateProperty);
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); set => SetValue(ItemTemplateProperty, value);
} }
/// <summary> /// <summary>
@ -82,7 +81,6 @@ namespace Avalonia.Controls
} }
private Classes? _classes; private Classes? _classes;
private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter() 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 /// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// process should be followed: /// process should be followed:
/// ///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is /// - <see cref="NeedsContainer(object, int, out object?)"/> should first be called to
/// derived from the <see cref="Control"/> class. If this method returns true then the /// determine whether the item needs a container. This method will return true if the item
/// item itself should be used as the container. /// should be wrapped in a container control, or false if the item itself can be used as a
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then /// container.
/// <see cref="CreateContainer"/> should be called to create a new 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 /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container. /// container.
/// - The container should then be added to the panel using /// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/> /// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called. /// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
/// ///
/// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> in the first step above returns true /// NOTE: If <see cref="NeedsContainer(object, int, out object?)"/> in the first step above
/// then the above steps should be carried out a single time; the first time the item is /// returns false then the above steps should be carried out a single time: the first time the
/// displayed. Otherwise the steps should be carried out each time a new container is realized /// item is displayed. Otherwise the steps should be carried out each time a new container is
/// for an item. /// realized for an item.
/// ///
/// When unrealizing a container, the following process should be followed: /// When unrealizing a container, the following process should be followed:
/// ///
/// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item /// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
/// cannot be unrealized or recycled. /// then the item cannot be unrealized or recycled.
/// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container /// - 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. /// - If recycling is supported by the panel and the container then the container should be
/// - It is assumed that recyclable containers will not be removed from the panel but instead /// added to a recycle pool keyed on the recycle key returned from
/// hidden from view using e.g. `container.IsVisible = false`. /// <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: /// 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. /// - The container should be made visible.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the /// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container. /// container.
@ -55,28 +65,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner; internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary> /// <summary>
/// Creates a new container control. /// Determines whether the specified item needs to be wrapped in a container control.
/// </summary> /// </summary>
/// <returns>The newly created container control.</returns> /// <param name="item">The item to display.</param>
/// <remarks> /// <param name="index">The index of the item.</param>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be /// <param name="recycleKey">
/// called to determine whether the item itself should be used as a container. After /// When the method returns, contains a key that can be used to locate a previously
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should /// recycled container of the correct type, or null if the item cannot be recycled.
/// be called to prepare the container to display the specified item. /// </param>
/// </remarks> /// <returns>
public Control CreateContainer() => _owner.CreateContainerForItemOverride(); /// 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> /// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container. /// Creates a new container control.
/// </summary> /// </summary>
/// <param name="container">The item.</param> /// <param name="item">The item to display.</param>
/// <returns>true if the item is its own container, otherwise false.</returns> /// <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> /// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only /// Before calling this method, <see cref="NeedsContainer(object, int, out object?)"/>
/// control items may be; the caller is responsible for checking if each item is a control /// should be called to determine whether the item itself should be used as a container.
/// and calling this method before creating a new 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> /// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container); public Control CreateContainer(object? item, int index, object? recycleKey)
=> _owner.CreateContainerForItemOverride(item, index, recycleKey);
/// <summary> /// <summary>
/// Prepares the specified element as the container for the corresponding item. /// 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="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param> /// <param name="index">The index of the item to display.</param>
/// <remarks> /// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method /// If <see cref="NeedsContainer(object, int, out object?)"/> is false for an
/// must only be called a single time, otherwise this method must be called after the /// item, then this method must only be called a single time; otherwise this method must
/// container is created, and each subsequent time the container is recycled to display a /// be called after the container is created, and each subsequent time the container is
/// new item. /// recycled to display a new item.
/// </remarks> /// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) => public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, 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 /// 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. /// 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 /// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then /// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
/// must be called only a single time. /// false then must be called only a single time.
/// </remarks> /// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) => public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, 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 /// 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 /// 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 /// 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> /// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container); 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); 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> /// <summary>
/// Returns the container for the item at the specified index. /// Returns the container for the item at the specified index.
/// </summary> /// </summary>
@ -362,7 +368,10 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Creates or a container that can be used to display an item. /// Creates or a container that can be used to display an item.
/// </summary> /// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter(); protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContentPresenter();
}
/// <summary> /// <summary>
/// Prepares the specified element to display the specified item. /// Prepares the specified element to display the specified item.
@ -495,11 +504,52 @@ namespace Avalonia.Controls
} }
/// <summary> /// <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> /// </summary>
/// <param name="item">The item to check.</param> /// <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> /// <param name="index">The index of the item.</param>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true; /// <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 /> /// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

14
src/Avalonia.Controls/ListBox.cs

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

3
src/Avalonia.Controls/Menu.cs

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

18
src/Avalonia.Controls/MenuBase.cs

@ -133,8 +133,22 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
protected internal override Control CreateContainerForItemOverride() => new MenuItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; {
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/> /// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)

18
src/Avalonia.Controls/MenuItem.cs

@ -339,8 +339,22 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
protected internal override Control CreateContainerForItemOverride() => new MenuItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator; {
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) protected override void OnPointerReleased(PointerReleasedEventArgs e)
{ {

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

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

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

@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
var generator = itemsControl.ItemContainerGenerator; var generator = itemsControl.ItemContainerGenerator;
Control container; Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c)) if (generator.NeedsContainer(item, index, out var recycleKey))
{ {
container = c; container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(ItemIsOwnContainerProperty, true);
} }
else else
{ {
container = generator.CreateContainer(); container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
} }
generator.PrepareItemContainer(container, item, index); 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.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Xml.Linq;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Data; using Avalonia.Data;
@ -528,13 +529,6 @@ namespace Avalonia.Controls.Primitives
protected internal override void ClearContainerForItemOverride(Control element) protected internal override void ClearContainerForItemOverride(Control element)
{ {
base.ClearContainerForItemOverride(element); base.ClearContainerForItemOverride(element);
if (Presenter?.Panel is InputElement panel &&
KeyboardNavigation.GetTabOnceActiveElement(panel) == element)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
}
element.ClearValue(IsSelectedProperty); element.ClearValue(IsSelectedProperty);
} }
@ -834,12 +828,6 @@ namespace Avalonia.Controls.Primitives
Selection.Clear(); Selection.Clear();
Selection.Select(index); Selection.Select(index);
} }
if (Presenter?.Panel is { } panel)
{
var container = ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(panel, container);
}
} }
/// <summary> /// <summary>
@ -928,6 +916,7 @@ namespace Avalonia.Controls.Primitives
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{ {
_hasScrolledToSelectedItem = false; _hasScrolledToSelectedItem = false;
KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(Selection.AnchorIndex));
AutoScrollToSelectedItemIfNecessary(); AutoScrollToSelectedItemIfNecessary();
} }
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) 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); ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
} }
protected internal override Control CreateContainerForItemOverride() => new TabStripItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem; {
return new TabStripItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabStripItem>(item, out recycleKey);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e) protected override void OnGotFocus(GotFocusEventArgs e)

35
src/Avalonia.Controls/TabControl.cs

@ -148,8 +148,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter); return RegisterContentPresenter(presenter);
} }
protected internal override Control CreateContainerForItemOverride() => new TabItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem; {
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) protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{ {
@ -218,6 +225,20 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter"); 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/> /// <inheritdoc/>
@ -261,7 +282,17 @@ namespace Avalonia.Controls
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == TabStripPlacementProperty) if (change.Property == TabStripPlacementProperty)
{
RefreshContainers(); 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); return (false, null);
} }
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; {
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) 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; 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) 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> /// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{ {
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty = private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer"); AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly object s_itemIsItsOwnContainer = new object();
private Size _extent; private Size _extent;
private Vector _offset; private Vector _offset;
private Size _viewport; private Size _viewport;
private Stack<Control>? _recyclePool; private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized; private Control? _realized;
private int _realizedIndex = -1; private int _realizedIndex = -1;
private Control? _transitionFrom; private Control? _transitionFrom;
@ -172,7 +173,7 @@ namespace Avalonia.Controls
return null; return null;
if (index == _realizedIndex) if (index == _realizedIndex)
return _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 c;
return null; return null;
} }
@ -246,10 +247,27 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index) private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{ {
return GetRealizedElement(index) ?? Debug.Assert(ItemContainerGenerator is not null);
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ?? var e = GetRealizedElement(index);
CreateElement(items, 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) private Control? GetRealizedElement(int index)
@ -257,44 +275,37 @@ namespace Avalonia.Controls
return _realizedIndex == index ? _realized : null; 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); 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; generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
if (controlItem.IsSet(ItemIsOwnContainerProperty)) controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
{ generator.ItemContainerPrepared(controlItem, item, index);
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;
}
} }
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); Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator; if (recycleKey is null)
var item = items[index]; 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; recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index); generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index); generator.ItemContainerPrepared(recycled, item, index);
@ -304,14 +315,14 @@ namespace Avalonia.Controls
return null; 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); Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator; var generator = ItemContainerGenerator!;
var item = items[index]; var container = generator.CreateContainer(item, index, recycleKey);
var container = generator.CreateContainer();
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index); generator.PrepareItemContainer(container, item, index);
AddInternalChild(container); AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index); generator.ItemContainerPrepared(container, item, index);
@ -323,7 +334,10 @@ namespace Avalonia.Controls
{ {
Debug.Assert(ItemContainerGenerator is not null); 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; element.IsVisible = false;
} }
@ -331,7 +345,14 @@ namespace Avalonia.Controls
{ {
ItemContainerGenerator.ClearItemContainer(element); ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new(); _recyclePool ??= new();
_recyclePool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
element.IsVisible = false; element.IsVisible = false;
} }
} }

112
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -52,10 +52,11 @@ namespace Avalonia.Controls
nameof(VerticalSnapPointsChanged), nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty = private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer"); AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0); 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, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved; private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex; private readonly Action<Control, int, int> _updateElementIndex;
@ -68,7 +69,7 @@ namespace Avalonia.Controls
private RealizedStackElements? _realizedElements; private RealizedStackElements? _realizedElements;
private ScrollViewer? _scrollViewer; private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport; private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool; private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement; private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1; private int _unrealizedFocusedIndex = -1;
@ -331,7 +332,7 @@ namespace Avalonia.Controls
return null; return null;
if (_realizedElements?.GetElement(index) is { } realized) if (_realizedElements?.GetElement(index) is { } realized)
return 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 c;
return null; return null;
} }
@ -492,8 +493,7 @@ namespace Avalonia.Controls
c = c?.GetVisualParent(); c = c?.GetVisualParent();
} }
return viewport.Intersect(new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity));
return viewport;
} }
private void RealizeElements( private void RealizeElements(
@ -561,10 +561,26 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index) private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{ {
var e = GetRealizedElement(index) ?? Debug.Assert(ItemContainerGenerator is not null);
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ?? var e = GetRealizedElement(index);
CreateElement(items, 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; return e;
} }
@ -575,38 +591,33 @@ namespace Avalonia.Controls
return _realizedElements?.GetElement(index); 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 controlItem = (Control)item!;
{ var generator = ItemContainerGenerator!;
var generator = ItemContainerGenerator!;
if (controlItem.IsSet(ItemIsOwnContainerProperty)) if (!controlItem.IsSet(RecycleKeyProperty))
{ {
controlItem.IsVisible = true; generator.PrepareItemContainer(controlItem, controlItem, index);
return controlItem; AddInternalChild(controlItem);
} controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
else if (generator.IsItemItsOwnContainer(controlItem)) generator.ItemContainerPrepared(controlItem, item, index);
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
} }
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); Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!; var generator = ItemContainerGenerator!;
var item = items[index];
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null) if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{ {
@ -617,9 +628,9 @@ namespace Avalonia.Controls
return element; 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; recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index); generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index); generator.ItemContainerPrepared(recycled, item, index);
@ -629,14 +640,14 @@ namespace Avalonia.Controls
return null; 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); Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!; var generator = ItemContainerGenerator!;
var item = items[index]; var container = generator.CreateContainer(item, index, recycleKey);
var container = generator.CreateContainer();
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index); generator.PrepareItemContainer(container, item, index);
AddInternalChild(container); AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index); generator.ItemContainerPrepared(container, item, index);
@ -650,7 +661,10 @@ namespace Avalonia.Controls
_scrollViewer?.UnregisterAnchorCandidate(element); _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; element.IsVisible = false;
} }
@ -663,8 +677,7 @@ namespace Avalonia.Controls
else else
{ {
ItemContainerGenerator!.ClearItemContainer(element); ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new(); PushToRecyclePool(recycleKey, element);
_recyclePool.Push(element);
element.IsVisible = false; element.IsVisible = false;
} }
} }
@ -673,19 +686,34 @@ namespace Avalonia.Controls
{ {
Debug.Assert(ItemContainerGenerator is not null); 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); RemoveInternalChild(element);
} }
else else
{ {
ItemContainerGenerator!.ClearItemContainer(element); ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new(); PushToRecyclePool(recycleKey, element);
_recyclePool.Push(element);
element.IsVisible = false; 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) private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
{ {
Debug.Assert(ItemContainerGenerator is not null); 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 namespace Avalonia.Diagnostics.Controls
{ {
class Application : AvaloniaObject internal class Application : TopLevelGroup
, Input.ICloseable, IDisposable , Input.ICloseable, IDisposable
{ {
@ -16,7 +16,8 @@ namespace Avalonia.Diagnostics.Controls
public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty = public static readonly StyledProperty<ThemeVariant?> RequestedThemeVariantProperty =
ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>(); ThemeVariantScope.RequestedThemeVariantProperty.AddOwner<Application>();
public Application(Avalonia.Application application) public Application(ClassicDesktopStyleApplicationLifetimeTopLevelGroup group, Avalonia.Application application)
: base(group)
{ {
_application = application; _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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Views; using Avalonia.Diagnostics.Views;
using Avalonia.Input; using Avalonia.Input;
@ -9,12 +11,9 @@ using Avalonia.Reactive;
namespace Avalonia.Diagnostics namespace Avalonia.Diagnostics
{ {
public static class DevTools internal static class DevTools
{ {
private static readonly Dictionary<AvaloniaObject, MainWindow> s_open = private static readonly Dictionary<IDevToolsTopLevelGroup, MainWindow> s_open = new();
new Dictionary<AvaloniaObject, MainWindow>();
private static bool s_attachedToApplication;
public static IDisposable Attach(TopLevel root, KeyGesture gesture) public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{ {
@ -26,11 +25,6 @@ namespace Avalonia.Diagnostics
public static IDisposable Attach(TopLevel root, DevToolsOptions options) 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) void PreviewKeyDown(object? sender, KeyEventArgs e)
{ {
if (options.Gesture.Matches(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, InputElement.KeyDownEvent,
PreviewKeyDown, PreviewKeyDown,
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
} }
public static IDisposable Open(TopLevel root) => public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current,new DevToolsOptions(),root as Window); Open(new SingleViewTopLevelGroup(root), options, root as Window, null);
public static IDisposable Open(TopLevel root, DevToolsOptions options) =>
Open(Application.Current, options, root as Window);
private static void DevToolsClosed(object? sender, EventArgs e) internal static IDisposable Open(IDevToolsTopLevelGroup group, DevToolsOptions options) =>
{ Open(group, options, null, null);
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 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 openedDisposable = new SerialDisposableValue();
var result = new CompositeDisposable(2); var result = new CompositeDisposable(2);
result.Add(openedDisposable); result.Add(openedDisposable);
// Skip if call on Design Mode // Skip if call on Design Mode
if (!Avalonia.Controls.Design.IsDesignMode if (!Design.IsDesignMode)
&& !s_attachedToApplication)
{ {
var lifeTime = application.ApplicationLifetime var lifeTime = application.ApplicationLifetime
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime; as Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime;
if (lifeTime is null) 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 => result.Add(application.InputManager.PreProcess.Subscribe(e =>
{ {
var owner = lifeTime.MainWindow;
if (e is RawKeyEventArgs keyEventArgs if (e is RawKeyEventArgs keyEventArgs
&& keyEventArgs.Type == RawKeyEventType.KeyUp && keyEventArgs.Type == RawKeyEventType.KeyUp
&& options.Gesture.Matches(keyEventArgs)) && options.Gesture.Matches(keyEventArgs))
{ {
openedDisposable.Disposable = Open(application, options, owner); openedDisposable.Disposable =
Open(new ClassicDesktopStyleApplicationLifetimeTopLevelGroup(lifeTime), options,
owner, application);
e.Handled = true;
} }
})); }));
} }
} }
return result; 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; var focussedControl = KeyboardDevice.Instance?.FocusedElement as Control;
if (application is null) AvaloniaObject root = topLevelGroup switch
{ {
throw new ArgumentNullException(nameof(application)); ClassicDesktopStyleApplicationLifetimeTopLevelGroup gr => new Controls.Application(gr, app ?? Application.Current!),
} SingleViewTopLevelGroup gr => gr.Items.First(),
if (s_open.TryGetValue(application, out var window)) _ => 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(); mainWindow.Activate();
window.SelectedControl(focussedControl); mainWindow.SelectedControl(focussedControl);
return Disposable.Empty;
} }
else if (topLevelGroup.Items.Count == 1 && topLevelGroup.Items is not INotifyCollectionChanged)
{ {
window = new MainWindow var singleTopLevel = topLevelGroup.Items.First();
{
Root = new Controls.Application(application), foreach (var group in s_open)
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 { })
{ {
window.Show(owner); if (group.Key.Items.Contains(singleTopLevel))
} {
else group.Value.Activate();
{ group.Value.SelectedControl(focussedControl);
window.Show(); 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 Children = avaloniaObject switch
{ {
ILogical logical => new LogicalTreeNodeCollection(this, logical), ILogical logical => new LogicalTreeNodeCollection(this, logical),
Controls.Application host => new ApplicationHostLogical(this, host), Controls.TopLevelGroup host => new TopLevelGroupHostLogical(this, host),
_ => TreeNodeCollection.Empty _ => TreeNodeCollection.Empty
}; };
} }
@ -55,70 +55,55 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
internal class ApplicationHostLogical : TreeNodeCollection internal class TopLevelGroupHostLogical : TreeNodeCollection
{ {
readonly Controls.Application _application; private readonly Controls.TopLevelGroup _group;
CompositeDisposable _subscriptions = new CompositeDisposable(2); private readonly CompositeDisposable _subscriptions = new(1);
public ApplicationHostLogical(TreeNode owner, Controls.Application host) :
public TopLevelGroupHostLogical(TreeNode owner, Controls.TopLevelGroup host) :
base(owner) base(owner)
{ {
_application = host; _group = host;
} }
protected override void Initialize(AvaloniaList<TreeNode> nodes) protected override void Initialize(AvaloniaList<TreeNode> nodes)
{ {
if (_application.ApplicationLifetime is Lifetimes.ISingleViewApplicationLifetime single && for (var i = 0; i < _group.Items.Count; i++)
single.MainView is not null)
{ {
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)
{ {
if (e is Views.MainWindow)
for (int i = 0; i < classic.Windows.Count; i++)
{ {
var window = classic.Windows[i]; return;
if (window is Views.MainWindow)
{
continue;
}
nodes.Add(new LogicalTreeNode(window, Owner));
} }
_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)=> return;
{ }
if (s is Views.MainWindow)
{ nodes.Add(new LogicalTreeNode(e, Owner));
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);
}
}
}),
};
} }
_group.Added += GroupOnAdded;
_group.Removed += GroupOnRemoved;
_subscriptions.Add(new Disposable.AnonymousDisposable(() =>
{
_group.Added -= GroupOnAdded;
_group.Removed -= GroupOnRemoved;
}));
} }
public override void Dispose() public override void Dispose()

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

@ -237,9 +237,9 @@ namespace Avalonia.Diagnostics.Views
else else
{ {
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1 //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); _frozenPopupStates.Remove(popup);
} }
} }

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

@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" /> <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>
<ItemGroup> <ItemGroup>

53
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -1,8 +1,8 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator; using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop namespace Avalonia.FreeDesktop
@ -22,39 +22,47 @@ namespace Avalonia.FreeDesktop
_settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValueAsync(); _ = TryGetInitialValuesAsync();
} }
public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues(); 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 try
{ {
var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); 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 try
{ {
var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor"); 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) if (exception is not null)
return; return;
@ -62,12 +70,8 @@ namespace Avalonia.FreeDesktop
switch (valueTuple) switch (valueTuple)
{ {
case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): case ("org.freedesktop.appearance", "color-scheme", { } colorScheme):
_themeVariant = ReadAsColorScheme(colorScheme); _themeVariant = ToColorScheme((colorScheme.Value as DBusUInt32Item)!.Value);
_lastColorValues = BuildPlatformColorValues(); _accentColor = await TryGetAccentColorAsync();
OnColorValuesChanged(_lastColorValues!);
break;
case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor):
_accentColor = ReadAsAccentColor(accentColor);
_lastColorValues = BuildPlatformColorValues(); _lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!); OnColorValuesChanged(_lastColorValues!);
break; break;
@ -85,21 +89,20 @@ namespace Avalonia.FreeDesktop
return null; return null;
} }
private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) private static PlatformThemeVariant ToColorScheme(uint value)
{ {
/* /*
<member>0: No preference</member> <member>0: No preference</member>
<member>1: Prefer dark appearance</member> <member>1: Prefer dark appearance</member>
<member>2: Prefer light 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; 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 = value.Split(',');
var rgb = colorStr.Split(',');
return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2])); 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"); var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
try try
{ {
await dbusFileChooser.GetVersionAsync(); await dbusFileChooser.GetVersionPropertyAsync();
} }
catch catch
{ {

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

@ -715,7 +715,8 @@ namespace Metsys.Bson
public MagicProperty FindProperty(string name) 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) public static TypeHelper GetHelperForType(Type type)
@ -1196,7 +1197,9 @@ namespace Metsys.Bson
} }
object container = null; object container = null;
var property = typeHelper.FindProperty(name); 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) if (property != null && property.Setter == null)
{ {
container = property.Getter(instance); container = property.Getter(instance);
@ -1588,7 +1591,7 @@ namespace Metsys.Bson.Configuration
{ {
return property; return property;
} }
return map.ContainsKey(property) ? map[property] : property; return map.TryGetValue(property, out var value) ? value : property;
} }
public void AddIgnore<T>(string name) 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. /// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary> /// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; } 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 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 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)] [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile(); 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(); public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] [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)] [JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple, 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)] [JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName, 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)] [JSImport("StorageItem.createWellKnownDirectory", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateWellKnownDirectory(string wellKnownDirectory); 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 System.Threading.Tasks;
using Avalonia.Browser.Interop; using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Browser.Storage; namespace Avalonia.Browser.Storage;
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
internal class BrowserStorageProvider : IStorageProvider internal class BrowserStorageProvider : IStorageProvider
{ {
internal const string PickerCancelMessage = "The user aborted a request"; internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied"; internal const string NoPermissionsMessage = "Permissions denied";
public bool CanOpen => true; public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker(); public bool CanSave => true;
public bool CanPickFolder => true; public bool CanPickFolder => true;
private bool PreferPolyfill =>
AvaloniaLocator.Current.GetService<BrowserPlatformOptions>()?.PreferFileDialogPolyfill ?? false;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{ {
await AvaloniaModule.ImportStorage(); await AvaloniaModule.ImportStorage();
@ -29,7 +31,7 @@ internal class BrowserStorageProvider : IStorageProvider
try 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) if (items is null)
{ {
return Array.Empty<IStorageFile>(); return Array.Empty<IStorageFile>();
@ -63,7 +65,9 @@ internal class BrowserStorageProvider : IStorageProvider
try 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; return item is not null ? new JSStorageFile(item) : null;
} }
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
@ -89,7 +93,7 @@ internal class BrowserStorageProvider : IStorageProvider
try 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>(); return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
} }
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) 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;
using System.Threading; using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia; using Avalonia.Browser.Skia;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@ -46,6 +47,13 @@ namespace Avalonia.Browser
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics()) .Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>(); .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) public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

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

@ -1,7 +1,8 @@
require("esbuild").build({ require("esbuild").build({
entryPoints: [ entryPoints: [
"./modules/avalonia.ts", "./modules/avalonia.ts",
"./modules/storage.ts" "./modules/storage.ts",
"./modules/sw.ts"
], ],
outdir: "../wwwroot", outdir: "../wwwroot",
bundle: true, 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 { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers"; 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 { export {
Caniuse, Caniuse,
Canvas, Canvas,
@ -17,5 +23,6 @@ export {
StreamHelper, StreamHelper,
NativeControlHost, NativeControlHost,
NavigationHelper, 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); const array = new Uint8Array(span.byteLength);
span.copyTo(array); span.copyTo(array);
const data = { return await stream.write(array);
type: "write",
data: array
};
return await stream.write(data);
} }
public static byteLength(stream: Blob) { 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 { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem"; 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 { declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
@ -12,10 +12,12 @@ declare global {
export class StorageProvider { export class StorageProvider {
public static async selectFolderDialog( 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. // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined) startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
_preferPolyfill: preferPolyfill
}; };
const handle = await showDirectoryPicker(options as any); const handle = await showDirectoryPicker(options as any);
@ -24,12 +26,14 @@ export class StorageProvider {
public static async openFileDialog( public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean, startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> { types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItems> {
const options = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple, multiple,
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types ?? undefined) types: (types ?? undefined),
_preferPolyfill: preferPolyfill
}; };
const handles = await showOpenFilePicker(options); const handles = await showOpenFilePicker(options);
@ -38,16 +42,17 @@ export class StorageProvider {
public static async saveFileDialog( public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null, startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> { types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItem> {
const options = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined), suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types ?? undefined) types: (types ?? undefined),
_preferPolyfill: preferPolyfill
}; };
// Always prefer native save file picker, as polyfill solutions are not reliable. const handle = await showSaveFilePicker(options);
const handle = await (globalThis as any).showSaveFilePicker(options);
return StorageItem.createFromHandle(handle); 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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <IsTestProject>false</IsTestProject>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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>
<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.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
namespace Avalonia.Headless.XUnit; namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework internal class AvaloniaTestFramework : XunitTestFramework
{ {
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{ {
@ -26,8 +27,7 @@ internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
IMessageSink executionMessageSink, IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions) ITestFrameworkExecutionOptions executionOptions)
{ {
executionOptions.SetValue("xunit.execution.DisableParallelization", false); using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync(); 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.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
@ -7,20 +9,13 @@ namespace Avalonia.Headless.XUnit;
/// <summary> /// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. /// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary> /// </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")] [TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute 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> /// <summary>
@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
/// <inheritdoc/> /// <inheritdoc/>
public Type GetTestFrameworkType(IAttributeInfo attribute) public Type GetTestFrameworkType(IAttributeInfo attribute)
{ {
var builderType = attribute.GetConstructorArguments().First() as Type return typeof(AvaloniaTestFramework);
?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute.");
return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType);
} }
} }

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\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Compile Remove="..\..\Shared\ModuleInitializer.cs" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" /> <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> </ItemGroup>
</Project> </Project>

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

@ -21,20 +21,21 @@ namespace Avalonia.Headless
private Action? _forceTick; private Action? _forceTick;
protected override IDisposable StartCore(Action<TimeSpan> tick) protected override IDisposable StartCore(Action<TimeSpan> tick)
{ {
bool cancelled = false;
var st = Stopwatch.StartNew(); var st = Stopwatch.StartNew();
_forceTick = () => tick(st.Elapsed); _forceTick = () => tick(st.Elapsed);
DispatcherTimer.Run(() =>
var timer = new DispatcherTimer(DispatcherPriority.Render)
{ {
if (cancelled) Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
return false; Tag = "HeadlessRenderTimer"
tick(st.Elapsed); };
return !cancelled; timer.Tick += (s, e) => tick(st.Elapsed);
}, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render); timer.Start();
return Disposable.Create(() => return Disposable.Create(() =>
{ {
_forceTick = null; _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 AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface()) .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" }; public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" };
@ -128,18 +129,30 @@ namespace Avalonia.Headless
Point baselineOrigin, Point baselineOrigin,
Rect bounds) 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() public void Dispose()
{ {
@ -234,8 +247,11 @@ namespace Avalonia.Headless
private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{ {
private HeadlessStreamingGeometryContextStub _context;
public HeadlessStreamingGeometryStub() : base(default) public HeadlessStreamingGeometryStub() : base(default)
{ {
_context = new HeadlessStreamingGeometryContextStub(this);
} }
public IStreamGeometryImpl Clone() public IStreamGeometryImpl Clone()
@ -245,13 +261,18 @@ namespace Avalonia.Headless
public IStreamGeometryContextImpl Open() public IStreamGeometryContextImpl Open()
{ {
return new HeadlessStreamingGeometryContextStub(this); return _context;
}
public override bool FillContains(Point point)
{
return _context.FillContains(point);
} }
private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl
{ {
private readonly HeadlessStreamingGeometryStub _parent; private readonly HeadlessStreamingGeometryStub _parent;
private double _x1, _y1, _x2, _y2; private List<Point> points = new List<Point>();
public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent) public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent)
{ {
_parent = parent; _parent = parent;
@ -259,19 +280,30 @@ namespace Avalonia.Headless
private void Track(Point pt) private void Track(Point pt)
{ {
if (_x1 > pt.X) points.Add(pt);
_x1 = pt.X;
if (_x2 < pt.X)
_x2 = pt.X;
if (_y1 > pt.Y)
_y1 = pt.Y;
if (_y2 < pt.Y)
_y2 = pt.Y;
} }
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() 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) 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() public void Dispose()
{ {

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

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
@ -11,6 +13,7 @@ using Avalonia.Input.Platform;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO; using Avalonia.Platform.Storage.FileIO;
@ -82,22 +85,22 @@ namespace Avalonia.Headless
{ {
public FontMetrics Metrics => new FontMetrics public FontMetrics Metrics => new FontMetrics
{ {
DesignEmHeight = 1, DesignEmHeight = 10,
Ascent = 8, Ascent = 2,
Descent = 4, Descent = 10,
IsFixedPitch = true,
LineGap = 0, LineGap = 0,
UnderlinePosition = 2, UnderlinePosition = 2,
UnderlineThickness = 1, UnderlineThickness = 1,
StrikethroughPosition = 2, StrikethroughPosition = 2,
StrikethroughThickness = 1, StrikethroughThickness = 1
IsFixedPitch = true
}; };
public int GlyphCount => 1337; 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; public FontWeight Weight => FontWeight.Normal;
@ -111,24 +114,31 @@ namespace Avalonia.Headless
public ushort GetGlyph(uint codepoint) public ushort GetGlyph(uint codepoint)
{ {
return 1; return (ushort)codepoint;
} }
public bool TryGetGlyph(uint codepoint, out ushort glyph) public bool TryGetGlyph(uint codepoint, out ushort glyph)
{ {
glyph = 1; glyph = 8;
return true; return true;
} }
public int GetGlyphAdvance(ushort glyph) public int GetGlyphAdvance(ushort glyph)
{ {
return 12; return 8;
} }
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) 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) public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
@ -146,8 +156,8 @@ namespace Avalonia.Headless
{ {
metrics = new GlyphMetrics metrics = new GlyphMetrics
{ {
Height = 10, Width = 10,
Width = 8 Height = 10
}; };
return true; return true;
@ -161,40 +171,81 @@ namespace Avalonia.Headless
var typeface = options.Typeface; var typeface = options.Typeface;
var fontRenderingEmSize = options.FontRenderingEmSize; var fontRenderingEmSize = options.FontRenderingEmSize;
var bidiLevel = options.BidiLevel; 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 internal class HeadlessFontManagerStub : IFontManagerImpl
{ {
private readonly string _defaultFamilyName;
public HeadlessFontManagerStub(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public int TryCreateGlyphTypefaceCount { get; private set; }
public string GetDefaultFontFamilyName() 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; 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; return true;
} }
} }
@ -249,4 +300,14 @@ namespace Avalonia.Headless
return ScreenHelper.ScreenFromWindow(window, AllScreens); 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. /// Simulates keyboard press on the headless window/toplevel.
/// </summary> /// </summary>
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers));
/// <summary> /// <summary>
/// Simulates keyboard release on the headless window/toplevel. /// Simulates keyboard release on the headless window/toplevel.
/// </summary> /// </summary>
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers));
/// <summary> /// <summary>
/// Simulates mouse down on the headless window/toplevel. /// Simulates mouse down on the headless window/toplevel.
/// </summary> /// </summary>
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) => RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); RunJobsOnImpl(topLevel, w => w.MouseDown(point, button, modifiers));
/// <summary> /// <summary>
/// Simulates mouse move on the headless window/toplevel. /// Simulates mouse move on the headless window/toplevel.
/// </summary> /// </summary>
public static void MouseMove(this TopLevel topLevel, Point point, public static void MouseMove(this TopLevel topLevel, Point point,
RawInputModifiers modifiers = RawInputModifiers.None) => RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); RunJobsOnImpl(topLevel, w => w.MouseMove(point, modifiers));
/// <summary> /// <summary>
/// Simulates mouse up on the headless window/toplevel. /// Simulates mouse up on the headless window/toplevel.
/// </summary> /// </summary>
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) => RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); RunJobsOnImpl(topLevel, w => w.MouseUp(point, button, modifiers));
/// <summary> /// <summary>
/// Simulates mouse wheel on the headless window/toplevel. /// Simulates mouse wheel on the headless window/toplevel.
/// </summary> /// </summary>
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta,
RawInputModifiers modifiers = RawInputModifiers.None) => RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); RunJobsOnImpl(topLevel, w => w.MouseWheel(point, delta, modifiers));
/// <summary> /// <summary>
/// Simulates drag'n'drop target on the headless window/toplevel. /// Simulates drag'n'drop target on the headless window/toplevel.
/// </summary> /// </summary>
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data,
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => 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(); Dispatcher.UIThread.RunJobs();
return GetImpl(topLevel); AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Dispatcher.UIThread.RunJobs();
action(GetImpl(topLevel));
Dispatcher.UIThread.RunJobs();
} }
private static IHeadlessWindow GetImpl(this TopLevel topLevel) 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"); throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
} }
if (valueNode.Manipulation is not XamlObjectInitializationNode if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
{ {
throw new XamlDocumentParseException(context.CurrentDocument, 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); var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{ {
if (targetDocument.BuildMethod is not null) 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) 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( return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName); var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null) 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) 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( return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
} }
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, 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)) 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}\".", $"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode); li, fallbackNode);
} }
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false)); IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode); newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode); newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode); return new NewObjectTransformer().Transform(context, newObjNode);
} }
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li, 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)) if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{ {
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
} }
var sp = context.Configuration.TypeMappings.ServiceProvider; 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( 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) 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 if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType()))) && type.IsAssignableFrom(on.Type.GetClrType())))
return node; return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null) if (keyDirective is null)
return node; 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"); var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), 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 Int { get; }
public IXamlType Long { get; } public IXamlType Long { get; }
public IXamlType Uri { get; } public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; } public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; } public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; } public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32"); Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64"); Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri"); Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily"); FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String }); FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant"); 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") })); .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)); using var dependsOnPropertiesArray = context.GetLocalOfType(context.Configuration.WellKnownTypes.String.MakeArrayType(1));
codeGen 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) 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) if (msg == WM_TASKBARCREATED)

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

Loading…
Cancel
Save