Browse Source

Merge pull request #4 from AvaloniaUI/master

update the thing lol
pull/5561/head
Splitwirez 6 years ago
committed by GitHub
parent
commit
680744dc59
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 159
      Avalonia.sln
  2. 16
      azure-pipelines.yml
  3. 2
      build/ApiDiff.props
  4. 4
      build/HarfBuzzSharp.props
  5. 4
      build/SkiaSharp.props
  6. 5
      build/iOSWorkarounds.props
  7. 3
      dirs.proj
  8. 4
      global.json
  9. 2
      native/Avalonia.Native/src/OSX/rendertarget.mm
  10. 60
      native/Avalonia.Native/src/OSX/window.mm
  11. 41
      nukebuild/Build.cs
  12. 1
      nukebuild/_build.csproj
  13. 6
      readme.md
  14. 1
      samples/BindingDemo/MainWindow.xaml
  15. 17
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  16. 23
      samples/ControlCatalog.iOS/AppDelegate.cs
  17. 14
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  18. 3
      samples/ControlCatalog/App.xaml
  19. 14
      samples/ControlCatalog/App.xaml.cs
  20. 8
      samples/ControlCatalog/Models/GDPValueConverter.cs
  21. 12
      samples/ControlCatalog/Models/Person.cs
  22. 5
      samples/ControlCatalog/Pages/DataGridPage.xaml
  23. 9
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  24. 10
      samples/ControlCatalog/Pages/SplitViewPage.xaml
  25. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  26. 15
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  27. 34
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  28. 11
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  29. 6
      src/Avalonia.Base/ApiCompatBaseline.txt
  30. 41
      src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs
  31. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  32. 230
      src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
  33. 2
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  34. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  35. 74
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  36. 13
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  37. 17
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  38. 2
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  39. 64
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  40. 22
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  41. 25
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  42. 2
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  43. 4
      src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs
  44. 23
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  45. 645
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  46. 23
      src/Avalonia.Controls/ApiCompatBaseline.txt
  47. 17
      src/Avalonia.Controls/AppBuilderBase.cs
  48. 3
      src/Avalonia.Controls/Avalonia.Controls.csproj
  49. 8
      src/Avalonia.Controls/ContextMenu.cs
  50. 249
      src/Avalonia.Controls/ISelectionModel.cs
  51. 200
      src/Avalonia.Controls/IndexPath.cs
  52. 30
      src/Avalonia.Controls/ItemsControl.cs
  53. 120
      src/Avalonia.Controls/ItemsSourceView.cs
  54. 7
      src/Avalonia.Controls/ListBox.cs
  55. 22
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  56. 553
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  57. 49
      src/Avalonia.Controls/SelectedItems.cs
  58. 66
      src/Avalonia.Controls/Selection/ISelectionModel.cs
  59. 82
      src/Avalonia.Controls/Selection/IndexRange.cs
  60. 82
      src/Avalonia.Controls/Selection/SelectedIndexes.cs
  61. 121
      src/Avalonia.Controls/Selection/SelectedItems.cs
  62. 749
      src/Avalonia.Controls/Selection/SelectionModel.cs
  63. 18
      src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs
  64. 85
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  65. 320
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  66. 894
      src/Avalonia.Controls/SelectionModel.cs
  67. 170
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  68. 103
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  69. 47
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  70. 971
      src/Avalonia.Controls/SelectionNode.cs
  71. 110
      src/Avalonia.Controls/SelectionNodeOperation.cs
  72. 62
      src/Avalonia.Controls/SplitView.cs
  73. 131
      src/Avalonia.Controls/TextBox.cs
  74. 2
      src/Avalonia.Controls/ToolTip.cs
  75. 7
      src/Avalonia.Controls/ToolTipService.cs
  76. 669
      src/Avalonia.Controls/TreeView.cs
  77. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  78. 156
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  79. 211
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  80. 189
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
  81. 31
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  82. 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  83. 21
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  84. 105
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  85. 45
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  86. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  87. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  88. 71
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  89. 7
      src/Avalonia.Input/Gestures.cs
  90. 3
      src/Avalonia.Native/Avalonia.Native.csproj
  91. 17
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  92. 628
      src/Avalonia.Native/Generated/Enumerations.cs
  93. 5
      src/Avalonia.Native/Generated/Functions.cs
  94. 3092
      src/Avalonia.Native/Generated/Interfaces.cs
  95. 202
      src/Avalonia.Native/Generated/LocalInterop.cs
  96. 246
      src/Avalonia.Native/Generated/Structures.cs
  97. 27
      src/Avalonia.Native/ScreenImpl.cs
  98. 15
      src/Avalonia.Native/WindowImplBase.cs
  99. 12
      src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj
  100. 2
      src/Avalonia.Themes.Default/Expander.xaml

159
Avalonia.sln

@ -87,8 +87,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CF
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.iOSTestApplication", "src\iOS\Avalonia.iOSTestApplication\Avalonia.iOSTestApplication.csproj", "{8C923867-8A8F-4F6B-8B80-47D9E8436166}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LeakTests", "tests\Avalonia.LeakTests\Avalonia.LeakTests.csproj", "{E1AA3DBF-9056-4530-9376-18119A7A3FFE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.UnitTests", "tests\Avalonia.UnitTests\Avalonia.UnitTests.csproj", "{88060192-33D5-4932-B0F9-8BD2763E857D}"
@ -124,6 +122,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
build\AndroidWorkarounds.props = build\AndroidWorkarounds.props
build\ApiDiff.props = build\ApiDiff.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
build\CoreLibraries.props = build\CoreLibraries.props
@ -150,14 +149,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\System.Memory.props = build\System.Memory.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\XUnit.props = build\XUnit.props
build\ApiDiff.props = build\ApiDiff.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
ProjectSection(SolutionItems) = preProject
build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
build\BuildTargets.targets = build\BuildTargets.targets
build\LegacyProject.targets = build\LegacyProject.targets
build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}"
@ -218,17 +216,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "sample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
@ -951,26 +952,6 @@ Global
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Release|iPhone.Build.0 = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.AppStore|iPhone.Build.0 = AppStore|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Debug|Any CPU.ActiveCfg = Debug|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Debug|iPhone.ActiveCfg = Debug|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Debug|iPhone.Build.0 = Debug|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Release|Any CPU.ActiveCfg = Release|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Release|iPhone.ActiveCfg = Release|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Release|iPhone.Build.0 = Release|iPhone
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{8C923867-8A8F-4F6B-8B80-47D9E8436166}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@ -1843,54 +1824,6 @@ Global
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -2011,6 +1944,54 @@ Global
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@ -2035,6 +2016,30 @@ Global
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -2061,7 +2066,6 @@ Global
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
{8C923867-8A8F-4F6B-8B80-47D9E8436166} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
{E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
@ -2094,6 +2098,7 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

16
azure-pipelines.yml

@ -35,16 +35,9 @@ jobs:
vmImage: 'macOS-10.14'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.101'
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
packageType: sdk
version: 3.1.101
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 3.1.1'
inputs:
packageType: runtime
version: 3.1.1
version: 3.1.401
- task: CmdLine@2
displayName: 'Install Mono 5.18'
@ -112,6 +105,11 @@ jobs:
pool:
vmImage: 'windows-2019'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
version: 3.1.401
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:

2
build/ApiDiff.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApiContractPackageVersion>0.10.0-preview2</ApiContractPackageVersion>
<ApiContractPackageVersion>0.10.0-preview3</ApiContractPackageVersion>
<NugetPackageName Condition="'$(PackageId)' != ''">$(PackageId)</NugetPackageName>
<NugetPackageName Condition="'$(PackageId)' == ''">Avalonia</NugetPackageName>
</PropertyGroup>

4
build/HarfBuzzSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.5" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.5" />
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.6" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.6" />
</ItemGroup>
</Project>

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.0" />
<PackageReference Include="SkiaSharp" Version="2.80.2-preview.33" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2-preview.33" />
</ItemGroup>
</Project>

5
build/iOSWorkarounds.props

@ -1,5 +0,0 @@
<Project>
<PropertyGroup Condition="'$(iOSRoslynPathHackRequired)' == 'true'">
<CscToolPath>$(MSBuildToolsPath)\..\Roslyn</CscToolPath>
</PropertyGroup>
</Project>

3
dirs.proj

@ -7,13 +7,14 @@
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<ProjectReference Remove="tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj" />
</ItemGroup>
<!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
<ItemGroup>
<ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.Android/ControlCatalog.Android.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\iOS') Or $([MSBuild]::IsOsPlatform('Windows')) ">
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\iOS')">
<ProjectReference Remove="src/iOS/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj" />
</ItemGroup>

4
global.json

@ -1,10 +1,10 @@
{
"sdk": {
"version": "3.1.101"
"version": "3.1.401"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "2.0.46",
"MSBuild.Sdk.Extras": "2.0.54",
"AggregatePackage.NuGet.Sdk" : "0.1.12"
}
}

2
native/Avalonia.Native/src/OSX/rendertarget.mm

@ -110,7 +110,7 @@
if(_renderbuffer != 0)
glDeleteRenderbuffers(1, &_renderbuffer);
}
IOSurfaceDecrementUseCount(surface);
CFRelease(surface);
}
@end

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

@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _queuedDisplayFromThread;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
AvnPixelSize _lastPixelSize;
@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
_modifierState = AvnInputModifiersNone;
return self;
}
@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result;
}
- (void)flagsChanged:(NSEvent *)event
{
auto newModifierState = [self getModifiers:[event modifierFlags]];
bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt;
bool isControlCurrentlyPressed = (_modifierState & Control) == Control;
bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift;
bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows;
bool isAltPressed = (newModifierState & Alt) == Alt;
bool isControlPressed = (newModifierState & Control) == Control;
bool isShiftPressed = (newModifierState & Shift) == Shift;
bool isCommandPressed = (newModifierState & Windows) == Windows;
if (isAltPressed && !isAltCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isAltCurrentlyPressed && !isAltPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isControlPressed && !isControlCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isControlCurrentlyPressed && !isControlPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isShiftPressed && !isShiftCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isShiftCurrentlyPressed && !isShiftPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if(isCommandPressed && !isCommandCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isCommandCurrentlyPressed && ! isCommandPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
_modifierState = newModifierState;
[[self inputContext] handleEvent:event];
[super flagsChanged:event];
}
- (void)keyDown:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyDown];

41
nukebuild/Build.cs

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Nuke.Common;
using Nuke.Common.Git;
@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
@ -139,7 +141,7 @@ partial class Build : NukeBuild
Target Compile => _ => _
.DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer)
.Executes(() =>
.Executes(async () =>
{
if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c
@ -153,8 +155,44 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
await CompileReactiveEvents();
});
async Task CompileReactiveEvents()
{
var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration);
var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll")
.Where(file => !file.Contains("Avalonia.Build.Tasks") &&
!file.Contains("Avalonia.Remote.Protocol"));
var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First();
var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs");
if (File.Exists(eventsBuildFile))
File.Delete(eventsBuildFile);
using (var stream = File.Create(eventsBuildFile))
using (var writer = new StreamWriter(stream))
{
await ObservablesForEventGenerator.ExtractEventsFromAssemblies(
writer, avaloniaAssemblies, new string[0], "netstandard2.0"
);
}
var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj");
if (Parameters.IsRunningOnWindows)
MsBuildCommon(eventsProject, c => c
.SetArgumentConfigurator(a => a.Add("/r"))
.AddTargets("Build")
);
else
DotNetBuild(c => c
.SetProjectFile(eventsProject)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
}
void RunCoreTest(string projectName)
{
Information($"Running tests from {projectName}");
@ -202,6 +240,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests");
});
Target RunRenderTests => _ => _

1
nukebuild/_build.csproj

@ -17,6 +17,7 @@
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" />
<PackageReference Include="Pharmacist.Core" Version="1.8.1" />
</ItemGroup>
<ItemGroup>

6
readme.md

@ -16,7 +16,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 Getting Started
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://avaloniaui.net/docs/quickstart/create-new-project).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@ -47,7 +47,7 @@ We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using
## Documentation
Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers.
Documentation can be found on our website at https://avaloniaui.net/docs/. We also have a [tutorial](https://avaloniaui.net/docs/tutorial/) over there for newcomers.
## Building and Using
@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)].
This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers

1
samples/BindingDemo/MainWindow.xaml

@ -116,6 +116,7 @@
<RadioButton Content="Radio Button" IsChecked="{Binding !!BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="RadioButton"/>
<TextBox Text="{Binding Path=StringValue}"/>
<Button Content="Nested View Model Button" Name="NestedTest" Command="{Binding NestedModel.Command}" />
<Button Content="Command Method Do" Command="{Binding Do}" x:Name="ToDo"/>
</StackPanel>
</TabItem>
</TabControl>

17
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,8 @@ using System.Threading.Tasks;
using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Controls.Selection;
namespace BindingDemo.ViewModels
{
@ -28,7 +30,7 @@ namespace BindingDemo.ViewModels
Detail = "Item " + x + " details",
}));
Selection = new SelectionModel();
Selection = new SelectionModel<TestItem> { SingleSelect = false };
ShuffleItems = ReactiveCommand.Create(() =>
{
@ -57,7 +59,7 @@ namespace BindingDemo.ViewModels
}
public ObservableCollection<TestItem> Items { get; }
public SelectionModel Selection { get; }
public SelectionModel<TestItem> Selection { get; }
public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
public string BooleanString
@ -102,5 +104,16 @@ namespace BindingDemo.ViewModels
get { return _nested; }
private set { this.RaiseAndSetIfChanged(ref _nested, value); }
}
public void Do(object parameter)
{
}
[DependsOn(nameof(BooleanFlag))]
bool CanDo(object parameter)
{
return BooleanFlag;
}
}
}

23
samples/ControlCatalog.iOS/AppDelegate.cs

@ -11,25 +11,8 @@ namespace ControlCatalog
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
public override UIWindow Window { get; set; }
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching(UIApplication uiapp, NSDictionary options)
{
AppBuilder.Configure<App>()
.UseiOS()
.UseSkia().SetupWithoutStarting();
Window = new AvaloniaWindow() {Content = new MainView(), StatusBarColor = Colors.LightSteelBlue};
Window.MakeKeyAndVisible();
return true;
}
}
}
}

14
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -9,6 +9,10 @@
<RootNamespace>ControlCatalog.iOS</RootNamespace>
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
<AssemblyName>ControlCatalogiOS</AssemblyName>
<MtouchEnableSGenConc>true</MtouchEnableSGenConc>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<ProvisioningType>automatic</ProvisioningType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols>
@ -19,8 +23,8 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
<MtouchArch>i386</MtouchArch>
<MtouchLink>SdkOnly</MtouchLink>
<MtouchArch>x86_64</MtouchArch>
<MtouchLink>None</MtouchLink>
<MtouchDebug>True</MtouchDebug>
<MtouchSdkVersion>9.1</MtouchSdkVersion>
<MtouchProfiling>False</MtouchProfiling>
@ -43,7 +47,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchLink>None</MtouchLink>
<MtouchArch>i386</MtouchArch>
<MtouchArch>x86_64</MtouchArch>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
@ -173,8 +177,10 @@
<Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
<Name>ControlCatalog</Name>
</ProjectReference>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.2-preview.33" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Import Project="..\..\build\iOSWorkarounds.props" />
<Import Project="..\..\build\LegacyProject.targets" />
<Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
</Project>

3
samples/ControlCatalog/App.xaml

@ -1,8 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
<Application.Styles>
<Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Medium" />

14
samples/ControlCatalog/App.xaml.cs

@ -9,12 +9,23 @@ namespace ControlCatalog
{
public class App : Application
{
private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
};
private static readonly StyleInclude DataGridDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Default.xaml")
};
public static Styles FluentDark = new Styles
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml")
},
DataGridFluent
};
public static Styles FluentLight = new Styles
@ -23,6 +34,7 @@ namespace ControlCatalog
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentLight.xaml")
},
DataGridFluent
};
public static Styles DefaultLight = new Styles
@ -43,6 +55,7 @@ namespace ControlCatalog
{
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
},
DataGridDefault
};
public static Styles DefaultDark = new Styles
@ -63,6 +76,7 @@ namespace ControlCatalog
{
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
},
DataGridDefault
};
public override void Initialize()

8
samples/ControlCatalog/Models/GDPValueConverter.cs

@ -19,11 +19,11 @@ namespace ControlCatalog.Models
if (value is int gdp)
{
if (gdp <= 5000)
return Brushes.Orange;
return new SolidColorBrush(Colors.Orange, 0.6);
else if (gdp <= 10000)
return Brushes.Yellow;
return new SolidColorBrush(Colors.Yellow, 0.6);
else
return Brushes.LightGreen;
return new SolidColorBrush(Colors.LightGreen, 0.6);
}
return value;
@ -34,4 +34,4 @@ namespace ControlCatalog.Models
throw new NotImplementedException();
}
}
}
}

12
samples/ControlCatalog/Models/Person.cs

@ -15,6 +15,7 @@ namespace ControlCatalog.Models
{
string _firstName;
string _lastName;
bool _isBanned;
public string FirstName
{
@ -47,6 +48,17 @@ namespace ControlCatalog.Models
}
}
public bool IsBanned
{
get => _isBanned;
set
{
_isBanned = value;
OnPropertyChanged(nameof(_isBanned));
}
}
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error)

5
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -18,7 +18,7 @@
</StackPanel>
<TabControl Grid.Row="1">
<TabItem Header="DataGrid">
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True">
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" />
<DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" />
@ -44,7 +44,8 @@
<DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="*" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />

9
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -13,7 +13,7 @@ namespace ControlCatalog.Pages
this.InitializeComponent();
var dg1 = this.FindControl<DataGrid>("dataGrid1");
dg1.IsReadOnly = true;
dg1.LoadingRow += Dg1_LoadingRow;
var collectionView1 = new DataGridCollectionView(Countries.All);
//collectionView.GroupDescriptions.Add(new PathGroupDescription("Region"));
@ -33,7 +33,7 @@ namespace ControlCatalog.Pages
var items = new List<Person>
{
new Person { FirstName = "John", LastName = "Doe" },
new Person { FirstName = "Elizabeth", LastName = "Thomas" },
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true },
new Person { FirstName = "Zack", LastName = "Ward" }
};
var collectionView3 = new DataGridCollectionView(items);
@ -44,6 +44,11 @@ namespace ControlCatalog.Pages
addButton.Click += (a, b) => collectionView3.AddNew();
}
private void Dg1_LoadingRow(object sender, DataGridRowEventArgs e)
{
e.Row.Header = e.Row.GetIndex() + 1;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);

10
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -58,12 +58,18 @@
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" />
<ListBoxItem Grid.Row="1" VerticalAlignment="Top" Margin="0 10">
<ComboBox Width="150" Grid.Row="1">
<ComboBoxItem Content="Item1"/>
<ComboBoxItem Content="Item2"/>
<ComboBoxItem Content="Item3"/>
</ComboBox>
<ListBoxItem Grid.Row="2" VerticalAlignment="Top" Margin="0 10">
<StackPanel Orientation="Horizontal">
<!--Path glyph from materialdesignicons.com-->
<Border Width="48">
@ -76,7 +82,7 @@
<TextBlock Text="People" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" />
<TextBlock Grid.Row="3" Text="Item at bottom" Margin="60,12" />
</Grid>
</SplitView.Pane>

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" Selection="{Binding Selection}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>

15
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using ReactiveUI;
namespace ControlCatalog.ViewModels
@ -15,16 +16,16 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel();
Selection = new SelectionModel<string>();
Selection.Select(1);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (Selection.SelectedItems.Count > 0)
while (Selection.Count > 0)
{
Items.Remove((string)Selection.SelectedItems.First());
Items.Remove(Selection.SelectedItems.First());
}
});
@ -32,9 +33,9 @@ namespace ControlCatalog.ViewModels
{
var random = new Random();
using (Selection.Update())
using (Selection.BatchUpdate())
{
Selection.ClearSelection();
Selection.Clear();
Selection.Select(random.Next(Items.Count - 1));
}
});
@ -42,7 +43,7 @@ namespace ControlCatalog.ViewModels
public ObservableCollection<string> Items { get; }
public SelectionModel Selection { get; }
public SelectionModel<string> Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
@ -55,7 +56,7 @@ namespace ControlCatalog.ViewModels
get => _selectionMode;
set
{
Selection.ClearSelection();
Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}

34
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
@ -18,8 +17,7 @@ namespace ControlCatalog.ViewModels
_root = new Node();
Items = _root.Children;
Selection = new SelectionModel();
Selection.SelectionChanged += SelectionChanged;
SelectedItems = new ObservableCollection<Node>();
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
@ -27,7 +25,7 @@ namespace ControlCatalog.ViewModels
}
public ObservableCollection<Node> Items { get; }
public SelectionModel Selection { get; }
public ObservableCollection<Node> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
@ -37,24 +35,24 @@ namespace ControlCatalog.ViewModels
get => _selectionMode;
set
{
Selection.ClearSelection();
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private void AddItem()
{
var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root;
var parentItem = SelectedItems.Count > 0 ? (Node)SelectedItems[0] : _root;
parentItem.AddItem();
}
private void RemoveItem()
{
while (Selection.SelectedItems.Count > 0)
while (SelectedItems.Count > 0)
{
Node lastItem = (Node)Selection.SelectedItems[0];
Node lastItem = (Node)SelectedItems[0];
RecursiveRemove(Items, lastItem);
Selection.DeselectAt(Selection.SelectedIndices[0]);
SelectedItems.RemoveAt(0);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
@ -80,16 +78,16 @@ namespace ControlCatalog.ViewModels
{
var random = new Random();
var depth = random.Next(4);
var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10));
var path = new IndexPath(indexes);
Selection.SelectedIndex = path;
}
var indexes = Enumerable.Range(0, depth).Select(x => random.Next(10));
var node = _root;
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
var selected = string.Join(",", e.SelectedIndices);
var deselected = string.Join(",", e.DeselectedIndices);
System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'");
foreach (var i in indexes)
{
node = node.Children[i];
}
SelectedItems.Clear();
SelectedItems.Add(node);
}
public class Node

11
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI;
using Avalonia.Layout;
using Avalonia.Controls.Selection;
namespace VirtualizationDemo.ViewModels
{
@ -48,7 +49,7 @@ namespace VirtualizationDemo.ViewModels
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public SelectionModel Selection { get; } = new SelectionModel();
public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
public AvaloniaList<ItemViewModel> Items
{
@ -137,9 +138,9 @@ namespace VirtualizationDemo.ViewModels
{
var index = Items.Count;
if (Selection.SelectedIndices.Count > 0)
if (Selection.SelectedItems.Count > 0)
{
index = Selection.SelectedIndex.GetAt(0);
index = Selection.SelectedIndex;
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@ -149,7 +150,7 @@ namespace VirtualizationDemo.ViewModels
{
if (Selection.SelectedItems.Count > 0)
{
Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
Items.RemoveAll(Selection.SelectedItems.ToList());
}
}
@ -163,7 +164,7 @@ namespace VirtualizationDemo.ViewModels
private void SelectItem(int index)
{
Selection.SelectedIndex = new IndexPath(index);
Selection.SelectedIndex = index;
}
}
}

6
src/Avalonia.Base/ApiCompatBaseline.txt

@ -1,6 +0,0 @@
Compat issues with assembly Avalonia.Base:
MembersMustExist : Member 'public void Avalonia.DirectProperty<TOwner, TValue>..ctor(System.String, System.Func<TOwner, TValue>, System.Action<TOwner, TValue>, Avalonia.DirectPropertyMetadata<TValue>, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.DirectPropertyBase<TValue>..ctor(Avalonia.AvaloniaProperty, System.Type, Avalonia.PropertyMetadata, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.DirectPropertyBase<TValue>..ctor(System.String, System.Type, Avalonia.PropertyMetadata, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Boolean Avalonia.DirectPropertyBase<TValue>.IsDataValidationEnabled.get()' does not exist in the implementation but it does exist in the contract.
Total Issues: 4

41
src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs

@ -1,41 +0,0 @@
using System;
using System.Globalization;
using System.Reflection;
using System.Windows.Input;
using Avalonia.Utilities;
namespace Avalonia.Data.Converters
{
class AlwaysEnabledDelegateCommand : ICommand
{
private readonly Delegate action;
private ParameterInfo parameterInfo;
public AlwaysEnabledDelegateCommand(Delegate action)
{
this.action = action;
var parameters = action.Method.GetParameters();
parameterInfo = parameters.Length == 0 ? null : parameters[0];
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public bool CanExecute(object parameter) => true;
public void Execute(object parameter)
{
if (parameterInfo == null)
{
action.DynamicInvoke();
}
else
{
TypeUtilities.TryConvert(parameterInfo.ParameterType, parameter, CultureInfo.CurrentCulture, out object convertedParameter);
action.DynamicInvoke(convertedParameter);
}
}
}
}

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -33,7 +33,7 @@ namespace Avalonia.Data.Converters
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
{
return new AlwaysEnabledDelegateCommand(d);
return new MethodToCommandConverter(d);
}
if (TypeUtilities.TryConvert(targetType, value, culture, out object result))

230
src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs

@ -0,0 +1,230 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Input;
using Avalonia.Utilities;
namespace Avalonia.Data.Converters
{
class MethodToCommandConverter : ICommand
{
readonly static Func<object, bool> AlwaysEnabled = (_) => true;
readonly static MethodInfo tryConvert = typeof(TypeUtilities)
.GetMethod(nameof(TypeUtilities.TryConvert), BindingFlags.Public | BindingFlags.Static);
readonly static PropertyInfo currentCulture = typeof(CultureInfo)
.GetProperty(nameof(CultureInfo.CurrentCulture), BindingFlags.Public | BindingFlags.Static);
readonly Func<object, bool> canExecute;
readonly Action<object> execute;
readonly WeakPropertyChangedProxy weakPropertyChanged;
readonly PropertyChangedEventHandler propertyChangedEventHandler;
readonly string[] dependencyProperties;
public MethodToCommandConverter(Delegate action)
{
var target = action.Target;
var canExecuteMethodName = "Can" + action.Method.Name;
var parameters = action.Method.GetParameters();
var parameterInfo = parameters.Length == 0 ? null : parameters[0].ParameterType;
if (parameterInfo == null)
{
execute = CreateExecute(target, action.Method);
}
else
{
execute = CreateExecute(target, action.Method, parameterInfo);
}
var canExecuteMethod = action.Method.DeclaringType.GetRuntimeMethods()
.FirstOrDefault(m => m.Name == canExecuteMethodName
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(object));
if (canExecuteMethod == null)
{
canExecute = AlwaysEnabled;
}
else
{
canExecute = CreateCanExecute(target, canExecuteMethod);
dependencyProperties = canExecuteMethod
.GetCustomAttributes(typeof(Metadata.DependsOnAttribute), true)
.OfType<Metadata.DependsOnAttribute>()
.Select(x => x.Name)
.ToArray();
if (dependencyProperties.Any()
&& target is INotifyPropertyChanged inpc)
{
propertyChangedEventHandler = OnPropertyChanged;
weakPropertyChanged = new WeakPropertyChangedProxy(inpc, propertyChangedEventHandler);
}
}
}
void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
{
Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)
, Threading.DispatcherPriority.Input);
}
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public bool CanExecute(object parameter) => canExecute(parameter);
public void Execute(object parameter) => execute(parameter);
static Action<object> CreateExecute(object target
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
var call = Expression.Call
(
instance,
method
);
return Expression
.Lambda<Action<object>>(call, parameter)
.Compile();
}
static Action<object> CreateExecute(object target
, System.Reflection.MethodInfo method
, Type parameterType)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
Expression body;
if (parameterType == typeof(object))
{
body = Expression.Call(instance,
method,
parameter
);
}
else
{
var arg0 = Expression.Variable(typeof(object), "argX");
var convertCall = Expression.Call(tryConvert,
Expression.Constant(parameterType),
parameter,
Expression.Property(null, currentCulture),
arg0
);
var call = Expression.Call(instance,
method,
Expression.Convert(arg0, parameterType)
);
body = Expression.Block(new[] { arg0 },
convertCall,
call
);
}
Action<object> action = null;
try
{
action = Expression
.Lambda<Action<object>>(body, parameter)
.Compile();
}
catch (Exception ex)
{
throw ex;
}
return action;
}
static Func<object, bool> CreateCanExecute(object target
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
var call = Expression.Call
(
instance,
method,
parameter
);
return Expression
.Lambda<Func<object, bool>>(call, parameter)
.Compile();
}
internal class WeakPropertyChangedProxy
{
readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null);
readonly PropertyChangedEventHandler _handler;
internal WeakReference<INotifyPropertyChanged> Source { get; } = new WeakReference<INotifyPropertyChanged>(null);
public WeakPropertyChangedProxy()
{
_handler = new PropertyChangedEventHandler(OnPropertyChanged);
}
public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this()
{
SubscribeTo(source, listener);
}
public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
{
source.PropertyChanged += _handler;
Source.SetTarget(source);
_listener.SetTarget(listener);
}
public void Unsubscribe()
{
if (Source.TryGetTarget(out INotifyPropertyChanged source) && source != null)
source.PropertyChanged -= _handler;
Source.SetTarget(null);
_listener.SetTarget(null);
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_listener.TryGetTarget(out var handler) && handler != null)
handler(sender, e);
else
Unsubscribe();
}
}
}
}

2
src/Avalonia.Base/Metadata/DependsOnAttribute.cs

@ -5,7 +5,7 @@ namespace Avalonia.Metadata
/// <summary>
/// Indicates that the property depends on the value of another property in markup.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
/// <summary>

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

@ -9,3 +9,4 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]

74
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -39,6 +39,7 @@ namespace Avalonia.Controls
private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter";
private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader";
private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader";
private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner";
private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary";
private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar";
@ -79,6 +80,7 @@ namespace Avalonia.Controls
private INotifyCollectionChanged _topLevelGroup;
private ContentControl _clipboardContentControl;
private IVisual _bottomRightCorner;
private DataGridColumnHeadersPresenter _columnHeadersPresenter;
private DataGridRowsPresenter _rowsPresenter;
private ScrollBar _vScrollBar;
@ -1825,6 +1827,22 @@ namespace Avalonia.Controls
}
}
private bool IsHorizontalScrollBarOverCells
{
get
{
return _columnHeadersPresenter != null && Grid.GetColumnSpan(_columnHeadersPresenter) == 2;
}
}
private bool IsVerticalScrollBarOverCells
{
get
{
return _rowsPresenter != null && Grid.GetRowSpan(_rowsPresenter) == 2;
}
}
private int NoSelectionChangeCount
{
get
@ -2323,6 +2341,7 @@ namespace Avalonia.Controls
_topLeftCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopLeftCornerHeaderName);
EnsureTopLeftCornerHeader(); // EnsureTopLeftCornerHeader checks for a null _topLeftCornerHeader;
_topRightCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopRightCornerHeaderName);
_bottomRightCorner = e.NameScope.Find<IVisual>(DATAGRID_elementBottomRightCornerHeaderName);
}
/// <summary>
@ -2753,7 +2772,7 @@ namespace Avalonia.Controls
//We don't need to refresh the state of AutoGenerated column headers because they're up-to-date
if (!column.IsAutoGenerated && column.HasHeaderCell)
{
column.HeaderCell.ApplyState();
column.HeaderCell.UpdatePseudoClasses();
}
}
@ -3293,6 +3312,10 @@ namespace Avalonia.Controls
//
}
bool isHorizontalScrollBarOverCells = IsHorizontalScrollBarOverCells;
bool isVerticalScrollBarOverCells = IsVerticalScrollBarOverCells;
double cellsWidth = CellsWidth;
double cellsHeight = CellsHeight;
@ -3308,10 +3331,17 @@ namespace Avalonia.Controls
// Compensate if the horizontal scrollbar is already taking up space
if (!forceHorizScrollbar && _hScrollBar.IsVisible)
{
cellsHeight += _hScrollBar.DesiredSize.Height;
if (!isHorizontalScrollBarOverCells)
{
cellsHeight += _hScrollBar.DesiredSize.Height;
}
}
if (!isHorizontalScrollBarOverCells)
{
horizScrollBarHeight = _hScrollBar.Height + _hScrollBar.Margin.Top + _hScrollBar.Margin.Bottom;
}
horizScrollBarHeight = _hScrollBar.Height + _hScrollBar.Margin.Top + _hScrollBar.Margin.Bottom;
}
bool allowVertScrollbar = false;
bool forceVertScrollbar = false;
double vertScrollBarWidth = 0;
@ -3324,9 +3354,15 @@ namespace Avalonia.Controls
// Compensate if the vertical scrollbar is already taking up space
if (!forceVertScrollbar && _vScrollBar.IsVisible)
{
cellsWidth += _vScrollBar.DesiredSize.Width;
if (!isVerticalScrollBarOverCells)
{
cellsWidth += _vScrollBar.DesiredSize.Width;
}
}
if (!isVerticalScrollBarOverCells)
{
vertScrollBarWidth = _vScrollBar.Width + _vScrollBar.Margin.Left + _vScrollBar.Margin.Right;
}
vertScrollBarWidth = _vScrollBar.Width + _vScrollBar.Margin.Left + _vScrollBar.Margin.Right;
}
// Now cellsWidth is the width potentially available for displaying data cells.
@ -3354,7 +3390,9 @@ namespace Avalonia.Controls
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
if (vertScrollBarWidth > 0 &&
allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
// Would we still need a horizontal scrollbar without the vertical one?
@ -3372,7 +3410,12 @@ namespace Avalonia.Controls
}
}
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
// Store the current FirstScrollingSlot because removing the horizontal scrollbar could scroll
// the DataGrid up; however, if we realize later that we need to keep the horizontal scrollbar
// then we should use the first slot stored here which is not scrolled.
int firstScrollingSlot = DisplayData.FirstScrollingSlot;
UpdateDisplayedRows(firstScrollingSlot, cellsHeight);
if (allowVertScrollbar &&
MathUtilities.GreaterThan(cellsHeight, 0) &&
MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
@ -3384,10 +3427,12 @@ namespace Avalonia.Controls
}
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
// we compute the number of visible columns only after we set up the vertical scroll bar.
ComputeDisplayedColumns();
if (allowHorizScrollbar &&
if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
@ -3398,7 +3443,7 @@ namespace Avalonia.Controls
Debug.Assert(cellsHeight >= 0);
needVertScrollbar = false;
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
UpdateDisplayedRows(firstScrollingSlot, cellsHeight);
if (cellsHeight > 0 &&
vertScrollBarWidth <= cellsWidth &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
@ -3479,6 +3524,15 @@ namespace Avalonia.Controls
_topRightCornerHeader.IsVisible = false;
}
}
if (_bottomRightCorner != null)
{
// Show the BottomRightCorner when both scrollbars are visible.
_bottomRightCorner.IsVisible =
_hScrollBar != null && _hScrollBar.IsVisible &&
_vScrollBar != null && _vScrollBar.IsVisible;
}
DisplayData.FullyRecycleElements();
}
@ -5417,7 +5471,7 @@ namespace Avalonia.Controls
}
else if (displayedElement is DataGridRowGroupHeader groupHeader)
{
groupHeader.ApplyState(useTransitions: true);
groupHeader.UpdatePseudoClasses();
if (AreRowHeadersVisible)
{
groupHeader.ApplyHeaderStatus();

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

@ -19,7 +19,7 @@ namespace Avalonia.Controls
private Rectangle _rightGridLine;
private DataGridColumn _owningColumn;
bool _isValid;
bool _isValid = true;
public static readonly DirectProperty<DataGridCell, bool> IsValidProperty =
AvaloniaProperty.RegisterDirect<DataGridCell, bool>(
@ -180,7 +180,18 @@ namespace Avalonia.Controls
internal void UpdatePseudoClasses()
{
if (OwningGrid == null || OwningColumn == null || OwningRow == null || !OwningRow.IsVisible || OwningRow.Slot == -1)
{
return;
}
PseudoClasses.Set(":selected", OwningRow.IsSelected);
PseudoClasses.Set(":current", IsCurrent);
PseudoClasses.Set(":edited", IsEdited);
PseudoClasses.Set(":invalid", !IsValid);
}
// Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the

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

@ -13,6 +13,7 @@ using System.Diagnostics;
using Avalonia.Utilities;
using System;
using Avalonia.Controls.Utils;
using Avalonia.Controls.Mixins;
namespace Avalonia.Controls
{
@ -39,7 +40,7 @@ namespace Avalonia.Controls
private static Cursor _originalCursor;
private static double _originalHorizontalOffset;
private static double _originalWidth;
private bool _desiredSeparatorVisibility;
private bool _desiredSeparatorVisibility = true;
private static Point? _dragStart;
private static DataGridColumn _dragColumn;
private static double _frozenColumnsWidth;
@ -68,6 +69,7 @@ namespace Avalonia.Controls
static DataGridColumnHeader()
{
AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
PressedMixin.Attach<DataGridColumnHeader>();
}
/// <summary>
@ -149,8 +151,7 @@ namespace Avalonia.Controls
}
}
//TODO Implement
internal void ApplyState()
internal void UpdatePseudoClasses()
{
CurrentSortingState = null;
if (OwningGrid != null
@ -441,7 +442,7 @@ namespace Avalonia.Controls
Point mousePosition = e.GetPosition(this);
OnMouseEnter(mousePosition);
ApplyState();
UpdatePseudoClasses();
}
private void DataGridColumnHeader_PointerLeave(object sender, PointerEventArgs e)
@ -452,12 +453,12 @@ namespace Avalonia.Controls
}
OnMouseLeave();
ApplyState();
UpdatePseudoClasses();
}
private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (OwningColumn == null || e.Handled || !IsEnabled || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningColumn == null || e.Handled || !IsEnabled || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}
@ -467,7 +468,7 @@ namespace Avalonia.Controls
OnMouseLeftButtonDown(ref handled, e, mousePosition);
e.Handled = handled;
ApplyState();
UpdatePseudoClasses();
}
private void DataGridColumnHeader_PointerReleased(object sender, PointerReleasedEventArgs e)
@ -483,7 +484,7 @@ namespace Avalonia.Controls
OnMouseLeftButtonUp(ref handled, e, mousePosition, mousePositionHeaders);
e.Handled = handled;
ApplyState();
UpdatePseudoClasses();
}
private void DataGridColumnHeader_PointerMove(object sender, PointerEventArgs e)

2
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@ -610,7 +610,7 @@ namespace Avalonia.Controls
// refresh sort description
foreach (DataGridColumn column in _owner.ColumnsItemsInternal)
{
column.HeaderCell.ApplyState();
column.HeaderCell.UpdatePseudoClasses();
}
}

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

@ -624,17 +624,17 @@ namespace Avalonia.Controls
{
if (_headerElement != null && OwningGrid.AreRowHeadersVisible)
{
_headerElement.ApplyOwnerStatus();
_headerElement.UpdatePseudoClasses();
}
}
//TODO Implement
internal void UpdatePseudoClasses()
{
PseudoClasses.Set(":selected", IsSelected);
PseudoClasses.Set(":editing", IsEditing);
if (RootElement != null && OwningGrid != null && IsVisible)
{
PseudoClasses.Set(":selected", IsSelected);
PseudoClasses.Set(":editing", IsEditing);
PseudoClasses.Set(":invalid", !IsValid);
ApplyHeaderStatus();
}
}
@ -789,7 +789,7 @@ namespace Avalonia.Controls
private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}
@ -917,23 +917,23 @@ namespace Avalonia.Controls
//TODO Cleanup
double? _previousDetailsHeight = null;
//TODO Animation
private void DetailsContent_SizeChanged(Rect newValue)
private void DetailsContent_HeightChanged(double newValue)
{
if (_previousDetailsHeight.HasValue)
{
var oldValue = _previousDetailsHeight.Value;
_previousDetailsHeight = newValue.Height;
if (newValue.Height != oldValue && newValue.Height != _detailsDesiredHeight)
_previousDetailsHeight = newValue;
if (newValue != oldValue && newValue != _detailsDesiredHeight)
{
if (AreDetailsVisible && _appliedDetailsTemplate != null)
{
// Update the new desired height for RowDetails
_detailsDesiredHeight = newValue.Height;
_detailsDesiredHeight = newValue;
_detailsElement.ContentHeight = newValue.Height;
_detailsElement.ContentHeight = newValue;
// Calling this when details are not visible invalidates during layout when we have no work
// to do. In certain scenarios, this could cause a layout cycle
@ -943,19 +943,29 @@ namespace Avalonia.Controls
}
else
{
_previousDetailsHeight = newValue.Height;
_previousDetailsHeight = newValue;
}
}
private void DetailsContent_BoundsChanged(Rect newValue)
private void DetailsContent_SizeChanged(Rect newValue)
{
if(_detailsContent != null)
DetailsContent_SizeChanged(newValue.Inflate(_detailsContent.Margin));
DetailsContent_HeightChanged(newValue.Height);
}
private void DetailsContent_MarginChanged(Thickness newValue)
{
if (_detailsContent != null)
DetailsContent_SizeChanged(_detailsContent.Bounds.Inflate(newValue));
}
private void DetailsContent_LayoutUpdated(object sender, EventArgs e)
{
if (_detailsContent != null)
{
var margin = _detailsContent.Margin;
var height = _detailsContent.DesiredSize.Height + margin.Top + margin.Bottom;
DetailsContent_HeightChanged(height);
}
}
//TODO Animation
// Sets AreDetailsVisible on the row and animates if necessary
@ -1035,12 +1045,26 @@ namespace Avalonia.Controls
if (_detailsContent != null)
{
_detailsContentSizeSubscription =
System.Reactive.Disposables.StableCompositeDisposable.Create(
_detailsContent.GetObservable(BoundsProperty)
.Subscribe(DetailsContent_BoundsChanged),
if (_detailsContent is Layout.Layoutable layoutableContent)
{
layoutableContent.LayoutUpdated += DetailsContent_LayoutUpdated;
_detailsContentSizeSubscription =
System.Reactive.Disposables.StableCompositeDisposable.Create(
System.Reactive.Disposables.Disposable.Create(() => layoutableContent.LayoutUpdated -= DetailsContent_LayoutUpdated),
_detailsContent.GetObservable(MarginProperty)
.Subscribe(DetailsContent_MarginChanged));
}
else
{
_detailsContentSizeSubscription =
_detailsContent.GetObservable(MarginProperty)
.Subscribe(DetailsContent_MarginChanged));
.Subscribe(DetailsContent_MarginChanged);
}
_detailsElement.Children.Add(_detailsContent);
}
}

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

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Media;
@ -96,6 +97,7 @@ namespace Avalonia.Controls
static DataGridRowGroupHeader()
{
SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>((x,e) => x.OnSublevelIndentChanged(e));
PressedMixin.Attach<DataGridRowGroupHeader>();
}
/// <summary>
@ -205,14 +207,18 @@ namespace Avalonia.Controls
{
if (_headerElement != null && OwningGrid.AreRowHeadersVisible)
{
_headerElement.ApplyOwnerStatus();
_headerElement.UpdatePseudoClasses();
}
}
//TODO Implement
internal void ApplyState(bool useTransitions)
internal void UpdatePseudoClasses()
{
PseudoClasses.Set(":current", IsCurrent);
if (RowGroupInfo?.CollectionViewGroup != null)
{
PseudoClasses.Set(":expanded", RowGroupInfo.IsVisible && RowGroupInfo.CollectionViewGroup.ItemCount > 0);
}
}
protected override Size ArrangeOverride(Size finalSize)
@ -328,7 +334,7 @@ namespace Avalonia.Controls
{
if (_headerElement != null && OwningGrid != null)
{
_headerElement.IsVisible = OwningGrid.AreColumnHeadersVisible;
_headerElement.IsVisible = OwningGrid.AreRowHeadersVisible;
}
}
@ -344,7 +350,7 @@ namespace Avalonia.Controls
{
EnsureExpanderButtonIsChecked();
EnsureHeaderVisibility();
ApplyState(useTransitions: false);
UpdatePseudoClasses();
ApplyHeaderStatus();
}
@ -353,7 +359,7 @@ namespace Avalonia.Controls
if (IsEnabled)
{
IsMouseOver = true;
ApplyState(useTransitions: true);
UpdatePseudoClasses();
}
base.OnPointerEnter(e);
@ -364,7 +370,7 @@ namespace Avalonia.Controls
if (IsEnabled)
{
IsMouseOver = false;
ApplyState(useTransitions: true);
UpdatePseudoClasses();
}
base.OnPointerLeave(e);
@ -402,7 +408,7 @@ namespace Avalonia.Controls
EnsureExpanderButtonIsChecked();
ApplyState(true /*useTransitions*/);
UpdatePseudoClasses();
}
}

25
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public class DataGridRowHeader : ContentControl
{
private const string DATAGRIDROWHEADER_elementRootName = "Root";
private const string DATAGRIDROWHEADER_elementRootName = "PART_Root";
private const double DATAGRIDROWHEADER_separatorThickness = 1;
private Control _rootElement;
@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives
_rootElement = e.NameScope.Find<Control>(DATAGRIDROWHEADER_elementRootName);
if (_rootElement != null)
{
ApplyOwnerStatus();
UpdatePseudoClasses();
}
}
@ -131,12 +131,27 @@ namespace Avalonia.Controls.Primitives
return measuredSize;
}
//TODO Implement
internal void ApplyOwnerStatus()
internal void UpdatePseudoClasses()
{
if (_rootElement != null && Owner != null && Owner.IsVisible)
{
if (OwningRow != null)
{
PseudoClasses.Set(":invalid", !OwningRow.IsValid);
PseudoClasses.Set(":selected", OwningRow.IsSelected);
PseudoClasses.Set(":editing", OwningRow.IsEditing);
if (OwningGrid != null)
{
PseudoClasses.Set(":current", OwningRow.Slot == OwningGrid.CurrentSlot);
}
}
else if (OwningRowGroupHeader != null && OwningGrid != null)
{
PseudoClasses.Set(":current", OwningRowGroupHeader.RowGroupInfo.Slot == OwningGrid.CurrentSlot);
}
}
}
@ -162,7 +177,7 @@ namespace Avalonia.Controls.Primitives
//TODO TabStop
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

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

@ -1912,7 +1912,7 @@ namespace Avalonia.Controls
{
// Assume it's a RowGroupHeader
DataGridRowGroupHeader groupHeader = element as DataGridRowGroupHeader;
groupHeader.ApplyState(useTransitions: true);
groupHeader.UpdatePseudoClasses();
}
}

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

@ -9,6 +9,7 @@ using Avalonia.Media;
using System;
using System.ComponentModel;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
{
@ -17,6 +18,7 @@ namespace Avalonia.Controls
/// </summary>
public class DataGridTextColumn : DataGridBoundColumn
{
private const string DATAGRID_TextColumnCellTextBlockMarginKey = "DataGridTextColumnCellTextBlockMargin";
private double? _fontSize;
private FontStyle? _fontStyle;
@ -186,7 +188,7 @@ namespace Avalonia.Controls
{
TextBlock textBlockElement = new TextBlock
{
Margin = new Thickness(4),
[!Layoutable.MarginProperty] = new DynamicResourceExtension(DATAGRID_TextColumnCellTextBlockMarginKey),
VerticalAlignment = VerticalAlignment.Center
};

23
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -1,5 +1,11 @@
<Styles xmlns="https://github.com/avaloniaui">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--TODO: Validation and Focus-->
<Styles.Resources>
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">4</Thickness>
</Styles.Resources>
<Style Selector="DataGridCell">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
@ -28,7 +34,7 @@
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="SeparatorBrush" Value="{DynamicResource ThemeControlDarkBrush}" />
<Setter Property="SeparatorBrush" Value="{DynamicResource ThemeControlLowColor}" />
<Setter Property="Padding" Value="4" />
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
@ -133,7 +139,7 @@
<Style Selector="DataGridRowHeader">
<Setter Property="Template">
<ControlTemplate>
<Grid
<Grid x:Name="PART_Root"
RowDefinitions="*,*,Auto"
ColumnDefinitions="Auto,*">
@ -188,17 +194,16 @@
</Style>
<Style Selector="DataGrid">
<Setter Property="RowBackground" Value="{DynamicResource SystemAccentColorDark2}" />
<Setter Property="RowBackground" Value="{DynamicResource ThemeAccentBrush4}" />
<Setter Property="AlternatingRowBackground" Value="#00FFFFFF" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="GridLinesVisibility" Value="Vertical" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderHighColor}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowColor}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
@ -218,7 +223,7 @@
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
<Rectangle Name="BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="PART_BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>

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

@ -0,0 +1,645 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<Thickness x:Key="DataGridTextColumnCellTextBlockMargin">12,0,12,0</Thickness>
<x:Double x:Key="ListAccentLowOpacity">0.6</x:Double>
<x:Double x:Key="ListAccentMediumOpacity">0.8</x:Double>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M1939 1581l90 -90l-1005 -1005l-1005 1005l90 90l915 -915z</StreamGeometry>
<SolidColorBrush x:Key="DataGridGridLinesBrush"
Color="{StaticResource SystemBaseMediumLowColor}"
Opacity="0.4" />
<SolidColorBrush x:Key="DataGridDropLocationIndicatorBackground"
Color="#3F4346" />
<SolidColorBrush x:Key="DataGridDisabledVisualElementBackground"
Color="#8CFFFFFF" />
<SolidColorBrush x:Key="DataGridFillerGridLinesBrush"
Color="Transparent" />
<SolidColorBrush x:Key="DataGridCurrencyVisualPrimaryBrush"
Color="Transparent" />
<StaticResource x:Key="DataGridColumnHeaderBackgroundColor"
ResourceKey="SystemAltHighColor" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush"
Color="{StaticResource DataGridColumnHeaderBackgroundColor}" />
<StaticResource x:Key="DataGridScrollBarsSeparatorBackground"
ResourceKey="SystemChromeLowColor" />
<StaticResource x:Key="DataGridColumnHeaderForegroundBrush"
ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="DataGridColumnHeaderHoveredBackgroundColor"
ResourceKey="SystemListLowColor" />
<StaticResource x:Key="DataGridColumnHeaderPressedBackgroundColor"
ResourceKey="SystemListMediumColor" />
<StaticResource x:Key="DataGridColumnHeaderDraggedBackgroundBrush"
ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
<StaticResource x:Key="DataGridColumnHeaderPointerOverBrush"
ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="DataGridColumnHeaderPressedBrush"
ResourceKey="SystemControlHighlightListMediumBrush" />
<StaticResource x:Key="DataGridDetailsPresenterBackgroundBrush"
ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
<StaticResource x:Key="DataGridFillerColumnGridLinesBrush"
ResourceKey="DataGridFillerGridLinesBrush" />
<StaticResource x:Key="DataGridRowSelectedBackgroundColor"
ResourceKey="SystemAccentColor" />
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity"
ResourceKey="ListAccentLowOpacity" />
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundColor"
ResourceKey="SystemAccentColor" />
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity"
ResourceKey="ListAccentMediumOpacity" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundColor"
ResourceKey="SystemAccentColor" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity"
ResourceKey="ListAccentLowOpacity" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundColor"
ResourceKey="SystemAccentColor" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity"
ResourceKey="ListAccentMediumOpacity" />
<StaticResource x:Key="DataGridRowHeaderForegroundBrush"
ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="DataGridRowHeaderBackgroundBrush"
ResourceKey="SystemControlBackgroundAltHighBrush" />
<StaticResource x:Key="DataGridRowGroupHeaderBackgroundBrush"
ResourceKey="SystemControlBackgroundChromeMediumBrush" />
<StaticResource x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush"
ResourceKey="SystemControlBackgroundListLowBrush" />
<StaticResource x:Key="DataGridRowGroupHeaderPressedBackgroundBrush"
ResourceKey="SystemControlBackgroundListMediumBrush" />
<StaticResource x:Key="DataGridRowGroupHeaderForegroundBrush"
ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="DataGridRowInvalidBrush"
ResourceKey="SystemErrorTextColor" />
<StaticResource x:Key="DataGridCellBackgroundBrush"
ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="DataGridCellFocusVisualPrimaryBrush"
ResourceKey="SystemControlFocusVisualPrimaryBrush" />
<StaticResource x:Key="DataGridCellFocusVisualSecondaryBrush"
ResourceKey="SystemControlFocusVisualSecondaryBrush" />
<StaticResource x:Key="DataGridCellInvalidBrush"
ResourceKey="SystemErrorTextColor" />
</Styles.Resources>
<Style Selector="DataGridCell">
<Setter Property="Background" Value="{DynamicResource DataGridCellBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_CellRoot"
ColumnDefinitions="*,Auto"
Background="{TemplateBinding Background}">
<Rectangle x:Name="CurrencyVisual"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid x:Name="FocusVisual"
IsHitTestVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
<Rectangle x:Name="InvalidVisualElement"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellInvalidBrush}"
StrokeThickness="1" />
<Rectangle Name="PART_RightGridLine"
Grid.Column="1"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource DataGridFillerColumnGridLinesBrush}" />
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridCell /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell:current /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid:focus DataGridCell:current /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="Padding" Value="12,0,0,0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<Grid Name="PART_ColumnHeaderRoot"
ColumnDefinitions="*,Auto"
Background="{TemplateBinding Background}">
<Grid HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition MinWidth="32"
Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}" />
<Path Name="SortIcon"
Grid.Column="1"
Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform"
Height="12" />
</Grid>
<Rectangle Name="VerticalSeparator"
Grid.Column="1"
Width="1"
VerticalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<Grid x:Name="FocusVisual"
IsHitTestVisible="False">
<Rectangle x:Name="FocusVisualPrimary"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle x:Name="FocusVisualSecondary"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridColumnHeader /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridColumnHeader:focus-visible /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridColumnHeader:pointerover /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderHoveredBackgroundColor}" />
</Style>
<Style Selector="DataGridColumnHeader:pressed /template/ Grid#PART_ColumnHeaderRoot">
<Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderPressedBackgroundColor}" />
</Style>
<Style Selector="DataGridColumnHeader:dragIndicator">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style Selector="DataGridColumnHeader /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridColumnHeader:sortascending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconAscendingPath}" />
</Style>
<Style Selector="DataGridColumnHeader:sortdescending /template/ Path#SortIcon">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Data" Value="{StaticResource DataGridSortIconDescendingPath}" />
</Style>
<Style Selector="DataGridRow">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<DataGridFrozenGrid Name="PART_Root"
RowDefinitions="*,Auto,Auto"
ColumnDefinitions="Auto,*">
<Rectangle Name="BackgroundRectangle"
Grid.RowSpan="2"
Grid.ColumnSpan="2" />
<Rectangle x:Name="InvalidVisualElement"
Grid.ColumnSpan="2"
Fill="{DynamicResource DataGridRowInvalidBrush}" />
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="3"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridCellsPresenter Name="PART_CellsPresenter"
Grid.Column="1"
DataGridFrozenGrid.IsFrozen="True" />
<DataGridDetailsPresenter Name="PART_DetailsPresenter"
Grid.Row="1"
Grid.Column="1"
Background="{DynamicResource DataGridDetailsPresenterBackgroundBrush}" />
<Rectangle Name="PART_BottomGridLine"
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Stretch"
Height="1" />
</DataGridFrozenGrid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRow /template/ Rectangle#InvalidVisualElement">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRow:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="Opacity" Value="0.4" />
</Style>
<Style Selector="DataGridRow:invalid /template/ Rectangle#BackgroundRectangle">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource SystemListLowColor}" />
</Style>
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:selected:pointerover:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowHeader">
<Setter Property="Background" Value="{DynamicResource DataGridRowHeaderBackgroundBrush}" />
<Setter Property="Focusable" Value="False" />
<Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="AreSeparatorsVisible" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="PART_Root"
RowDefinitions="*,*,Auto"
ColumnDefinitions="Auto,*">
<Border Grid.RowSpan="3"
Grid.ColumnSpan="2"
BorderBrush="{TemplateBinding SeparatorBrush}"
BorderThickness="0,0,1,0">
<Grid Background="{TemplateBinding Background}">
<Rectangle x:Name="RowInvalidVisualElement"
Fill="{DynamicResource DataGridRowInvalidBrush}"
Stretch="Fill" />
<Rectangle x:Name="BackgroundRectangle"
Stretch="Fill" />
</Grid>
</Border>
<Rectangle x:Name="HorizontalSeparator"
Grid.Row="2"
Grid.ColumnSpan="2"
Height="1"
Margin="1,0,1,0"
HorizontalAlignment="Stretch"
Fill="{TemplateBinding SeparatorBrush}"
IsVisible="{TemplateBinding AreSeparatorsVisible}" />
<ContentPresenter Grid.RowSpan="2"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowHeader /template/ Rectangle#RowInvalidVisualElement">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#RowInvalidVisualElement">
<Setter Property="Opacity" Value="0.4" />
</Style>
<Style Selector="DataGridRowHeader:invalid /template/ Rectangle#BackgroundRectangle">
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource SystemControlTransparentBrush}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource SystemListLowColor}" />
</Style>
<Style Selector="DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredUnfocusedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRow:pointerover DataGridRowHeader:selected:focus /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundColor}" />
<Setter Property="Opacity" Value="{DynamicResource DataGridRowSelectedHoveredBackgroundOpacity}" />
</Style>
<Style Selector="DataGridRowGroupHeader">
<Setter Property="Focusable" Value="False" />
<Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate>
<DataGridFrozenGrid Name="PART_Root"
MinHeight="{TemplateBinding MinHeight}"
ColumnDefinitions="Auto,Auto,Auto,Auto,*"
RowDefinitions="*,Auto">
<Rectangle Name="IndentSpacer"
Grid.Column="1" />
<ToggleButton Name="ExpanderButton"
Grid.Column="2"
Width="12"
Height="12"
Margin="12,0,0,0"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
Focusable="False" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<TextBlock Name="PropertyNameElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsPropertyNameVisible}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Margin="4,0,0,0"
Text="{Binding Key}"
Foreground="{TemplateBinding Foreground}" />
<TextBlock Name="ItemCountElement"
Margin="4,0,0,0"
IsVisible="{TemplateBinding IsItemCountVisible}"
Foreground="{TemplateBinding Foreground}" />
</StackPanel>
<Rectangle x:Name="CurrencyVisual"
Grid.ColumnSpan="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCurrencyVisualPrimaryBrush}"
StrokeThickness="1" />
<Grid x:Name="FocusVisual"
Grid.ColumnSpan="5"
IsHitTestVisible="False">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualPrimaryBrush}"
StrokeThickness="2" />
<Rectangle Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="Transparent"
IsHitTestVisible="False"
Stroke="{DynamicResource DataGridCellFocusVisualSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<DataGridRowHeader Name="PART_RowHeader"
Grid.RowSpan="2"
DataGridFrozenGrid.IsFrozen="True" />
<Rectangle x:Name="PART_BottomGridLine"
Grid.Row="1"
Grid.ColumnSpan="5"
Height="1" />
</DataGridFrozenGrid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton">
<Setter Property="Template">
<ControlTemplate>
<Border Grid.Column="0"
Width="12"
Height="12"
Background="Transparent"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Path Fill="{TemplateBinding Foreground}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Stretch="Uniform" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton /template/ Path">
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconOpenedPath}" />
<Setter Property="Stretch" Value="Uniform" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ ToggleButton#ExpanderButton:checked /template/ Path">
<Setter Property="Data" Value="{StaticResource DataGridRowGroupHeaderIconClosedPath}" />
<Setter Property="Stretch" Value="UniformToFill" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{Binding $parent[DataGridRowGroupHeader].Background}" />
</Style>
<Style Selector="DataGridRowGroupHeader:pointerover /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderHoveredBackgroundBrush}" />
</Style>
<Style Selector="DataGridRowGroupHeader:pressed /template/ DataGridFrozenGrid#PART_Root">
<Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderPressedBackgroundBrush}" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridRowGroupHeader /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="DataGridRowGroupHeader:current /template/ Rectangle#CurrencyVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid:focus DataGridRowGroupHeader:current /template/ Grid#FocusVisual">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGrid">
<Setter Property="RowBackground" Value="Transparent" />
<Setter Property="AlternatingRowBackground" Value="Transparent" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
<Setter Property="DropLocationIndicatorTemplate">
<Template>
<Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"
Width="2" />
</Template>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<Grid RowDefinitions="Auto,*,Auto,Auto"
ColumnDefinitions="Auto,*,Auto">
<Grid.Resources>
<ControlTemplate x:Key="TopLeftHeaderTemplate"
TargetType="DataGridColumnHeader">
<Grid x:Name="TopLeftHeaderRoot"
RowDefinitions="*,*,Auto">
<Border Grid.RowSpan="2"
BorderThickness="0,0,1,0"
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
<Rectangle Grid.RowSpan="2"
VerticalAlignment="Bottom"
StrokeThickness="1"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="TopRightHeaderTemplate"
TargetType="DataGridColumnHeader">
<Grid x:Name="RootElement" />
</ControlTemplate>
</Grid.Resources>
<DataGridColumnHeader Name="PART_TopLeftCornerHeader"
Template="{StaticResource TopLeftHeaderTemplate}" />
<DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
Grid.Column="1"
Grid.ColumnSpan="2" />
<!--<DataGridColumnHeader Name="PART_TopRightCornerHeader"
Grid.Column="2"
Template="{StaticResource TopRightHeaderTemplate}" />-->
<!--<Rectangle Name="PART_ColumnHeadersAndRowsSeparator"
Grid.ColumnSpan="3"
VerticalAlignment="Bottom"
StrokeThickness="1"
Height="1"
Fill="{DynamicResource DataGridGridLinesBrush}" />-->
<Border Name="PART_ColumnHeadersAndRowsSeparator"
Grid.ColumnSpan="3"
Height="2"
VerticalAlignment="Bottom"
BorderThickness="0,0,0,1"
BorderBrush="{DynamicResource DataGridGridLinesBrush}" />
<DataGridRowsPresenter Name="PART_RowsPresenter"
Grid.Row="1"
Grid.RowSpan="2"
Grid.ColumnSpan="3" />
<Rectangle Name="PART_BottomRightCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Column="2"
Grid.Row="2" />
<!--<Rectangle Name="BottomLeftCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Row="2"
Grid.ColumnSpan="2" />-->
<ScrollBar Name="PART_VerticalScrollbar"
Orientation="Vertical"
Grid.Column="2"
Grid.Row="1"
Width="{DynamicResource ScrollBarSize}" />
<Grid Grid.Column="1"
Grid.Row="2"
ColumnDefinitions="Auto,*">
<Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
<ScrollBar Name="PART_HorizontalScrollbar"
Grid.Column="1"
Orientation="Horizontal"
Height="{DynamicResource ScrollBarSize}" />
</Grid>
<Border x:Name="PART_DisabledVisualElement"
Grid.ColumnSpan="3"
Grid.RowSpan="4"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="2"
Background="{DynamicResource DataGridDisabledVisualElementBackground}"
IsVisible="{Binding !$parent[DataGrid].IsEnabled}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Styles>

23
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,7 +1,18 @@
Compat issues with assembly Avalonia.Controls:
MembersMustExist : Member 'protected void Avalonia.Controls.ComboBox.PopupClosedOverride(Avalonia.Controls.Primitives.PopupClosedEventArgs)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.Primitives.Popup.StaysOpenProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.add_Closed(System.EventHandler<Avalonia.Controls.Primitives.PopupClosedEventArgs>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.remove_Closed(System.EventHandler<Avalonia.Controls.Primitives.PopupClosedEventArgs>)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.Primitives.PopupClosedEventArgs' does not exist in the implementation but it does exist in the contract.
Total Issues: 5
TypesMustExist : Type 'Avalonia.Controls.IndexPath' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.ISelectedItemInfo' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.ISelectionModel' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.ListBox.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.ListBox.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.ListBox.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModel' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModelChildrenRequestedEventArgs' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.SelectionModelSelectionChangedEventArgs' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.Controls.TreeView.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
Total Issues: 16

17
src/Avalonia.Controls/AppBuilderBase.cs

@ -88,6 +88,23 @@ namespace Avalonia.Controls
};
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <param name="appFactory">Factory function for <typeparamref name="TApp"/>.</param>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <remarks><paramref name="appFactory"/> is useful for passing of dependencies to <typeparamref name="TApp"/>.</remarks>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public static TAppBuilder Configure<TApp>(Func<TApp> appFactory)
where TApp : Application
{
return new TAppBuilder()
{
ApplicationType = typeof(TApp),
_appFactory = appFactory
};
}
protected TAppBuilder Self => (TAppBuilder)this;
public TAppBuilder AfterSetup(Action<TAppBuilder> callback)

3
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -2,6 +2,9 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

8
src/Avalonia.Controls/ContextMenu.cs

@ -279,10 +279,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
if (PlacementTarget is null && _popup.PlacementTarget != control)
{
_popup.PlacementTarget = control;
}
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;
RaiseEvent(new RoutedEventArgs
{

249
src/Avalonia.Controls/ISelectionModel.cs

@ -1,249 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the selected items for a control.
/// </summary>
public interface ISelectionModel : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the anchor index.
/// </summary>
IndexPath AnchorIndex { get; set; }
/// <summary>
/// Gets or set the index of the first selected item.
/// </summary>
IndexPath SelectedIndex { get; set; }
/// <summary>
/// Gets or set the indexes of the selected items.
/// </summary>
IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the first selected item.
/// </summary>
object SelectedItem { get; }
/// <summary>
/// Gets the selected items.
/// </summary>
IReadOnlyList<object> SelectedItems { get; }
/// <summary>
/// Gets a value indicating whether the model represents a single or multiple selection.
/// </summary>
bool SingleSelect { get; set; }
/// <summary>
/// Gets a value indicating whether to always keep an item selected where possible.
/// </summary>
bool AutoSelect { get; set; }
/// <summary>
/// Gets or sets the collection that contains the items that can be selected.
/// </summary>
object Source { get; set; }
/// <summary>
/// Raised when the children of a selection are required.
/// </summary>
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested;
/// <summary>
/// Raised when the selection has changed.
/// </summary>
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged;
/// <summary>
/// Clears the selection.
/// </summary>
void ClearSelection();
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void Deselect(int index);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Deselect(int groupIndex, int itemIndex);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void DeselectAt(IndexPath index);
/// <summary>
/// Deselects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void DeselectRange(IndexPath start, IndexPath end);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchor(int index);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Disposes the object and clears the selection.
/// </summary>
void Dispose();
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
bool IsSelected(int index);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
bool IsSelected(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
public bool IsSelectedAt(IndexPath index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartial(int index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartialAt(IndexPath index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void Select(int index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Select(int groupIndex, int itemIndex);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void SelectAt(IndexPath index);
/// <summary>
/// Selects all items.
/// </summary>
void SelectAll();
/// <summary>
/// Selects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void SelectRange(IndexPath start, IndexPath end);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchor(int index);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The anchor index.</param>
void SetAnchorIndex(int index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="index">The index of the item in the group.</param>
void SetAnchorIndex(int groupIndex, int index);
/// <summary>
/// Begins a batch update of the selection.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
IDisposable Update();
}
}

200
src/Avalonia.Controls/IndexPath.cs

@ -1,200 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath>
{
public static readonly IndexPath Unselected = default;
private readonly int _index;
private readonly int[]? _path;
public IndexPath(int index)
{
_index = index + 1;
_path = null;
}
public IndexPath(int groupIndex, int itemIndex)
{
_index = 0;
_path = new[] { groupIndex, itemIndex };
}
public IndexPath(IEnumerable<int>? indices)
{
if (indices != null)
{
_index = 0;
_path = indices.ToArray();
}
else
{
_index = 0;
_path = null;
}
}
private IndexPath(int[] basePath, int index)
{
basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
_index = 0;
_path = new int[basePath.Length + 1];
Array.Copy(basePath, _path, basePath.Length);
_path[basePath.Length] = index;
}
public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1);
public int GetAt(int index)
{
if (index >= GetSize())
{
throw new IndexOutOfRangeException();
}
return _path?[index] ?? (_index - 1);
}
public int CompareTo(IndexPath other)
{
var rhsPath = other;
int compareResult = 0;
int lhsCount = GetSize();
int rhsCount = rhsPath.GetSize();
if (lhsCount == 0 || rhsCount == 0)
{
// one of the paths are empty, compare based on size
compareResult = (lhsCount - rhsCount);
}
else
{
// both paths are non-empty, but can be of different size
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++)
{
if (GetAt(i) < rhsPath.GetAt(i))
{
compareResult = -1;
break;
}
else if (GetAt(i) > rhsPath.GetAt(i))
{
compareResult = 1;
break;
}
}
// if both match upto min(lhsCount, rhsCount), compare based on size
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult;
}
if (compareResult != 0)
{
compareResult = compareResult > 0 ? 1 : -1;
}
return compareResult;
}
public IndexPath CloneWithChildIndex(int childIndex)
{
if (_path != null)
{
return new IndexPath(_path, childIndex);
}
else if (_index != 0)
{
return new IndexPath(_index - 1, childIndex);
}
else
{
return new IndexPath(childIndex);
}
}
public bool IsAncestorOf(in IndexPath other)
{
if (other.GetSize() <= GetSize())
{
return false;
}
var size = GetSize();
for (int i = 0; i < size; i++)
{
if (GetAt(i) != other.GetAt(i))
{
return false;
}
}
return true;
}
public override string ToString()
{
if (_path != null)
{
return "R" + string.Join(".", _path);
}
else if (_index != 0)
{
return "R" + (_index - 1);
}
else
{
return "R";
}
}
public static IndexPath CreateFrom(int index) => new IndexPath(index);
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex);
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices);
public override bool Equals(object obj) => obj is IndexPath other && Equals(other);
public bool Equals(IndexPath other) => CompareTo(other) == 0;
public override int GetHashCode()
{
var hashCode = -504981047;
if (_path != null)
{
foreach (var i in _path)
{
hashCode = hashCode * -1521134295 + i.GetHashCode();
}
}
else
{
hashCode = hashCode * -1521134295 + _index.GetHashCode();
}
return hashCode;
}
public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; }
public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; }
public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; }
public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; }
public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; }
public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; }
public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; }
public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; }
}
}

30
src/Avalonia.Controls/ItemsControl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
/// <summary>
/// Displays a collection of items.
/// </summary>
public class ItemsControl : TemplatedControl, IItemsPresenterHost
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -53,7 +53,6 @@ namespace Avalonia.Controls
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
private IDisposable _itemsCollectionChangedSubscription;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
@ -150,6 +149,19 @@ namespace Avalonia.Controls
ItemContainerGenerator.Clear();
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
ItemsCollectionChanged(sender, e);
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
@ -315,12 +327,14 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
_itemsCollectionChangedSubscription?.Dispose();
_itemsCollectionChangedSubscription = null;
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
if (oldValue is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
}
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
@ -418,11 +432,9 @@ namespace Avalonia.Controls
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
PseudoClasses.Set(":singleitem", items != null && items.Count() == 1);
var incc = items as INotifyCollectionChanged;
if (incc != null)
if (items is INotifyCollectionChanged incc)
{
_itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
CollectionChangedEventManager.Instance.AddListener(incc, this);
}
}

120
src/Avalonia.Controls/Repeater/ItemsSourceView.cs → src/Avalonia.Controls/ItemsSourceView.cs

@ -7,7 +7,11 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls
{
@ -23,8 +27,13 @@ namespace Avalonia.Controls
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
private protected readonly IList _inner;
private INotifyCollectionChanged? _notifyCollectionChanged;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
@ -32,7 +41,7 @@ namespace Avalonia.Controls
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
{
Contract.Requires<ArgumentNullException>(source != null);
source = source ?? throw new ArgumentNullException(nameof(source));
if (source is IList list)
{
@ -63,10 +72,17 @@ namespace Avalonia.Controls
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public object? this[int index] => GetAt(index);
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
/// <inheritdoc/>
public void Dispose()
@ -81,10 +97,26 @@ namespace Avalonia.Controls
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
/// <returns>The item.</returns>
public object? GetAt(int index) => _inner[index];
public int IndexOf(object item) => _inner.IndexOf(item);
public int IndexOf(object? item) => _inner.IndexOf(item);
public static ItemsSourceView GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView isv)
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView(items);
}
}
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
@ -112,6 +144,22 @@ namespace Avalonia.Controls
throw new NotImplementedException();
}
internal void AddListener(ICollectionChangedListener listener)
{
if (_inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.AddListener(incc, listener);
}
}
internal void RemoveListener(ICollectionChangedListener listener)
{
if (_inner is INotifyCollectionChanged incc)
{
CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
}
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
@ -131,4 +179,62 @@ namespace Avalonia.Controls
OnItemsSourceChanged(e);
}
}
public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public new static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>());
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable<T> source)
: base(source)
{
}
private ItemsSourceView(IEnumerable source)
: base(source)
{
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
#pragma warning disable CS8603
public new T this[int index] => GetAt(index);
#pragma warning restore CS8603
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
[return: MaybeNull]
public new T GetAt(int index) => (T)_inner[index];
public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
{
if (items is ItemsSourceView<T> isv)
{
return isv;
}
else if (items is null)
{
return Empty;
}
else
{
return new ItemsSourceView<T>(items);
}
}
}
}

7
src/Avalonia.Controls/ListBox.cs

@ -2,6 +2,7 @@ using System.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.VisualTree;
@ -76,9 +77,7 @@ namespace Avalonia.Controls
set => base.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// </summary>
/// <inheritdoc/>
public new ISelectionModel Selection
{
get => base.Selection;
@ -115,7 +114,7 @@ namespace Avalonia.Controls
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public void UnselectAll() => Selection.ClearSelection();
public void UnselectAll() => Selection.Clear();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()

22
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -257,15 +257,7 @@ namespace Avalonia.Controls.Presenters
return base.ArrangeOverride(finalSize);
}
try
{
_arranging = true;
return ArrangeWithAnchoring(finalSize);
}
finally
{
_arranging = false;
}
return ArrangeWithAnchoring(finalSize);
}
private Size ArrangeWithAnchoring(Size finalSize)
@ -316,7 +308,17 @@ namespace Avalonia.Controls.Presenters
}
Extent = newExtent;
Offset = newOffset;
try
{
_arranging = true;
Offset = newOffset;
}
finally
{
_arranging = false;
}
ArrangeOverrideImpl(size, -Offset);
}

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

@ -3,9 +3,9 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
@ -13,6 +13,8 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
@ -24,8 +26,8 @@ namespace Avalonia.Controls.Primitives
/// that maintain a selection (single or multiple). By default only its
/// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
/// current multiple <see cref="Selection"/> and <see cref="SelectedItems"/> together with the
/// <see cref="SelectionMode"/> and properties are protected, however a derived class can
/// expose these if it wishes to support multiple selection.
/// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
/// these if it wishes to support multiple selection.
/// </para>
/// <para>
/// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
@ -58,8 +60,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
public static readonly DirectProperty<SelectingItemsControl, object> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, object>(
public static readonly DirectProperty<SelectingItemsControl, object?> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, object?>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
@ -77,7 +79,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
protected static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, ISelectionModel>(
nameof(Selection),
o => o.Selection,
@ -109,21 +111,12 @@ namespace Avalonia.Controls.Primitives
RoutingStrategies.Bubble);
private static readonly IList Empty = Array.Empty<object>();
private readonly SelectedItemsSync _selectedItems;
private ISelectionModel _selection;
private int _selectedIndex = -1;
private object _selectedItem;
private SelectedItemsSync? _selectedItemsSync;
private ISelectionModel? _selection;
private int _oldSelectedIndex;
private object? _oldSelectedItem;
private int _initializing;
private bool _ignoreContainerSelectionChanged;
private int _updateCount;
private int _updateSelectedIndex;
private object _updateSelectedItem;
public SelectingItemsControl()
{
// Setting Selection to null causes a default SelectionModel to be created.
Selection = null;
_selectedItems = new SelectedItemsSync(Selection);
}
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -156,42 +149,17 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public int SelectedIndex
{
get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1;
set
{
if (_updateCount == 0)
{
if (value != SelectedIndex)
{
Selection.SelectedIndex = new IndexPath(value);
}
}
else
{
_updateSelectedIndex = value;
_updateSelectedItem = null;
}
}
get => Selection.SelectedIndex;
set => Selection.SelectedIndex = value;
}
/// <summary>
/// Gets or sets the selected item.
/// </summary>
public object SelectedItem
public object? SelectedItem
{
get => Selection.SelectedItem;
set
{
if (_updateCount == 0)
{
SelectedIndex = IndexOf(Items, value);
}
else
{
_updateSelectedItem = value;
_updateSelectedIndex = int.MinValue;
}
}
set => Selection.SelectedItem = value;
}
/// <summary>
@ -199,46 +167,40 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected IList SelectedItems
{
get => _selectedItems.GetOrCreateItems();
set => _selectedItems.SetItems(value);
get => SelectedItemsSync.SelectedItems;
set => SelectedItemsSync.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// Gets or sets the model that holds the current selection.
/// </summary>
protected ISelectionModel Selection
protected ISelectionModel Selection
{
get => _selection;
set
get
{
value ??= new SelectionModel
if (_selection is null)
{
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
RetainSelectionOnReset = true,
};
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection;
}
set
{
value ??= CreateDefaultSelectionModel();
if (_selection != value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
}
else if (value.Source != null && value.Source != Items)
{
throw new ArgumentException("Selection has invalid Source.");
}
List<object> oldSelection = null;
if (_selection != null)
if (value.Source != null && value.Source != Items)
{
oldSelection = Selection.SelectedItems.ToList();
_selection.PropertyChanged -= OnSelectionModelPropertyChanged;
_selection.SelectionChanged -= OnSelectionModelSelectionChanged;
MarkContainersUnselected();
throw new ArgumentException(
"The supplied ISelectionModel already has an assigned Source but this " +
"collection is different to the Items on the control.");
}
var oldSelection = _selection?.SelectedItems.ToList();
DeinitializeSelectionModel(_selection);
_selection = value;
if (oldSelection?.Count > 0)
@ -249,55 +211,7 @@ namespace Avalonia.Controls.Primitives
Array.Empty<object>()));
}
if (_selection != null)
{
_selection.Source = Items;
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
if (_selection.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
if (_selection.AutoSelect)
{
SelectionMode |= SelectionMode.AlwaysSelected;
}
else
{
SelectionMode &= ~SelectionMode.AlwaysSelected;
}
UpdateContainerSelection();
var selectedIndex = SelectedIndex;
var selectedItem = SelectedItem;
if (_selectedIndex != selectedIndex)
{
RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex);
_selectedIndex = selectedIndex;
}
if (_selectedItem != selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
_selectedItem = selectedItem;
}
if (selectedIndex != -1)
{
RaiseEvent(new SelectionChangedEventArgs(
SelectionChangedEvent,
Array.Empty<object>(),
Selection.SelectedItems.ToList()));
}
}
InitializeSelectionModel(_selection);
}
}
}
@ -320,20 +234,20 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
/// <inheritdoc/>
public override void BeginInit()
{
base.BeginInit();
InternalBeginInit();
++_initializing;
}
/// <inheritdoc/>
public override void EndInit()
{
InternalEndInit();
base.EndInit();
--_initializing;
}
/// <summary>
@ -353,7 +267,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="eventSource">The control that raised the event.</param>
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
protected IControl? GetContainerFromEventSource(IInteractive eventSource)
{
var parent = (IVisual)eventSource;
@ -371,21 +285,14 @@ namespace Avalonia.Controls.Primitives
return null;
}
/// <inheritdoc/>
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (_updateCount == 0)
{
Selection.Source = e.NewValue;
}
base.ItemsChanged(e);
}
/// <inheritdoc/>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
}
}
/// <inheritdoc/>
@ -400,9 +307,10 @@ namespace Avalonia.Controls.Primitives
Selection.Select(container.Index);
MarkContainerSelected(container.ContainerControl, true);
}
else if (Selection.IsSelected(container.Index) == true)
else
{
MarkContainerSelected(container.ContainerControl, true);
var selected = Selection.IsSelected(container.Index);
MarkContainerSelected(container.ContainerControl, selected);
}
}
}
@ -433,7 +341,7 @@ namespace Avalonia.Controls.Primitives
{
if (i.ContainerControl != null && i.Item != null)
{
bool selected = Selection.IsSelected(i.Index) == true;
bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
}
@ -443,27 +351,39 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextBeginUpdate();
++_initializing;
InternalBeginInit();
if (_selection is object)
{
_selection.Source = null;
}
}
/// <inheritdoc/>
protected override void OnDataContextEndUpdate()
{
base.OnDataContextEndUpdate();
--_initializing;
if (_selection is object && _initializing == 0)
{
_selection.Source = Items;
InternalEndInit();
if (Items is null)
{
_selection.Clear();
_selectedItemsSync?.SelectedItems?.Clear();
}
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void OnInitialized()
{
base.OnPropertyChanged(change);
base.OnInitialized();
if (change.Property == SelectionModeProperty)
if (_selection is object)
{
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
_selection.Source = Items;
}
}
@ -487,6 +407,29 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ItemsProperty &&
_initializing == 0 &&
_selection is object)
{
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
_selection.Source = newValue;
if (newValue is null)
{
_selection.Clear();
}
}
else if (change.Property == SelectionModeProperty && _selection is object)
{
var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
_selection.SingleSelect = !newValue.HasFlagCustom(SelectionMode.Multiple);
}
}
/// <summary>
/// Moves the selection in the specified direction relative to the current selection.
/// </summary>
@ -506,7 +449,7 @@ namespace Avalonia.Controls.Primitives
/// <param name="direction">The direction to move.</param>
/// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is IControl next)
@ -538,71 +481,62 @@ namespace Avalonia.Controls.Primitives
bool toggleModifier = false,
bool rightButton = false)
{
if (index != -1)
if (index < 0 || index >= ItemCount)
{
if (select)
{
var mode = SelectionMode;
var multi = (mode & SelectionMode.Multiple) != 0;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (rightButton)
{
if (Selection.IsSelected(index) == false)
{
SelectedIndex = index;
}
}
else if (range)
{
using var operation = Selection.Update();
var anchor = Selection.AnchorIndex;
if (anchor.GetSize() == 0)
{
anchor = new IndexPath(0);
}
return;
}
Selection.ClearSelection();
Selection.AnchorIndex = anchor;
Selection.SelectRangeFromAnchor(index);
}
else if (multi && toggle)
{
if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
Selection.Select(index);
}
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{
using var operation = Selection.Update();
Selection.ClearSelection();
Selection.Select(index);
}
var mode = SelectionMode;
var multi = (mode & SelectionMode.Multiple) != 0;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
if (!select)
{
Selection.Deselect(index);
}
else if (rightButton)
{
if (Selection.IsSelected(index) == false)
{
SelectedIndex = index;
}
}
else if (range)
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.SelectRange(Selection.AnchorIndex, index);
}
else if (multi && toggle)
{
if (Selection.IsSelected(index) == true)
{
Selection.Deselect(index);
}
else
{
LostSelection();
Selection.Select(index);
}
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{
using var operation = Selection.BatchUpdate();
Selection.Clear();
Selection.Select(index);
}
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
}
}
/// <summary>
@ -660,23 +594,35 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
/// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
{
if (Selection.AnchorIndex.GetSize() > 0)
if (Selection.AnchorIndex > 0)
{
ScrollIntoView(Selection.AnchorIndex.GetAt(0));
ScrollIntoView(Selection.AnchorIndex);
}
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex))
{
RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
_oldSelectedIndex = SelectedIndex;
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedItem))
{
RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
_oldSelectedItem = SelectedItem;
}
}
/// <summary>
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
/// Called when <see cref="ISelectionModel.SelectionChanged"/> event is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
@ -692,46 +638,40 @@ namespace Avalonia.Controls.Primitives
}
}
if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
foreach (var i in e.SelectedIndexes)
{
foreach (var i in e.SelectedIndices)
{
Mark(i.GetAt(0), true);
}
foreach (var i in e.DeselectedIndices)
{
Mark(i.GetAt(0), false);
}
Mark(i, true);
}
else if (e.DeselectedItems.Count > 0)
foreach (var i in e.DeselectedIndexes)
{
// (De)selected indices being empty means that a selected item was removed from
// the Items (it can't tell us the index of the item because the index is no longer
// valid). In this case, we just update the selection state of all containers.
UpdateContainerSelection();
Mark(i, false);
}
var newSelectedIndex = SelectedIndex;
var newSelectedItem = SelectedItem;
var route = BuildEventRoute(SelectionChangedEvent);
if (newSelectedIndex != _selectedIndex)
if (route.HasHandlers)
{
RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex);
_selectedIndex = newSelectedIndex;
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
}
if (newSelectedItem != _selectedItem)
/// <summary>
/// Called when <see cref="ISelectionModel.LostSelection"/> event is raised on
/// <see cref="Selection"/>.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelLostSelection(object sender, EventArgs e)
{
if (AlwaysSelected && Items is object)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
_selectedItem = newSelectedItem;
SelectedIndex = 0;
}
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
/// <summary>
@ -760,23 +700,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Called when the currently selected item is lost and the selection must be changed
/// depending on the <see cref="SelectionMode"/> property.
/// </summary>
private void LostSelection()
{
var items = Items?.Cast<object>();
var index = -1;
if (items != null && AlwaysSelected)
{
index = Math.Min(SelectedIndex, items.Count() - 1);
}
SelectedIndex = index;
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
@ -819,16 +742,6 @@ namespace Avalonia.Controls.Primitives
}
}
private void UpdateContainerSelection()
{
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(
container.ContainerControl,
Selection.IsSelected(container.Index) != false);
}
}
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
@ -844,52 +757,92 @@ namespace Avalonia.Controls.Primitives
}
}
private void UpdateFinished()
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param>
private int MarkItemSelected(object item, bool selected)
{
Selection.Source = Items;
var index = IndexOf(Items, item);
if (_updateSelectedItem != null)
if (index != -1)
{
SelectedItem = _updateSelectedItem;
MarkItemSelected(index, selected);
}
else
return index;
}
private void UpdateContainerSelection()
{
if (Presenter?.Panel is IPanel panel)
{
if (ItemCount == 0 && SelectedIndex != -1)
foreach (var container in panel.Children)
{
SelectedIndex = -1;
}
else
{
if (_updateSelectedIndex != int.MinValue)
{
SelectedIndex = _updateSelectedIndex;
}
if (AlwaysSelected && SelectedIndex == -1)
{
SelectedIndex = 0;
}
MarkContainerSelected(
container,
Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
}
}
}
private void InternalBeginInit()
private ISelectionModel CreateDefaultSelectionModel()
{
return new SelectionModel<object>
{
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
};
}
private void InitializeSelectionModel(ISelectionModel model)
{
if (_updateCount == 0)
if (_initializing == 0)
{
_updateSelectedIndex = int.MinValue;
model.Source = Items;
}
++_updateCount;
model.PropertyChanged += OnSelectionModelPropertyChanged;
model.SelectionChanged += OnSelectionModelSelectionChanged;
model.LostSelection += OnSelectionModelLostSelection;
if (model.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
_oldSelectedIndex = model.SelectedIndex;
_oldSelectedItem = model.SelectedItem;
if (AlwaysSelected && model.Count == 0)
{
model.SelectedIndex = 0;
}
UpdateContainerSelection();
_selectedItemsSync ??= new SelectedItemsSync(model);
_selectedItemsSync.SelectionModel = model;
if (SelectedIndex != -1)
{
RaiseEvent(new SelectionChangedEventArgs(
SelectionChangedEvent,
Array.Empty<object>(),
Selection.SelectedItems.ToList()));
}
}
private void InternalEndInit()
private void DeinitializeSelectionModel(ISelectionModel? model)
{
Debug.Assert(_updateCount > 0);
if (--_updateCount == 0)
if (model is object)
{
UpdateFinished();
model.PropertyChanged -= OnSelectionModelPropertyChanged;
model.SelectionChanged -= OnSelectionModelSelectionChanged;
}
}
}

49
src/Avalonia.Controls/SelectedItems.cs

@ -1,49 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
public interface ISelectedItemInfo
{
public IndexPath Path { get; }
}
internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue>
where Tinfo : ISelectedItemInfo
{
private readonly List<Tinfo> _infos;
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl;
public SelectedItems(
List<Tinfo> infos,
int count,
Func<List<Tinfo>, int, TValue> getAtImpl)
{
_infos = infos;
_getAtImpl = getAtImpl;
Count = count;
}
public TValue this[int index] => _getAtImpl(_infos, index);
public int Count { get; }
public IEnumerator<TValue> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

66
src/Avalonia.Controls/Selection/ISelectionModel.cs

@ -0,0 +1,66 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
#nullable enable
namespace Avalonia.Controls.Selection
{
public interface ISelectionModel : INotifyPropertyChanged
{
IEnumerable? Source { get; set; }
bool SingleSelect { get; set; }
int SelectedIndex { get; set; }
IReadOnlyList<int> SelectedIndexes { get; }
object? SelectedItem { get; set; }
IReadOnlyList<object?> SelectedItems { get; }
int AnchorIndex { get; set; }
int Count { get; }
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
public event EventHandler? LostSelection;
public event EventHandler? SourceReset;
public void BeginBatchUpdate();
public void EndBatchUpdate();
bool IsSelected(int index);
void Select(int index);
void Deselect(int index);
void SelectRange(int start, int end);
void DeselectRange(int start, int end);
void SelectAll();
void Clear();
}
public static class SelectionModelExtensions
{
public static IDisposable BatchUpdate(this ISelectionModel model)
{
return new BatchUpdateOperation(model);
}
public struct BatchUpdateOperation : IDisposable
{
private readonly ISelectionModel _owner;
private bool _isDisposed;
public BatchUpdateOperation(ISelectionModel owner)
{
_owner = owner;
_isDisposed = false;
owner.BeginBatchUpdate();
}
public void Dispose()
{
if (!_isDisposed)
{
_owner?.EndBatchUpdate();
_isDisposed = true;
}
}
}
}
}

82
src/Avalonia.Controls/IndexRange.cs → src/Avalonia.Controls/Selection/IndexRange.cs

@ -8,12 +8,18 @@ using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
namespace Avalonia.Controls.Selection
{
internal readonly struct IndexRange : IEquatable<IndexRange>
{
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
public IndexRange(int index)
{
Begin = index;
End = index;
}
public IndexRange(int begin, int end)
{
// Accept out of order begin/end pairs, just swap them.
@ -87,6 +93,43 @@ namespace Avalonia.Controls
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
public static bool Contains(IReadOnlyList<IndexRange>? ranges, int index)
{
if (ranges is null || index < 0)
{
return false;
}
foreach (var range in ranges)
{
if (range.Contains(index))
{
return true;
}
}
return false;
}
public static int GetAt(IReadOnlyList<IndexRange> ranges, int index)
{
var currentIndex = 0;
foreach (var range in ranges)
{
var currentCount = range.Count;
if (index >= currentIndex && index < currentIndex + currentCount)
{
return range.Begin + (index - currentIndex);
}
currentIndex += currentCount;
}
throw new IndexOutOfRangeException("The index was out of range.");
}
public static int Add(
IList<IndexRange> ranges,
IndexRange range,
@ -132,6 +175,21 @@ namespace Avalonia.Controls
return result;
}
public static int Add(
IList<IndexRange> destination,
IReadOnlyList<IndexRange> source,
IList<IndexRange>? added = null)
{
var result = 0;
foreach (var range in source)
{
result += Add(destination, range, added);
}
return result;
}
public static int Intersect(
IList<IndexRange> ranges,
IndexRange range,
@ -180,10 +238,15 @@ namespace Avalonia.Controls
}
public static int Remove(
IList<IndexRange> ranges,
IList<IndexRange>? ranges,
IndexRange range,
IList<IndexRange>? removed = null)
{
if (ranges is null)
{
return 0;
}
var result = 0;
for (var i = 0; i < ranges.Count; ++i)
@ -224,15 +287,16 @@ namespace Avalonia.Controls
return result;
}
public static IEnumerable<IndexRange> Subtract(
IndexRange lhs,
IEnumerable<IndexRange> rhs)
public static int Remove(
IList<IndexRange> destination,
IReadOnlyList<IndexRange> source,
IList<IndexRange>? added = null)
{
var result = new List<IndexRange> { lhs };
foreach (var range in rhs)
var result = 0;
foreach (var range in source)
{
Remove(result, range);
result += Remove(destination, range, added);
}
return result;

82
src/Avalonia.Controls/Selection/SelectedIndexes.cs

@ -0,0 +1,82 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class SelectedIndexes<T> : IReadOnlyList<int>
{
private readonly SelectionModel<T>? _owner;
private readonly IReadOnlyList<IndexRange>? _ranges;
public SelectedIndexes(SelectionModel<T> owner) => _owner = owner;
public SelectedIndexes(IReadOnlyList<IndexRange> ranges) => _ranges = ranges;
public int this[int index]
{
get
{
if (index >= Count)
{
throw new IndexOutOfRangeException("The index was out of range.");
}
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex;
}
else
{
return IndexRange.GetAt(Ranges!, index);
}
}
}
public int Count
{
get
{
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex == -1 ? 0 : 1;
}
else
{
return IndexRange.GetCount(Ranges!);
}
}
}
private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!;
public IEnumerator<int> GetEnumerator()
{
IEnumerator<int> SingleSelect()
{
if (_owner.SelectedIndex >= 0)
{
yield return _owner.SelectedIndex;
}
}
if (_owner?.SingleSelect == true)
{
return SingleSelect();
}
else
{
return IndexRange.EnumerateIndices(Ranges).GetEnumerator();
}
}
public static SelectedIndexes<T>? Create(IReadOnlyList<IndexRange>? ranges)
{
return ranges is object ? new SelectedIndexes<T>(ranges) : null;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

121
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -0,0 +1,121 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class SelectedItems<T> : IReadOnlyList<T>
{
private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items;
private readonly IReadOnlyList<IndexRange>? _ranges;
public SelectedItems(SelectionModel<T> owner) => _owner = owner;
public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items)
{
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
_items = items;
}
[MaybeNull]
public T this[int index]
{
#pragma warning disable CS8766
get
#pragma warning restore CS8766
{
if (index >= Count)
{
throw new IndexOutOfRangeException("The index was out of range.");
}
if (_owner?.SingleSelect == true)
{
return _owner.SelectedItem;
}
else if (Items is object)
{
return Items[index];
}
else
{
return default;
}
}
}
public int Count
{
get
{
if (_owner?.SingleSelect == true)
{
return _owner.SelectedIndex == -1 ? 0 : 1;
}
else
{
return Ranges is object ? IndexRange.GetCount(Ranges) : 0;
}
}
}
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator()
{
if (_owner?.SingleSelect == true)
{
if (_owner.SelectedIndex >= 0)
{
#pragma warning disable CS8603
yield return _owner.SelectedItem;
#pragma warning restore CS8603
}
}
else
{
var items = Items;
foreach (var range in Ranges!)
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8603
yield return items is object ? items[i] : default;
#pragma warning restore CS8603
}
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static SelectedItems<T>? Create(
IReadOnlyList<IndexRange>? ranges,
ItemsSourceView<T>? items)
{
return ranges is object ? new SelectedItems<T>(ranges, items) : null;
}
public class Untyped : IReadOnlyList<object?>
{
private readonly IReadOnlyList<T> _source;
public Untyped(IReadOnlyList<T> source) => _source = source;
public object? this[int index] => _source[index];
public int Count => _source.Count;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<object?> GetEnumerator()
{
foreach (var i in _source)
{
yield return i;
}
}
}
}
}

749
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -0,0 +1,749 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
#nullable enable
namespace Avalonia.Controls.Selection
{
public class SelectionModel<T> : SelectionNodeBase<T>, ISelectionModel
{
private bool _singleSelect = true;
private int _anchorIndex = -1;
private int _selectedIndex = -1;
private Operation? _operation;
private SelectedIndexes<T>? _selectedIndexes;
private SelectedItems<T>? _selectedItems;
private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
[AllowNull] private T _initSelectedItem = default;
private bool _hasInitSelectedItem;
public SelectionModel()
{
}
public SelectionModel(IEnumerable<T>? source)
{
Source = source;
}
public new IEnumerable<T>? Source
{
get => base.Source as IEnumerable<T>;
set => SetSource(value);
}
public bool SingleSelect
{
get => _singleSelect;
set
{
if (_singleSelect != value)
{
if (value == true)
{
using var update = BatchUpdate();
var selectedIndex = SelectedIndex;
Clear();
SelectedIndex = selectedIndex;
}
_singleSelect = value;
RangesEnabled = !value;
if (RangesEnabled && _selectedIndex >= 0)
{
CommitSelect(new IndexRange(_selectedIndex));
}
RaisePropertyChanged(nameof(SingleSelect));
}
}
}
public int SelectedIndex
{
get => _selectedIndex;
set
{
using var update = BatchUpdate();
Clear();
Select(value);
}
}
public IReadOnlyList<int> SelectedIndexes => _selectedIndexes ??= new SelectedIndexes<T>(this);
[MaybeNull, AllowNull]
public T SelectedItem
{
get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem;
set
{
if (ItemsView is object)
{
SelectedIndex = ItemsView.IndexOf(value!);
}
else
{
Clear();
_initSelectedItem = value;
_hasInitSelectedItem = true;
}
}
}
public IReadOnlyList<T> SelectedItems
{
get
{
if (ItemsView is null && _hasInitSelectedItem)
{
return new[] { _initSelectedItem };
}
return _selectedItems ??= new SelectedItems<T>(this);
}
}
public int AnchorIndex
{
get => _anchorIndex;
set
{
using var update = BatchUpdate();
var index = CoerceIndex(value);
update.Operation.AnchorIndex = index;
}
}
public int Count
{
get
{
if (SingleSelect)
{
return _selectedIndex >= 0 ? 1 : 0;
}
else
{
return IndexRange.GetCount(Ranges);
}
}
}
IEnumerable? ISelectionModel.Source
{
get => Source;
set => SetSource(value);
}
object? ISelectionModel.SelectedItem
{
get => SelectedItem;
set
{
if (value is T t)
{
SelectedItem = t;
}
else
{
SelectedIndex = -1;
}
}
}
IReadOnlyList<object?> ISelectionModel.SelectedItems
{
get => _selectedItemsUntyped ??= new SelectedItems<T>.Untyped(SelectedItems);
}
public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs<T>>? SelectionChanged;
public event EventHandler? LostSelection;
public event EventHandler? SourceReset;
public event PropertyChangedEventHandler? PropertyChanged;
event EventHandler<SelectionModelSelectionChangedEventArgs>? ISelectionModel.SelectionChanged
{
add => _untypedSelectionChanged += value;
remove => _untypedSelectionChanged -= value;
}
public BatchUpdateOperation BatchUpdate() => new BatchUpdateOperation(this);
public void BeginBatchUpdate()
{
_operation ??= new Operation(this);
++_operation.UpdateCount;
}
public void EndBatchUpdate()
{
if (_operation is null || _operation.UpdateCount == 0)
{
throw new InvalidOperationException("No batch update in progress.");
}
if (--_operation.UpdateCount == 0)
{
// If the collection is currently changing, commit the update when the
// collection change finishes.
if (!IsSourceCollectionChanging)
{
CommitOperation(_operation);
}
}
}
public bool IsSelected(int index)
{
if (index < 0)
{
return false;
}
else if (SingleSelect)
{
return _selectedIndex == index;
}
else
{
return IndexRange.Contains(Ranges, index);
}
}
public void Select(int index) => SelectRange(index, index, false, true);
public void Deselect(int index) => DeselectRange(index, index);
public void SelectRange(int start, int end) => SelectRange(start, end, false, false);
public void DeselectRange(int start, int end)
{
using var update = BatchUpdate();
var o = update.Operation;
var range = CoerceRange(start, end);
if (range.Begin == -1)
{
return;
}
if (RangesEnabled)
{
var selected = Ranges.ToList();
var deselected = new List<IndexRange>();
var operationDeselected = new List<IndexRange>();
o.DeselectedRanges ??= new List<IndexRange>();
IndexRange.Remove(o.SelectedRanges, range, operationDeselected);
IndexRange.Remove(selected, range, deselected);
IndexRange.Add(o.DeselectedRanges, deselected);
if (IndexRange.Contains(deselected, o.SelectedIndex) ||
IndexRange.Contains(operationDeselected, o.SelectedIndex))
{
o.SelectedIndex = GetFirstSelectedIndexFromRanges(except: deselected);
}
}
else if(range.Contains(_selectedIndex))
{
o.SelectedIndex = -1;
}
_initSelectedItem = default;
_hasInitSelectedItem = false;
}
public void SelectAll() => SelectRange(0, int.MaxValue);
public void Clear() => DeselectRange(0, int.MaxValue);
protected void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void SetSource(IEnumerable? value)
{
if (base.Source != value)
{
if (_operation is object)
{
throw new InvalidOperationException("Cannot change source while update is in progress.");
}
if (base.Source is object && value is object)
{
using var update = BatchUpdate();
update.Operation.SkipLostSelection = true;
Clear();
}
base.Source = value;
using (var update = BatchUpdate())
{
update.Operation.IsSourceUpdate = true;
if (_hasInitSelectedItem)
{
SelectedItem = _initSelectedItem;
_initSelectedItem = default;
_hasInitSelectedItem = false;
}
else
{
TrimInvalidSelections(update.Operation);
}
RaisePropertyChanged(nameof(Source));
}
}
}
private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
}
private protected override void OnSourceReset()
{
_selectedIndex = _anchorIndex = -1;
CommitDeselect(new IndexRange(0, int.MaxValue));
if (SourceReset is object)
{
SourceReset.Invoke(this, EventArgs.Empty);
}
else
{
//Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
// this,
// "SelectionModel received Reset but no SourceReset handler was registered to handle it. " +
// "Selection may be out of sync.",
// typeof(SelectionModel));
}
}
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
{
// Note: We're *not* putting this in a using scope. A collection update is still in progress
// so the operation won't get commited by normal means: we have to commit it manually.
var update = BatchUpdate();
update.Operation.DeselectedItems = deselectedItems;
if (_selectedIndex == -1 && LostSelection is object)
{
LostSelection(this, EventArgs.Empty);
}
CommitOperation(update.Operation);
}
private protected override CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = SelectedIndex >= index;
var shiftCount = shifted ? count : 0;
_selectedIndex += shiftCount;
_anchorIndex += shiftCount;
var baseResult = base.OnItemsAdded(index, items);
shifted |= baseResult.ShiftDelta != 0;
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? count : 0,
};
}
private protected override CollectionChangeState OnItemsRemoved(int index, IList items)
{
var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1);
var shifted = false;
List<T>? removed;
var baseResult = base.OnItemsRemoved(index, items);
shifted |= baseResult.ShiftDelta != 0;
removed = baseResult.RemovedItems;
if (removedRange.Contains(SelectedIndex))
{
if (SingleSelect)
{
#pragma warning disable CS8604
removed = new List<T> { (T)items[SelectedIndex - index] };
#pragma warning restore CS8604
}
_selectedIndex = GetFirstSelectedIndexFromRanges();
}
else if (SelectedIndex >= index)
{
_selectedIndex -= count;
shifted = true;
}
if (removedRange.Contains(AnchorIndex))
{
_anchorIndex = GetFirstSelectedIndexFromRanges();
}
else if (AnchorIndex >= index)
{
_anchorIndex -= count;
shifted = true;
}
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? -count : 0,
RemovedItems = removed,
};
}
private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_operation?.UpdateCount > 0)
{
throw new InvalidOperationException("Source collection was modified during selection update.");
}
var oldAnchorIndex = _anchorIndex;
var oldSelectedIndex = _selectedIndex;
base.OnSourceCollectionChanged(e);
if (oldSelectedIndex != _selectedIndex)
{
RaisePropertyChanged(nameof(SelectedIndex));
}
if (oldAnchorIndex != _anchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
}
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
{
return false;
}
if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add)
{
if (e.NewStartingIndex <= _selectedIndex)
{
return _selectedIndex + e.NewItems.Count < ItemsView.Count;
}
if (e.NewStartingIndex <= _anchorIndex)
{
return _anchorIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
protected override void OnSourceCollectionChangeFinished()
{
if (_operation is object)
{
CommitOperation(_operation);
}
}
private int GetFirstSelectedIndexFromRanges(List<IndexRange>? except = null)
{
if (RangesEnabled)
{
var count = IndexRange.GetCount(Ranges);
var index = 0;
while (index < count)
{
var result = IndexRange.GetAt(Ranges, index++);
if (!IndexRange.Contains(except, result))
{
return result;
}
}
}
return -1;
}
private void SelectRange(
int start,
int end,
bool forceSelectedIndex,
bool forceAnchorIndex)
{
if (SingleSelect && start != end)
{
throw new InvalidOperationException("Cannot select range with single selection.");
}
var range = CoerceRange(start, end);
if (range.Begin == -1)
{
return;
}
using var update = BatchUpdate();
var o = update.Operation;
var selected = new List<IndexRange>();
if (RangesEnabled)
{
o.SelectedRanges ??= new List<IndexRange>();
IndexRange.Remove(o.DeselectedRanges, range);
IndexRange.Add(o.SelectedRanges, range);
IndexRange.Remove(o.SelectedRanges, Ranges);
if (o.SelectedIndex == -1 || forceSelectedIndex)
{
o.SelectedIndex = range.Begin;
}
if (o.AnchorIndex == -1 || forceAnchorIndex)
{
o.AnchorIndex = range.Begin;
}
}
else
{
o.SelectedIndex = o.AnchorIndex = start;
}
_initSelectedItem = default;
_hasInitSelectedItem = false;
}
[return: MaybeNull]
private T GetItemAt(int index)
{
if (ItemsView is null || index < 0 || index >= ItemsView.Count)
{
return default;
}
return ItemsView[index];
}
private int CoerceIndex(int index)
{
index = Math.Max(index, -1);
if (ItemsView is object && index >= ItemsView.Count)
{
index = -1;
}
return index;
}
private IndexRange CoerceRange(int start, int end)
{
var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
if (start > max || (start < 0 && end < 0))
{
return new IndexRange(-1);
}
start = Math.Max(start, 0);
end = Math.Min(end, max);
return new IndexRange(start, end);
}
private void TrimInvalidSelections(Operation operation)
{
if (ItemsView is null)
{
return;
}
var max = ItemsView.Count - 1;
if (operation.SelectedIndex > max)
{
operation.SelectedIndex = GetFirstSelectedIndexFromRanges();
}
if (operation.AnchorIndex > max)
{
operation.AnchorIndex = GetFirstSelectedIndexFromRanges();
}
if (RangesEnabled && Ranges.Count > 0)
{
var selected = Ranges.ToList();
if (max < 0)
{
operation.DeselectedRanges = selected;
}
else
{
var valid = new IndexRange(0, max);
var removed = new List<IndexRange>();
IndexRange.Intersect(selected, valid, removed);
operation.DeselectedRanges = removed;
}
}
}
private void CommitOperation(Operation operation)
{
try
{
var oldAnchorIndex = _anchorIndex;
var oldSelectedIndex = _selectedIndex;
var indexesChanged = false;
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
{
operation.UpdateCount++;
LostSelection?.Invoke(this, EventArgs.Empty);
}
_selectedIndex = operation.SelectedIndex;
_anchorIndex = operation.AnchorIndex;
if (operation.SelectedRanges is object)
{
indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
}
if (operation.DeselectedRanges is object)
{
indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
}
if (SelectionChanged is object || _untypedSelectionChanged is object)
{
IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
if (SingleSelect && oldSelectedIndex != _selectedIndex)
{
if (oldSelectedIndex != -1)
{
deselected = new[] { new IndexRange(oldSelectedIndex) };
}
if (_selectedIndex != -1)
{
selected = new[] { new IndexRange(_selectedIndex) };
}
}
if (deselected?.Count > 0 || selected?.Count > 0 || operation.DeselectedItems is object)
{
// If the operation was caused by Source being updated, then use a null source
// so that the items will appear as nulls.
var deselectedSource = operation.IsSourceUpdate ? null : ItemsView;
// If the operation contains DeselectedItems then we're notifying a source
// CollectionChanged event. LostFocus may have caused another item to have been
// selected, but it can't have caused a deselection (as it was called due to
// selection being lost) so we're ok to discard `deselected` here.
var deselectedItems = operation.DeselectedItems ??
SelectedItems<T>.Create(deselected, deselectedSource);
var e = new SelectionModelSelectionChangedEventArgs<T>(
SelectedIndexes<T>.Create(deselected),
SelectedIndexes<T>.Create(selected),
deselectedItems,
SelectedItems<T>.Create(selected, ItemsView));
SelectionChanged?.Invoke(this, e);
_untypedSelectionChanged?.Invoke(this, e);
}
}
if (oldSelectedIndex != _selectedIndex)
{
indexesChanged = true;
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedItem));
}
if (oldAnchorIndex != _anchorIndex)
{
indexesChanged = true;
RaisePropertyChanged(nameof(AnchorIndex));
}
if (indexesChanged)
{
RaisePropertyChanged(nameof(SelectedIndexes));
RaisePropertyChanged(nameof(SelectedItems));
}
}
finally
{
_operation = null;
}
}
public struct BatchUpdateOperation : IDisposable
{
private readonly SelectionModel<T> _owner;
private bool _isDisposed;
public BatchUpdateOperation(SelectionModel<T> owner)
{
_owner = owner;
_isDisposed = false;
owner.BeginBatchUpdate();
}
internal Operation Operation => _owner._operation!;
public void Dispose()
{
if (!_isDisposed)
{
_owner?.EndBatchUpdate();
_isDisposed = true;
}
}
}
internal class Operation
{
public Operation(SelectionModel<T> owner)
{
AnchorIndex = owner.AnchorIndex;
SelectedIndex = owner.SelectedIndex;
}
public int UpdateCount { get; set; }
public bool IsSourceUpdate { get; set; }
public bool SkipLostSelection { get; set; }
public int AnchorIndex { get; set; }
public int SelectedIndex { get; set; }
public List<IndexRange>? SelectedRanges { get; set; }
public List<IndexRange>? DeselectedRanges { get; set; }
public IReadOnlyList<T>? DeselectedItems { get; set; }
}
}
}

18
src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs

@ -0,0 +1,18 @@
using System;
#nullable enable
namespace Avalonia.Controls.Selection
{
public class SelectionModelIndexesChangedEventArgs : EventArgs
{
public SelectionModelIndexesChangedEventArgs(int startIndex, int delta)
{
StartIndex = startIndex;
Delta = delta;
}
public int StartIndex { get; }
public int Delta { get; }
}
}

85
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -0,0 +1,85 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls.Selection;
#nullable enable
namespace Avalonia.Controls.Selection
{
public abstract class SelectionModelSelectionChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the indexes of the items that were removed from the selection.
/// </summary>
public abstract IReadOnlyList<int> DeselectedIndexes { get; }
/// <summary>
/// Gets the indexes of the items that were added to the selection.
/// </summary>
public abstract IReadOnlyList<int> SelectedIndexes { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object?> DeselectedItems => GetUntypedDeselectedItems();
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object?> SelectedItems => GetUntypedSelectedItems();
protected abstract IReadOnlyList<object?> GetUntypedDeselectedItems();
protected abstract IReadOnlyList<object?> GetUntypedSelectedItems();
}
public class SelectionModelSelectionChangedEventArgs<T> : SelectionModelSelectionChangedEventArgs
{
private IReadOnlyList<object?>? _deselectedItems;
private IReadOnlyList<object?>? _selectedItems;
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null)
{
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>();
DeselectedItems = deselectedItems ?? Array.Empty<T>();
SelectedItems = selectedItems ?? Array.Empty<T>();
}
/// <summary>
/// Gets the indexes of the items that were removed from the selection.
/// </summary>
public override IReadOnlyList<int> DeselectedIndexes { get; }
/// <summary>
/// Gets the indexes of the items that were added to the selection.
/// </summary>
public override IReadOnlyList<int> SelectedIndexes { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public new IReadOnlyList<T> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public new IReadOnlyList<T> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{
return _deselectedItems ??= (DeselectedItems as IReadOnlyList<object?>) ??
new SelectedItems<T>.Untyped(DeselectedItems);
}
protected override IReadOnlyList<object?> GetUntypedSelectedItems()
{
return _selectedItems ??= (SelectedItems as IReadOnlyList<object?>) ??
new SelectedItems<T>.Untyped(SelectedItems);
}
}
}

320
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -0,0 +1,320 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls.Selection
{
public abstract class SelectionNodeBase<T> : ICollectionChangedListener
{
private IEnumerable? _source;
private bool _rangesEnabled;
private List<IndexRange>? _ranges;
private int _collectionChanging;
protected IEnumerable? Source
{
get => _source;
set
{
if (_source != value)
{
ItemsView?.RemoveListener(this);
_source = value;
ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null;
ItemsView?.AddListener(this);
}
}
}
protected bool IsSourceCollectionChanging => _collectionChanging > 0;
protected bool RangesEnabled
{
get => _rangesEnabled;
set
{
if (_rangesEnabled != value)
{
_rangesEnabled = value;
if (!_rangesEnabled)
{
_ranges = null;
}
}
}
}
internal ItemsSourceView<T>? ItemsView { get; set; }
internal IReadOnlyList<IndexRange> Ranges
{
get
{
if (!RangesEnabled)
{
throw new InvalidOperationException("Ranges not enabled.");
}
return _ranges ??= new List<IndexRange>();
}
}
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
++_collectionChanging;
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
OnSourceCollectionChanged(e);
}
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
if (--_collectionChanging == 0)
{
OnSourceCollectionChangeFinished();
}
}
protected abstract void OnSourceCollectionChangeFinished();
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
private protected abstract void OnSourceReset();
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems);
private protected int CommitSelect(IndexRange range)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, range);
}
return 0;
}
private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, ranges);
}
return 0;
}
private protected int CommitDeselect(IndexRange range)
{
if (RangesEnabled)
{
_ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, range);
}
return 0;
}
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
{
if (RangesEnabled && _ranges is object)
{
return IndexRange.Remove(_ranges, ranges);
}
return 0;
}
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
{
var count = items.Count;
var shifted = false;
if (_ranges is object)
{
List<IndexRange>? toAdd = null;
for (var i = 0; i < Ranges!.Count; ++i)
{
var range = Ranges[i];
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
if (range.Contains(index - 1))
{
range.Split(index - 1, out var before, out _);
(toAdd ??= new List<IndexRange>()).Add(before);
begin = index;
}
// Shift the range to the right
_ranges[i] = new IndexRange(begin + count, range.End + count);
shifted = true;
}
}
if (toAdd is object)
{
foreach (var range in toAdd)
{
IndexRange.Add(_ranges, range);
}
}
}
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? count : 0,
};
}
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
{
var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1);
bool shifted = false;
List<T>? removed = null;
if (_ranges is object)
{
var deselected = new List<IndexRange>();
if (IndexRange.Remove(_ranges, removedRange, deselected) > 0)
{
removed = new List<T>();
foreach (var range in deselected)
{
for (var i = range.Begin; i <= range.End; ++i)
{
#pragma warning disable CS8604
removed.Add((T)items[i - index]);
#pragma warning restore CS8604
}
}
}
for (var i = 0; i < Ranges!.Count; ++i)
{
var existing = Ranges[i];
if (existing.End > removedRange.Begin)
{
_ranges[i] = new IndexRange(existing.Begin - count, existing.End - count);
shifted = true;
}
}
}
return new CollectionChangeState
{
ShiftIndex = index,
ShiftDelta = shifted ? -count : 0,
RemovedItems = removed,
};
}
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
var change = OnItemsAdded(e.NewStartingIndex, e.NewItems);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
break;
}
case NotifyCollectionChangedAction.Remove:
{
var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
shiftIndex = change.ShiftIndex;
shiftDelta = change.ShiftDelta;
removed = change.RemovedItems;
break;
}
case NotifyCollectionChangedAction.Replace:
{
var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems);
shiftIndex = removeChange.ShiftIndex;
shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
removed = removeChange.RemovedItems;
}
break;
case NotifyCollectionChangedAction.Reset:
OnSourceReset();
break;
}
if (shiftDelta != 0)
{
OnIndexesChanged(shiftIndex, shiftDelta);
}
if (removed is object)
{
OnSelectionChanged(removed);
}
}
private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
// If the selection is modified in a CollectionChanged handler before the selection
// model's CollectionChanged handler has had chance to run then we can end up with
// a selected index that refers to the *new* state of the Source intermixed with
// indexes that reference an old state of the source.
//
// There's not much we can do in this situation, so detect whether shifting the
// current selected indexes would result in an invalid index in the source, and if
// so bail.
//
// See unit test Handles_Selection_Made_In_CollectionChanged for more details.
if (ItemsView is object &&
RangesEnabled &&
Ranges.Count > 0 &&
e.Action == NotifyCollectionChangedAction.Add)
{
var lastIndex = Ranges.Last().End;
if (e.NewStartingIndex <= lastIndex)
{
return lastIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
private protected struct CollectionChangeState
{
public int ShiftIndex;
public int ShiftDelta;
public List<T>? RemovedItems;
}
}
}

894
src/Avalonia.Controls/SelectionModel.cs

@ -1,894 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls
{
public class SelectionModel : ISelectionModel, IDisposable
{
private readonly SelectionNode _rootNode;
private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
private IndexPath _oldAnchorIndex;
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
public event PropertyChangedEventHandler? PropertyChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
public SelectionModel()
{
_rootNode = new SelectionNode(this, null);
SharedLeafNode = new SelectionNode(this, null);
}
public object? Source
{
get => _rootNode.Source;
set
{
if (_rootNode.Source != value)
{
var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
if (_rootNode.Source != null)
{
// Temporarily prevent auto-select when switching source.
var restoreAutoSelect = _autoSelect;
_autoSelect = false;
try
{
using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
}
finally
{
_autoSelect = restoreAutoSelect;
}
}
_rootNode.Source = value;
ApplyAutoSelect(true);
RaisePropertyChanged("Source");
if (raiseChanged)
{
var e = new SelectionModelSelectionChangedEventArgs(
null,
SelectedIndices,
null,
SelectedItems);
OnSelectionChanged(e);
}
}
}
}
public bool SingleSelect
{
get => _singleSelect;
set
{
if (_singleSelect != value)
{
_singleSelect = value;
var selectedIndices = SelectedIndices;
if (value && selectedIndices != null && selectedIndices.Count > 0)
{
using var operation = new Operation(this);
// We want to be single select, so make sure there is only
// one selected item.
var firstSelectionIndexPath = selectedIndices[0];
ClearSelection(resetAnchor: true);
SelectWithPathImpl(firstSelectionIndexPath, select: true);
SelectedIndex = firstSelectionIndexPath;
}
RaisePropertyChanged("SingleSelect");
}
}
}
public bool RetainSelectionOnReset
{
get => _rootNode.RetainSelectionOnReset;
set => _rootNode.RetainSelectionOnReset = value;
}
public bool AutoSelect
{
get => _autoSelect;
set
{
if (_autoSelect != value)
{
_autoSelect = value;
ApplyAutoSelect(true);
}
}
}
public IndexPath AnchorIndex
{
get
{
IndexPath anchor = default;
if (_rootNode.AnchorIndex >= 0)
{
var path = new List<int>();
SelectionNode? current = _rootNode;
while (current?.AnchorIndex >= 0)
{
path.Add(current.AnchorIndex);
current = current.GetAt(current.AnchorIndex, false, default);
}
anchor = new IndexPath(path);
}
return anchor;
}
set
{
var oldValue = AnchorIndex;
if (value != null)
{
SelectionTreeHelper.TraverseIndexPath(
_rootNode,
value,
realizeChildren: true,
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
}
else
{
_rootNode.AnchorIndex = -1;
}
if (_operationCount == 0 && oldValue != AnchorIndex)
{
RaisePropertyChanged("AnchorIndex");
}
}
}
public IndexPath SelectedIndex
{
get
{
IndexPath selectedIndex = default;
var selectedIndices = SelectedIndices;
if (selectedIndices?.Count > 0)
{
selectedIndex = selectedIndices[0];
}
return selectedIndex;
}
set
{
if (!IsSelectedAt(value) || SelectedItems.Count > 1)
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
SelectWithPathImpl(value, select: true);
}
}
}
public object? SelectedItem
{
get
{
object? item = null;
var selectedItems = SelectedItems;
if (selectedItems?.Count > 0)
{
item = selectedItems[0];
}
return item;
}
}
public IReadOnlyList<object?> SelectedItems
{
get
{
if (_selectedItemsCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
var count = 0;
if (_rootNode.Source != null)
{
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: false,
currentInfo =>
{
if (currentInfo.Node.SelectedCount > 0)
{
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
count += currentInfo.Node.SelectedCount;
}
});
}
// Instead of creating a dumb vector that takes up the space for all the selected items,
// we create a custom IReadOnlyList implementation that calls back using a delegate to find
// the selected item at a particular index. This avoid having to create the storage and copying
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
// easier to consume flat vector view of objects.
var selectedItems = new SelectedItems<object?, SelectedItemInfo> (
selectedInfos,
count,
(infos, index) =>
{
var currentIndex = 0;
object? item = null;
foreach (var info in infos)
{
var node = info.Node;
if (node != null)
{
var currentCount = node.SelectedCount;
if (index >= currentIndex && index < currentIndex + currentCount)
{
var targetIndex = node.SelectedIndices[index - currentIndex];
item = node.ItemsSourceView!.GetAt(targetIndex);
break;
}
currentIndex += currentCount;
}
else
{
throw new InvalidOperationException(
"Selection has changed since SelectedItems property was read.");
}
}
return item;
});
_selectedItemsCached = selectedItems;
}
return _selectedItemsCached;
}
}
public IReadOnlyList<IndexPath> SelectedIndices
{
get
{
if (_selectedIndicesCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
var count = 0;
SelectionTreeHelper.Traverse(
_rootNode,
false,
currentInfo =>
{
if (currentInfo.Node.SelectedCount > 0)
{
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
count += currentInfo.Node.SelectedCount;
}
});
// Instead of creating a dumb vector that takes up the space for all the selected indices,
// we create a custom VectorView implimentation that calls back using a delegate to find
// the IndexPath at a particular index. This avoid having to create the storage and copying
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
// easier to consume flat vector view of IndexPaths.
var indices = new SelectedItems<IndexPath, SelectedItemInfo>(
selectedInfos,
count,
(infos, index) => // callback for GetAt(index)
{
var currentIndex = 0;
IndexPath path = default;
foreach (var info in infos)
{
var node = info.Node;
if (node != null)
{
var currentCount = node.SelectedCount;
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = node.SelectedIndices[index - currentIndex];
path = info.Path.CloneWithChildIndex(targetIndex);
break;
}
currentIndex += currentCount;
}
else
{
throw new InvalidOperationException(
"Selection has changed since SelectedIndices property was read.");
}
}
return path;
});
_selectedIndicesCached = indices;
}
return _selectedIndicesCached;
}
}
internal SelectionNode SharedLeafNode { get; private set; }
public void Dispose()
{
ClearSelection(resetAnchor: false);
_rootNode.Cleanup();
_rootNode.Dispose();
_selectedIndicesCached = null;
_selectedItemsCached = null;
}
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index);
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index);
public void Select(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: true);
}
public void Select(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: true);
}
public void SelectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: true);
}
public void Deselect(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: false);
}
public void Deselect(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
}
public void DeselectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: false);
}
public bool IsSelected(int index) => _rootNode.IsSelected(index);
public bool IsSelected(int grouIndex, int itemIndex)
{
return IsSelectedAt(new IndexPath(grouIndex, itemIndex));
}
public bool IsSelectedAt(IndexPath index)
{
var path = index;
SelectionNode? node = _rootNode;
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, false, default);
if (node == null)
{
return false;
}
}
return node.IsSelected(index.GetAt(index.GetSize() - 1));
}
public bool? IsSelectedWithPartial(int index)
{
if (index < 0)
{
throw new ArgumentException("Index must be >= 0", nameof(index));
}
var isSelected = _rootNode.IsSelectedWithPartial(index);
return isSelected;
}
public bool? IsSelectedWithPartial(int groupIndex, int itemIndex)
{
if (groupIndex < 0)
{
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex));
}
if (itemIndex < 0)
{
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex));
}
var isSelected = (bool?)false;
var childNode = _rootNode.GetAt(groupIndex, false, default);
if (childNode != null)
{
isSelected = childNode.IsSelectedWithPartial(itemIndex);
}
return isSelected;
}
public bool? IsSelectedWithPartialAt(IndexPath index)
{
var path = index;
var isRealized = true;
SelectionNode? node = _rootNode;
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, false, default);
if (node == null)
{
isRealized = false;
break;
}
}
var isSelected = (bool?)false;
if (isRealized)
{
var size = path.GetSize();
if (size == 0)
{
isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes());
}
else
{
isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1));
}
}
return isSelected;
}
public void SelectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: true);
}
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
}
public void SelectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: true);
}
public void DeselectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: false);
}
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
}
public void DeselectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: false);
}
public void SelectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: true);
}
public void DeselectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: false);
}
public void SelectAll()
{
using var operation = new Operation(this);
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: true,
info =>
{
if (info.Node.DataCount > 0)
{
info.Node.SelectAll();
}
});
}
public void ClearSelection()
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
}
public IDisposable Update() => new Operation(this);
protected void OnPropertyChanged(string propertyName)
{
RaisePropertyChanged(propertyName);
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnSelectionInvalidatedDueToCollectionChange(
bool selectionInvalidated,
IReadOnlyList<object?>? removedItems)
{
SelectionModelSelectionChangedEventArgs? e = null;
if (selectionInvalidated)
{
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
}
OnSelectionChanged(e);
ApplyAutoSelect(true);
}
internal IObservable<object?>? ResolvePath(
object data,
IndexPath dataIndexPath,
IndexPath finalIndexPath)
{
IObservable<object?>? resolved = null;
// Raise ChildrenRequested event if there is a handler
if (ChildrenRequested != null)
{
if (_childrenRequestedEventArgs == null)
{
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(
data,
dataIndexPath,
finalIndexPath,
false);
}
else
{
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false);
}
ChildrenRequested(this, _childrenRequestedEventArgs);
resolved = _childrenRequestedEventArgs.Children;
// Clear out the values in the args so that it cannot be used after the event handler call.
_childrenRequestedEventArgs.Initialize(null, default, default, true);
}
return resolved;
}
private void ClearSelection(bool resetAnchor)
{
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: false,
info => info.Node.Clear());
if (resetAnchor)
{
AnchorIndex = default;
}
OnSelectionChanged();
}
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
{
_selectedIndicesCached = null;
_selectedItemsCached = null;
if (e != null)
{
SelectionChanged?.Invoke(this, e);
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
}
}
}
private void SelectImpl(int index, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
var selected = _rootNode.Select(index, select);
if (selected)
{
AnchorIndex = new IndexPath(index);
}
OnSelectionChanged();
}
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex));
var selected = childNode!.Select(itemIndex, select);
if (selected)
{
AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
OnSelectionChanged();
}
private void SelectWithPathImpl(IndexPath index, bool select)
{
bool selected = false;
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
SelectionTreeHelper.TraverseIndexPath(
_rootNode,
index,
true,
(currentNode, path, depth, childIndex) =>
{
if (depth == path.GetSize() - 1)
{
selected = currentNode.Select(childIndex, select);
}
}
);
if (selected)
{
AnchorIndex = index;
}
OnSelectionChanged();
}
private void SelectRangeFromAnchorImpl(int index, bool select)
{
int anchorIndex = 0;
var anchor = AnchorIndex;
if (anchor != null)
{
anchorIndex = anchor.GetAt(0);
}
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
OnSelectionChanged();
}
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
{
var startGroupIndex = 0;
var startItemIndex = 0;
var anchorIndex = AnchorIndex;
if (anchorIndex != null)
{
startGroupIndex = anchorIndex.GetAt(0);
startItemIndex = anchorIndex.GetAt(1);
}
// Make sure start > end
if (startGroupIndex > endGroupIndex ||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex))
{
int temp = startGroupIndex;
startGroupIndex = endGroupIndex;
endGroupIndex = temp;
temp = startItemIndex;
startItemIndex = endItemIndex;
endItemIndex = temp;
}
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
{
var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
OnSelectionChanged();
}
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
{
var winrtStart = start;
var winrtEnd = end;
// Make sure start <= end
if (winrtEnd.CompareTo(winrtStart) == -1)
{
var temp = winrtStart;
winrtStart = winrtEnd;
winrtEnd = temp;
}
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
SelectionTreeHelper.TraverseRangeRealizeChildren(
_rootNode,
winrtStart,
winrtEnd,
info =>
{
if (info.Path >= winrtStart && info.Path <= winrtEnd)
{
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
}
});
OnSelectionChanged();
}
private void BeginOperation()
{
if (_operationCount++ == 0)
{
_oldAnchorIndex = AnchorIndex;
_rootNode.BeginOperation();
}
}
private void EndOperation()
{
if (_operationCount == 0)
{
throw new AvaloniaInternalException("No selection operation in progress.");
}
SelectionModelSelectionChangedEventArgs? e = null;
if (--_operationCount == 0)
{
ApplyAutoSelect(false);
var changes = new List<SelectionNodeOperation>();
_rootNode.EndOperation(changes);
if (changes.Count > 0)
{
var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs();
}
OnSelectionChanged(e);
if (_oldAnchorIndex != AnchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
_rootNode.Cleanup();
_oldAnchorIndex = default;
}
}
private void ApplyAutoSelect(bool createOperation)
{
if (AutoSelect)
{
_selectedIndicesCached = null;
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
{
if (createOperation)
{
using var operation = new Operation(this);
SelectImpl(0, true);
}
else
{
SelectImpl(0, true);
}
}
}
}
internal class SelectedItemInfo : ISelectedItemInfo
{
public SelectedItemInfo(SelectionNode node, IndexPath path)
{
Node = node;
Path = path;
}
public SelectionNode Node { get; }
public IndexPath Path { get; }
public int Count => Node.SelectedCount;
}
private struct Operation : IDisposable
{
private readonly SelectionModel _manager;
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
public void Dispose() => _manager.EndOperation();
}
}
}

170
src/Avalonia.Controls/SelectionModelChangeSet.cs

@ -1,170 +0,0 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionModelChangeSet
{
private readonly List<SelectionNodeOperation> _changes;
public SelectionModelChangeSet(List<SelectionNodeOperation> changes)
{
_changes = changes;
}
public SelectionModelSelectionChangedEventArgs CreateEventArgs()
{
var deselectedIndexCount = 0;
var selectedIndexCount = 0;
var deselectedItemCount = 0;
var selectedItemCount = 0;
foreach (var change in _changes)
{
deselectedIndexCount += change.DeselectedCount;
selectedIndexCount += change.SelectedCount;
if (change.Items != null)
{
deselectedItemCount += change.DeselectedCount;
selectedItemCount += change.SelectedCount;
}
}
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes,
deselectedIndexCount,
GetDeselectedIndexAt);
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes,
selectedIndexCount,
GetSelectedIndexAt);
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
deselectedItemCount,
GetDeselectedItemAt);
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
selectedItemCount,
GetSelectedItemAt);
return new SelectionModelSelectionChangedEventArgs(
deselectedIndices,
selectedIndices,
deselectedItems,
selectedItems);
}
private IndexPath GetDeselectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetSelectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetDeselectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetSelectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetIndexAt(
List<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{
var currentIndex = 0;
IndexPath path = default;
foreach (var info in infos)
{
var currentCount = getCount(info);
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
path = info.Path.CloneWithChildIndex(targetIndex);
break;
}
currentIndex += currentCount;
}
return path;
}
private object? GetItemAt(
List<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{
var currentIndex = 0;
object? item = null;
foreach (var info in infos)
{
var currentCount = getCount(info);
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null;
break;
}
currentIndex += currentCount;
}
return item;
}
private int GetIndexAt(List<IndexRange>? ranges, int index)
{
var currentIndex = 0;
if (ranges != null)
{
foreach (var range in ranges)
{
var currentCount = (range.End - range.Begin) + 1;
if (index >= currentIndex && index < currentIndex + currentCount)
{
return range.Begin + (index - currentIndex);
}
currentIndex += currentCount;
}
}
throw new IndexOutOfRangeException();
}
}
}

103
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@ -1,103 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
/// </summary>
public class SelectionModelChildrenRequestedEventArgs : EventArgs
{
private object? _source;
private IndexPath _sourceIndexPath;
private IndexPath _finalIndexPath;
private bool _throwOnAccess;
internal SelectionModelChildrenRequestedEventArgs(
object source,
IndexPath sourceIndexPath,
IndexPath finalIndexPath,
bool throwOnAccess)
{
source = source ?? throw new ArgumentNullException(nameof(source));
Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess);
}
/// <summary>
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
/// object.
/// </summary>
public IObservable<object?>? Children { get; set; }
/// <summary>
/// Gets the object whose children are being requested.
/// </summary>
public object Source
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _source!;
}
}
/// <summary>
/// Gets the index of the object whose children are being requested.
/// </summary>
public IndexPath SourceIndex
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _sourceIndexPath;
}
}
/// <summary>
/// Gets the index of the final object which is being attempted to be retrieved.
/// </summary>
public IndexPath FinalIndex
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _finalIndexPath;
}
}
internal void Initialize(
object? source,
IndexPath sourceIndexPath,
IndexPath finalIndexPath,
bool throwOnAccess)
{
if (!throwOnAccess && source == null)
{
throw new ArgumentNullException(nameof(source));
}
_source = source;
_sourceIndexPath = sourceIndexPath;
_finalIndexPath = finalIndexPath;
_throwOnAccess = throwOnAccess;
}
}
}

47
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@ -1,47 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
public class SelectionModelSelectionChangedEventArgs : EventArgs
{
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<IndexPath>? deselectedIndices,
IReadOnlyList<IndexPath>? selectedIndices,
IReadOnlyList<object?>? deselectedItems,
IReadOnlyList<object?>? selectedItems)
{
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
DeselectedItems = deselectedItems ?? Array.Empty<object?>();
SelectedItems= selectedItems ?? Array.Empty<object?>();
}
/// <summary>
/// Gets the indices of the items that were removed from the selection.
/// </summary>
public IReadOnlyList<IndexPath> DeselectedIndices { get; }
/// <summary>
/// Gets the indices of the items that were added to the selection.
/// </summary>
public IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object?> SelectedItems { get; }
}
}

971
src/Avalonia.Controls/SelectionNode.cs

@ -1,971 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Tracks nested selection.
/// </summary>
/// <remarks>
/// SelectionNode is the internal tree data structure that we keep track of for selection in
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
/// collection changes and keeps the selected indices up to date. This can either be a leaf
/// node or a non leaf node.
/// </remarks>
internal class SelectionNode : IDisposable
{
private readonly SelectionModel _manager;
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>();
private readonly SelectionNode? _parent;
private readonly List<IndexRange> _selected = new List<IndexRange>();
private readonly List<int> _selectedIndicesCached = new List<int>();
private IDisposable? _childrenSubscription;
private SelectionNodeOperation? _operation;
private object? _source;
private bool _selectedIndicesCacheIsValid;
private bool _retainSelectionOnReset;
private List<object?>? _selectedItems;
public SelectionNode(SelectionModel manager, SelectionNode? parent)
{
_manager = manager;
_parent = parent;
}
public int AnchorIndex { get; set; } = -1;
public bool RetainSelectionOnReset
{
get => _retainSelectionOnReset;
set
{
if (_retainSelectionOnReset != value)
{
_retainSelectionOnReset = value;
if (_retainSelectionOnReset)
{
_selectedItems = new List<object?>();
PopulateSelectedItemsFromSelectedIndices();
}
else
{
_selectedItems = null;
}
foreach (var child in _childrenNodes)
{
if (child != null)
{
child.RetainSelectionOnReset = value;
}
}
}
}
}
public object? Source
{
get => _source;
set
{
if (_source != value)
{
if (_source != null)
{
ClearSelection();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
_source = value;
// Setup ItemsSourceView
var newDataSource = value as ItemsSourceView;
if (value != null && newDataSource == null)
{
newDataSource = new ItemsSourceView((IEnumerable)value);
}
ItemsSourceView = newDataSource;
TrimInvalidSelections();
PopulateSelectedItemsFromSelectedIndices();
HookupCollectionChangedHandler();
OnSelectionChanged();
}
}
}
private void TrimInvalidSelections()
{
if (_selected == null || ItemsSourceView == null)
{
return;
}
var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1);
var removed = new List<IndexRange>();
var removedCount = IndexRange.Intersect(_selected, validRange, removed);
if (removedCount > 0)
{
using var operation = _manager.Update();
SelectedCount -= removedCount;
OnSelectionChanged();
_operation!.Deselected(removed);
}
}
public ItemsSourceView? ItemsSourceView { get; private set; }
public int DataCount => ItemsSourceView?.Count ?? 0;
public int ChildrenNodeCount => _childrenNodes.Count;
public int RealizedChildrenNodeCount { get; private set; }
public IndexPath IndexPath
{
get
{
var path = new List<int>(); ;
var parent = _parent;
var child = this;
while (parent != null)
{
var childNodes = parent._childrenNodes;
var index = childNodes.IndexOf(child);
// We are walking up to the parent, so the path will be backwards
path.Insert(0, index);
child = parent;
parent = parent._parent;
}
return new IndexPath(path);
}
}
// For a genuine tree view, we dont know which node is leaf until we
// actually walk to it, so currently the tree builds up to the leaf. I don't
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
// an explosion of node objects. However, I'm still creating the m_childrenNodes
// collection unfortunately.
public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath)
{
SelectionNode? child = null;
if (realizeChild)
{
if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count)
{
throw new IndexOutOfRangeException();
}
if (_childrenNodes.Count == 0)
{
if (ItemsSourceView != null)
{
for (int i = 0; i < ItemsSourceView.Count; i++)
{
_childrenNodes.Add(null);
}
}
}
if (_childrenNodes[index] == null)
{
var childData = ItemsSourceView!.GetAt(index);
IObservable<object?>? resolver = null;
if (childData != null)
{
var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath);
}
if (resolver != null)
{
child = new SelectionNode(_manager, parent: this);
child.SetChildrenObservable(resolver);
}
else if (childData is IEnumerable<object> || childData is IList)
{
child = new SelectionNode(_manager, parent: this);
child.Source = childData;
}
else
{
child = _manager.SharedLeafNode;
}
if (_operation != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
_childrenNodes[index] = child;
RealizedChildrenNodeCount++;
}
else
{
child = _childrenNodes[index];
}
}
else
{
if (_childrenNodes.Count > 0)
{
child = _childrenNodes[index];
}
}
return child;
}
public void SetChildrenObservable(IObservable<object?> resolver)
{
_childrenSubscription = resolver.Subscribe(x =>
{
if (Source != null)
{
using (_manager.Update())
{
SelectionTreeHelper.Traverse(
this,
realizeChildren: false,
info => info.Node.Clear());
}
}
Source = x;
});
}
public int SelectedCount { get; private set; }
public bool IsSelected(int index)
{
var isSelected = false;
foreach (var range in _selected)
{
if (range.Contains(index))
{
isSelected = true;
break;
}
}
return isSelected;
}
// True -> Selected
// False -> Not Selected
// Null -> Some descendents are selected and some are not
public bool? IsSelectedWithPartial()
{
var isSelected = (bool?)false;
if (_parent != null)
{
var parentsChildren = _parent._childrenNodes;
var myIndexInParent = parentsChildren.IndexOf(this);
if (myIndexInParent != -1)
{
isSelected = _parent.IsSelectedWithPartial(myIndexInParent);
}
}
return isSelected;
}
// True -> Selected
// False -> Not Selected
// Null -> Some descendents are selected and some are not
public bool? IsSelectedWithPartial(int index)
{
SelectionState selectionState;
if (_childrenNodes.Count == 0 || // no nodes realized
_childrenNodes.Count <= index || // target node is not realized
_childrenNodes[index] == null || // target node is not realized
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
{
// Ask parent if the target node is selected.
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected;
}
else
{
// targetNode is the node representing the index. This node is the parent.
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
// based on children of targetNode.
var targetNode = _childrenNodes[index];
selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes();
}
return ConvertToNullableBool(selectionState);
}
public int SelectedIndex
{
get => SelectedCount > 0 ? SelectedIndices[0] : -1;
set
{
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value)))
{
ClearSelection();
if (value != -1)
{
Select(value, true);
}
}
}
}
public List<int> SelectedIndices
{
get
{
if (!_selectedIndicesCacheIsValid)
{
_selectedIndicesCacheIsValid = true;
foreach (var range in _selected)
{
for (int index = range.Begin; index <= range.End; index++)
{
// Avoid duplicates
if (!_selectedIndicesCached.Contains(index))
{
_selectedIndicesCached.Add(index);
}
}
}
// Sort the list for easy consumption
_selectedIndicesCached.Sort();
}
return _selectedIndicesCached;
}
}
public IEnumerable<object> SelectedItems
{
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
}
public void Dispose()
{
_childrenSubscription?.Dispose();
ItemsSourceView?.Dispose();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
public void BeginOperation()
{
if (_operation != null)
{
throw new AvaloniaInternalException("Selection operation already in progress.");
}
_operation = new SelectionNodeOperation(this);
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
}
}
public void EndOperation(List<SelectionNodeOperation> changes)
{
if (_operation == null)
{
throw new AvaloniaInternalException("No selection operation in progress.");
}
if (_operation.HasChanges)
{
changes.Add(_operation);
}
_operation = null;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.EndOperation(changes);
}
}
}
public bool Cleanup()
{
var result = SelectedCount == 0;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null)
{
if (child.Cleanup())
{
child.Dispose();
_childrenNodes[i] = null;
}
else
{
result = false;
}
}
}
return result;
}
public bool Select(int index, bool select)
{
return Select(index, select, raiseOnSelectionChanged: true);
}
public bool ToggleSelect(int index)
{
return Select(index, !IsSelected(index));
}
public void SelectAll()
{
if (ItemsSourceView != null)
{
var size = ItemsSourceView.Count;
if (size > 0)
{
SelectRange(new IndexRange(0, size - 1), select: true);
}
}
}
public void Clear() => ClearSelection();
public bool SelectRange(IndexRange range, bool select)
{
if (IsValidIndex(range.Begin) && IsValidIndex(range.End))
{
if (select)
{
AddRange(range, raiseOnSelectionChanged: true);
}
else
{
RemoveRange(range, raiseOnSelectionChanged: true);
}
return true;
}
return false;
}
private void HookupCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged += OnSourceListChanged;
}
}
private void UnhookCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged -= OnSourceListChanged;
}
}
private bool IsValidIndex(int index)
{
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
}
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
{
var selected = new List<IndexRange>();
SelectedCount += IndexRange.Add(_selected, addRange, selected);
if (selected.Count > 0)
{
_operation?.Selected(selected);
if (_selectedItems != null && ItemsSourceView != null)
{
for (var i = addRange.Begin; i <= addRange.End; ++i)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
{
var removed = new List<IndexRange>();
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
if (removed.Count > 0)
{
_operation?.Deselected(removed);
if (_selectedItems != null)
{
for (var i = removeRange.Begin; i <= removeRange.End; ++i)
{
_selectedItems.Remove(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void ClearSelection()
{
// Deselect all items
if (_selected.Count > 0)
{
_operation?.Deselected(_selected);
_selected.Clear();
OnSelectionChanged();
}
_selectedItems?.Clear();
SelectedCount = 0;
AnchorIndex = -1;
}
private void ClearChildNodes()
{
for (int i = 0; i < _childrenNodes.Count; i++)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.Dispose();
_childrenNodes[i] = null;
}
}
RealizedChildrenNodeCount = 0;
}
private bool Select(int index, bool select, bool raiseOnSelectionChanged)
{
if (IsValidIndex(index))
{
// Ignore duplicate selection calls
if (IsSelected(index) == select)
{
return true;
}
var range = new IndexRange(index, index);
if (select)
{
AddRange(range, raiseOnSelectionChanged);
}
else
{
RemoveRange(range, raiseOnSelectionChanged);
}
return true;
}
return false;
}
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
{
bool selectionInvalidated = false;
List<object?>? removed = null;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
case NotifyCollectionChangedAction.Remove:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
break;
}
case NotifyCollectionChangedAction.Reset:
{
if (_selectedItems == null)
{
ClearSelection();
}
else
{
removed = RecreateSelectionFromSelectedItems();
}
selectionInvalidated = true;
break;
}
case NotifyCollectionChangedAction.Replace:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
}
if (selectionInvalidated)
{
OnSelectionChanged();
}
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
}
private bool OnItemsAdded(int index, int count)
{
var selectionInvalidated = false;
// Update ranges for leaf items
var toAdd = new List<IndexRange>();
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
if (range.Contains(index - 1))
{
range.Split(index - 1, out var before, out _);
toAdd.Add(before);
begin = index;
}
// Shift the range to the right
_selected[i] = new IndexRange(begin + count, range.End + count);
selectionInvalidated = true;
}
}
// Add the left sides of the split ranges
_selected.AddRange(toAdd);
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
_childrenNodes.Insert(index, null);
}
}
// Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex += count;
}
// Check if adding a node invalidated an ancestors
// selection state. For example if parent was selected before
// adding a new item makes the parent partially selected now.
if (!selectionInvalidated)
{
var parent = _parent;
while (parent != null)
{
var isSelected = parent.IsSelectedWithPartial();
// If a parent is selected, then it will become partially selected.
// If it is not selected or partially selected - there is no change.
if (isSelected == true)
{
selectionInvalidated = true;
break;
}
parent = parent._parent;
}
}
return selectionInvalidated;
}
private (bool, List<object?>) OnItemsRemoved(int index, IList items)
{
var selectionInvalidated = false;
var removed = new List<object?>();
var count = items.Count;
var isSelected = false;
for (int i = 0; i <= count - 1; i++)
{
if (IsSelected(index + i))
{
isSelected = true;
removed.Add(items[i]);
}
}
if (isSelected)
{
var removeRange = new IndexRange(index, index + count - 1);
SelectedCount -= IndexRange.Remove(_selected, removeRange);
selectionInvalidated = true;
if (_selectedItems != null)
{
foreach (var i in items)
{
_selectedItems.Remove(i);
}
}
}
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
// The range is after the removed items, need to shift the range left
if (range.End > index)
{
// Shift the range to the left
_selected[i] = new IndexRange(range.Begin - count, range.End - count);
selectionInvalidated = true;
}
}
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
if (_childrenNodes[index] != null)
{
removed.AddRange(_childrenNodes[index]!.SelectedItems);
RealizedChildrenNodeCount--;
_childrenNodes[index]!.Dispose();
}
_childrenNodes.RemoveAt(index);
}
}
//Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex -= count;
}
return (selectionInvalidated, removed);
}
private void OnSelectionChanged()
{
_selectedIndicesCacheIsValid = false;
_selectedIndicesCached.Clear();
}
public static bool? ConvertToNullableBool(SelectionState isSelected)
{
bool? result = null; // PartialySelected
if (isSelected == SelectionState.Selected)
{
result = true;
}
else if (isSelected == SelectionState.NotSelected)
{
result = false;
}
return result;
}
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes()
{
var selectionState = SelectionState.NotSelected;
int realizedChildrenNodeCount = RealizedChildrenNodeCount;
int selectedCount = SelectedCount;
if (realizedChildrenNodeCount != 0 || selectedCount != 0)
{
// There are realized children or some selected leaves.
int dataCount = DataCount;
if (realizedChildrenNodeCount == 0 && selectedCount > 0)
{
// All nodes are leaves under it - we didn't create children nodes as an optimization.
// See if all/some or none of the leaves are selected.
selectionState = dataCount != selectedCount ?
SelectionState.PartiallySelected :
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected;
}
else
{
// There are child nodes, walk them individually and evaluate based on each child
// being selected/not selected or partially selected.
selectedCount = 0;
int notSelectedCount = 0;
for (int i = 0; i < ChildrenNodeCount; i++)
{
var child = GetAt(i, false, default);
if (child != null)
{
// child is realized, ask it.
var isChildSelected = IsSelectedWithPartial(i);
if (isChildSelected == null)
{
selectionState = SelectionState.PartiallySelected;
break;
}
else if (isChildSelected == true)
{
selectedCount++;
}
else
{
notSelectedCount++;
}
}
else
{
// not realized.
if (IsSelected(i))
{
selectedCount++;
}
else
{
notSelectedCount++;
}
}
if (selectedCount > 0 && notSelectedCount > 0)
{
selectionState = SelectionState.PartiallySelected;
break;
}
}
if (selectionState != SelectionState.PartiallySelected)
{
if (selectedCount != 0 && selectedCount != dataCount)
{
selectionState = SelectionState.PartiallySelected;
}
else
{
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected;
}
}
}
}
return selectionState;
}
private void PopulateSelectedItemsFromSelectedIndices()
{
if (_selectedItems != null)
{
_selectedItems.Clear();
foreach (var i in SelectedIndices)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
}
private List<object?> RecreateSelectionFromSelectedItems()
{
var removed = new List<object?>();
_selected.Clear();
SelectedCount = 0;
for (var i = 0; i < _selectedItems!.Count; ++i)
{
var item = _selectedItems[i];
var index = ItemsSourceView!.IndexOf(item);
if (index != -1)
{
IndexRange.Add(_selected, new IndexRange(index, index));
++SelectedCount;
}
else
{
removed.Add(item);
_selectedItems.RemoveAt(i--);
}
}
return removed;
}
public enum SelectionState
{
Selected,
NotSelected,
PartiallySelected
}
}
}

110
src/Avalonia.Controls/SelectionNodeOperation.cs

@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionNodeOperation : ISelectedItemInfo
{
private readonly SelectionNode _owner;
private List<IndexRange>? _selected;
private List<IndexRange>? _deselected;
private int _selectedCount = -1;
private int _deselectedCount = -1;
public SelectionNodeOperation(SelectionNode owner)
{
_owner = owner;
}
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0;
public List<IndexRange>? SelectedRanges => _selected;
public List<IndexRange>? DeselectedRanges => _deselected;
public IndexPath Path => _owner.IndexPath;
public ItemsSourceView? Items => _owner.ItemsSourceView;
public int SelectedCount
{
get
{
if (_selectedCount == -1)
{
_selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0;
}
return _selectedCount;
}
}
public int DeselectedCount
{
get
{
if (_deselectedCount == -1)
{
_deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0;
}
return _deselectedCount;
}
}
public void Selected(IndexRange range)
{
Add(range, ref _selected, _deselected);
_selectedCount = -1;
}
public void Selected(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
Selected(range);
}
}
public void Deselected(IndexRange range)
{
Add(range, ref _deselected, _selected);
_deselectedCount = -1;
}
public void Deselected(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
Deselected(range);
}
}
private static void Add(
IndexRange range,
ref List<IndexRange>? add,
List<IndexRange>? remove)
{
if (remove != null)
{
var removed = new List<IndexRange>();
IndexRange.Remove(remove, range, removed);
var selected = IndexRange.Subtract(range, removed);
if (selected.Any())
{
add ??= new List<IndexRange>();
foreach (var r in selected)
{
IndexRange.Add(add, r);
}
}
}
else
{
add ??= new List<IndexRange>();
IndexRange.Add(add, range);
}
}
}
}

62
src/Avalonia.Controls/SplitView.cs

@ -145,7 +145,7 @@ namespace Avalonia.Controls
private bool _isPaneOpen;
private Panel _pane;
private CompositeDisposable _pointerDisposables;
private IDisposable _pointerDisposable;
public SplitView()
{
@ -320,37 +320,14 @@ namespace Avalonia.Controls
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
//Logic adapted from Popup
//Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
IDisposable subscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
_pointerDisposables = new CompositeDisposable(
window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
InputManager.Instance?.Process.Subscribe(OnNonClientClick),
subscribeToEventHandler<Window, EventHandler>(window, Window_Deactivated,
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
subscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, OnWindowLostFocus,
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
_pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
_pointerDisposable?.Dispose();
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
@ -371,7 +348,12 @@ namespace Avalonia.Controls
var src = e.Source as IVisual;
while (src != null)
{
if (src == _pane)
// Make assumption that if Popup is in visual tree,
// owning control is within pane
// This works because if pane is triggered to close
// when clicked anywhere else in Window, the pane
// would close before the popup is opened
if (src == _pane || src is PopupRoot)
{
closePane = false;
break;
@ -385,31 +367,7 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
private void OnNonClientClick(RawInputEventArgs obj)
{
if (!IsPaneOpen)
{
return;
}
var mouse = obj as RawPointerEventArgs;
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
if (ShouldClosePane())
IsPaneOpen = false;
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
private bool ShouldClosePane()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);

131
src/Avalonia.Controls/TextBox.cs

@ -18,6 +18,15 @@ namespace Avalonia.Controls
{
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{
public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Cut.FirstOrDefault();
public static KeyGesture CopyGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Copy.FirstOrDefault();
public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault();
public static readonly StyledProperty<bool> AcceptsReturnProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn));
@ -103,6 +112,21 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
struct UndoRedoState : IEquatable<UndoRedoState>
{
@ -126,6 +150,9 @@ namespace Avalonia.Controls
private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
private bool _canCut;
private bool _canCopy;
private bool _canPaste;
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
@ -369,6 +396,41 @@ namespace Avalonia.Controls
get { return _newLine; }
set { SetAndRaise(NewLineProperty, ref _newLine, value); }
}
/// <summary>
/// Clears the current selection, maintaining the <see cref="CaretIndex"/>
/// </summary>
public void ClearSelection()
{
SelectionStart = SelectionEnd = CaretIndex;
}
/// <summary>
/// Property for determining if the Cut command can be executed.
/// </summary>
public bool CanCut
{
get { return _canCut; }
private set { SetAndRaise(CanCutProperty, ref _canCut, value); }
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
public bool CanCopy
{
get { return _canCopy; }
private set { SetAndRaise(CanCopyProperty, ref _canCopy, value); }
}
/// <summary>
/// Property for determining if the Paste command can be executed.
/// </summary>
public bool CanPaste
{
get { return _canPaste; }
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); }
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@ -387,9 +449,19 @@ namespace Avalonia.Controls
if (change.Property == TextProperty)
{
UpdatePseudoclasses();
UpdateCommandStates();
}
}
private void UpdateCommandStates()
{
var text = GetSelection();
var isSelectionNullOrEmpty = string.IsNullOrEmpty(text);
CanCopy = !IsPasswordBox && !isSelectionNullOrEmpty;
CanCut = !IsPasswordBox && !isSelectionNullOrEmpty && !IsReadOnly;
CanPaste = !IsReadOnly;
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
@ -404,16 +476,24 @@ namespace Avalonia.Controls
SelectAll();
}
UpdateCommandStates();
_presenter?.ShowCaret();
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
SelectionStart = 0;
SelectionEnd = 0;
if (ContextMenu == null || !ContextMenu.IsOpen)
{
ClearSelection();
RevealPassword = false;
}
UpdateCommandStates();
_presenter?.HideCaret();
RevealPassword = false;
}
protected override void OnTextInput(TextInputEventArgs e)
@ -439,7 +519,7 @@ namespace Avalonia.Controls
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
_undoRedoHelper.DiscardRedo();
}
}
@ -455,19 +535,31 @@ namespace Avalonia.Controls
return text;
}
private async void Copy()
public async void Cut()
{
var text = GetSelection();
if (text is null) return;
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
}
public async void Copy()
{
var text = GetSelection();
if (text is null) return;
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(GetSelection());
.SetTextAsync(text);
}
private async void Paste()
public async void Paste()
{
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text == null)
{
return;
}
if (text is null) return;
_undoRedoHelper.Snapshot();
HandleTextInput(text);
@ -506,23 +598,18 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
Cut();
}
handled = true;
}
else if (Match(keymap.Paste))
{
Paste();
handled = true;
}
else if (Match(keymap.Undo))
{
try
{
_isUndoingRedoing = true;
@ -657,7 +744,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex - removedCharacters) +
text.Substring(caretIndex));
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
}
_undoRedoHelper.Snapshot();
@ -730,7 +817,7 @@ namespace Avalonia.Controls
}
else if (movement)
{
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
}
if (handled || movement)
@ -1037,7 +1124,8 @@ namespace Avalonia.Controls
var end = Math.Max(selectionStart, selectionEnd);
var text = Text;
SetTextInternal(text.Substring(0, start) + text.Substring(end));
SelectionStart = SelectionEnd = CaretIndex = start;
CaretIndex = start;
ClearSelection();
return true;
}
else
@ -1126,7 +1214,8 @@ namespace Avalonia.Controls
set
{
Text = value.Text;
SelectionStart = SelectionEnd = CaretIndex = value.CaretPosition;
CaretIndex = value.CaretPosition;
ClearSelection();
}
}
}

2
src/Avalonia.Controls/ToolTip.cs

@ -55,7 +55,7 @@ namespace Avalonia.Controls
/// <summary>
/// Stores the current <see cref="ToolTip"/> instance in the control.
/// </summary>
private static readonly AttachedProperty<ToolTip> ToolTipProperty =
internal static readonly AttachedProperty<ToolTip> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
private IPopupHost _popup;

7
src/Avalonia.Controls/ToolTipService.cs

@ -35,6 +35,13 @@ namespace Avalonia.Controls
control.PointerEnter += ControlPointerEnter;
control.PointerLeave += ControlPointerLeave;
}
if (ToolTip.GetIsOpen(control) && e.NewValue != e.OldValue && !(e.NewValue is ToolTip))
{
var tip = control.GetValue(ToolTip.ToolTipProperty);
tip.Content = e.NewValue;
}
}
internal void TipOpenChanged(AvaloniaPropertyChangedEventArgs e)

669
src/Avalonia.Controls/TreeView.cs

@ -2,9 +2,11 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
@ -44,29 +46,16 @@ namespace Avalonia.Controls
o => o.SelectedItems,
(o, v) => o.SelectedItems = v);
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty.AddOwner<TreeView>(
o => o.Selection,
(o, v) => o.Selection = v);
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
ListBox.SelectionModeProperty.AddOwner<TreeView>();
/// <summary>
/// Defines the <see cref="SelectionChanged"/> property.
/// </summary>
public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
SelectingItemsControl.SelectionChangedEvent;
private static readonly IList Empty = Array.Empty<object>();
private object _selectedItem;
private ISelectionModel _selection;
private readonly SelectedItemsSync _selectedItems;
private IList _selectedItems;
private bool _syncingSelectedItems;
/// <summary>
/// Initializes static members of the <see cref="TreeView"/> class.
@ -76,13 +65,6 @@ namespace Avalonia.Controls
// HACK: Needed or SelectedItem property will not be found in Release build.
}
public TreeView()
{
// Setting Selection to null causes a default SelectionModel to be created.
Selection = null;
_selectedItems = new SelectedItemsSync(Selection);
}
/// <summary>
/// Occurs when the control's selection changes.
/// </summary>
@ -125,94 +107,56 @@ namespace Avalonia.Controls
/// </remarks>
public object SelectedItem
{
get => Selection.SelectedItem;
set => Selection.SelectedIndex = IndexFromItem(value);
}
get => _selectedItem;
set
{
var selectedItems = SelectedItems;
/// <summary>
/// Gets or sets the selected items.
/// </summary>
protected IList SelectedItems
{
get => _selectedItems.GetOrCreateItems();
set => _selectedItems.SetItems(value);
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
if (value != null)
{
if (selectedItems.Count != 1 || selectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// Gets or sets the selected items.
/// </summary>
public ISelectionModel Selection
public IList SelectedItems
{
get => _selection;
set
get
{
value ??= new SelectionModel
if (_selectedItems == null)
{
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
RetainSelectionOnReset = true,
};
if (_selection != value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
}
else if (value.Source != null && value.Source != Items)
{
throw new ArgumentException("Selection has invalid Source.");
}
List<object> oldSelection = null;
if (_selection != null)
{
oldSelection = Selection.SelectedItems.ToList();
_selection.PropertyChanged -= OnSelectionModelPropertyChanged;
_selection.SelectionChanged -= OnSelectionModelSelectionChanged;
_selection.ChildrenRequested -= OnSelectionModelChildrenRequested;
MarkContainersUnselected();
}
_selection = value;
if (_selection != null)
{
_selection.Source = Items;
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
_selection.ChildrenRequested += OnSelectionModelChildrenRequested;
if (_selection.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
if (_selection.AutoSelect)
{
SelectionMode |= SelectionMode.AlwaysSelected;
}
else
{
SelectionMode &= ~SelectionMode.AlwaysSelected;
}
UpdateContainerSelection();
_selectedItems = new AvaloniaList<object>();
SubscribeToSelectedItems();
}
var selectedItem = SelectedItem;
return _selectedItems;
}
if (_selectedItem != selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
_selectedItem = selectedItem;
}
}
set
{
if (value?.IsFixedSize == true || value?.IsReadOnly == true)
{
throw new NotSupportedException(
"Cannot use a fixed size or read-only collection as SelectedItems.");
}
UnsubscribeFromSelectedItems();
_selectedItems = value ?? new AvaloniaList<object>();
SubscribeToSelectedItems();
}
}
@ -245,13 +189,186 @@ namespace Avalonia.Controls
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll() => Selection.SelectAll();
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll() => Selection.ClearSelection();
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
private void SubscribeToSelectedItems()
{
if (_selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
SelectedItemsCollectionChanged(
_selectedItems,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void SelectSingleItem(object item)
{
SelectedItems.Clear();
SelectedItems.Add(item);
}
/// <summary>
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
IList added = null;
IList removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
if (AutoScrollToSelectedItem)
{
var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
container?.BringIntoView();
}
added = e.NewItems;
break;
case NotifyCollectionChangedAction.Remove:
if (!_syncingSelectedItems)
{
if (SelectedItems.Count == 0)
{
SelectedItem = null;
}
else
{
var selectedIndex = SelectedItems.IndexOf(_selectedItem);
if (selectedIndex == -1)
{
var old = _selectedItem;
_selectedItem = SelectedItems[0];
RaisePropertyChanged(SelectedItemProperty, old, _selectedItem);
}
}
}
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
removed = e.OldItems;
break;
case NotifyCollectionChangedAction.Reset:
foreach (IControl container in ItemContainerGenerator.Index.Containers)
{
MarkContainerSelected(container, false);
}
if (SelectedItems.Count > 0)
{
SelectedItemsAdded(SelectedItems);
added = SelectedItems;
}
else if (!_syncingSelectedItems)
{
SelectedItem = null;
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
foreach (var item in e.NewItems)
{
MarkItemSelected(item, true);
}
if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
{
var oldItem = SelectedItem;
var item = SelectedItems[0];
_selectedItem = item;
RaisePropertyChanged(SelectedItemProperty, oldItem, item);
}
added = e.NewItems;
removed = e.OldItems;
break;
}
if (added?.Count > 0 || removed?.Count > 0)
{
var changed = new SelectionChangedEventArgs(
SelectingItemsControl.SelectionChangedEvent,
removed ?? Empty,
added ?? Empty);
RaiseEvent(changed);
}
}
private void MarkItemSelected(object item, bool selected)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(item);
MarkContainerSelected(container, selected);
}
private void SelectedItemsAdded(IList items)
{
if (items.Count == 0)
{
return;
}
foreach (object item in items)
{
MarkItemSelected(item, true);
}
if (SelectedItem == null && !_syncingSelectedItems)
{
SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]);
}
}
/// <summary>
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
private void UnsubscribeFromSelectedItems()
{
if (_selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
(bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
NavigationDirection direction)
{
@ -334,86 +451,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
{
var container = ContainerFromIndex(Selection.AnchorIndex);
if (container != null)
{
DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero);
}
}
}
/// <summary>
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
void Mark(IndexPath index, bool selected)
{
var container = ContainerFromIndex(index);
if (container != null)
{
MarkContainerSelected(container, selected);
}
}
foreach (var i in e.SelectedIndices)
{
Mark(i, true);
}
foreach (var i in e.DeselectedIndices)
{
Mark(i, false);
}
var newSelectedItem = SelectedItem;
if (newSelectedItem != _selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
_selectedItem = newSelectedItem;
}
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem;
if (container is object)
{
if (e.SourceIndex.IsAncestorOf(e.FinalIndex))
{
container.IsExpanded = true;
container.ApplyTemplate();
container.Presenter?.ApplyTemplate();
}
e.Children = Observable.CombineLatest(
container.GetObservable(TreeViewItem.IsExpandedProperty),
container.GetObservable(ItemsProperty),
(expanded, items) => expanded ? items : null);
}
}
private TreeViewItem GetContainerInDirection(
TreeViewItem from,
NavigationDirection direction,
@ -467,12 +504,6 @@ namespace Avalonia.Controls
return result;
}
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
Selection.Source = Items;
base.ItemsChanged(e);
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -494,18 +525,6 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectionModeProperty)
{
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}
}
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>
@ -521,9 +540,9 @@ namespace Avalonia.Controls
bool toggleModifier = false,
bool rightButton = false)
{
var index = IndexFromContainer((TreeViewItem)container);
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
if (index.GetSize() == 0)
if (item == null)
{
return;
}
@ -540,48 +559,41 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (!select)
if (rightButton)
{
Selection.DeselectAt(index);
}
else if (rightButton)
{
if (!Selection.IsSelectedAt(index))
if (!SelectedItems.Contains(item))
{
Selection.SelectedIndex = index;
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{
Selection.SelectedIndex = index;
SelectSingleItem(item);
}
else if (multi && range)
{
using var operation = Selection.Update();
var anchor = Selection.AnchorIndex;
if (anchor.GetSize() == 0)
{
anchor = new IndexPath(0);
}
Selection.ClearSelection();
Selection.AnchorIndex = anchor;
Selection.SelectRangeFromAnchorTo(index);
SynchronizeItems(
SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
}
else
{
if (Selection.IsSelectedAt(index))
{
Selection.DeselectAt(index);
}
else if (multi)
var i = SelectedItems.IndexOf(item);
if (i != -1)
{
Selection.SelectAt(index);
SelectedItems.Remove(item);
}
else
{
Selection.SelectedIndex = index;
if (multi)
{
SelectedItems.Add(item);
}
else
{
SelectedItem = item;
}
}
}
}
@ -604,6 +616,117 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Find which node is first in hierarchy.
/// </summary>
/// <param name="treeView">Search root.</param>
/// <param name="nodeA">Nodes to find.</param>
/// <param name="nodeB">Node to find.</param>
/// <returns>Found first node.</returns>
private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
{
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
}
private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
TreeViewItem nodeA,
TreeViewItem nodeB)
{
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
foreach (ItemContainerInfo container in containers)
{
TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
if (node != null)
{
return node;
}
}
return null;
}
private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB)
{
if (node == null)
{
return null;
}
TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null;
if (match != null)
{
return match;
}
return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
}
/// <summary>
/// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
/// The range is inclusive.
/// </summary>
/// <param name="from">From container.</param>
/// <param name="to">To container.</param>
private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
{
var items = new List<object>();
if (from == null || to == null)
{
return items;
}
TreeViewItem firstItem = FindFirstNode(this, from, to);
if (firstItem == null)
{
return items;
}
bool wasReversed = false;
if (firstItem == to)
{
var temp = from;
from = to;
to = temp;
wasReversed = true;
}
TreeViewItem node = from;
while (node != to)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(node);
if (item != null)
{
items.Add(item);
}
node = GetContainerInDirection(node, NavigationDirection.Down, true);
}
var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
if (toItem != null)
{
items.Add(toItem);
}
if (wasReversed)
{
items.Reverse();
}
return items;
}
/// <summary>
/// Updates the selection based on an event that may have originated in a container that
/// belongs to the control.
@ -709,90 +832,26 @@ namespace Avalonia.Controls
}
}
private void MarkContainersUnselected()
{
foreach (var container in ItemContainerGenerator.Index.Containers)
{
MarkContainerSelected(container, false);
}
}
private void UpdateContainerSelection()
{
var index = ItemContainerGenerator.Index;
foreach (var container in index.Containers)
{
var i = IndexFromContainer((TreeViewItem)container);
MarkContainerSelected(
container,
Selection.IsSelectedAt(i) != false);
}
}
private static IndexPath IndexFromContainer(TreeViewItem container)
{
var result = new List<int>();
while (true)
{
if (container.Level == 0)
{
var treeView = container.FindAncestorOfType<TreeView>();
if (treeView == null)
{
return default;
}
result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container));
result.Reverse();
return new IndexPath(result);
}
else
{
var parent = container.FindAncestorOfType<TreeViewItem>();
if (parent == null)
{
return default;
}
result.Add(parent.ItemContainerGenerator.IndexFromContainer(container));
container = parent;
}
}
}
private IndexPath IndexFromItem(object item)
/// <summary>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
var list = items.Cast<object>().ToList();
var toRemove = list.Except(desired).ToList();
var toAdd = desired.Except(list).ToList();
if (container != null)
foreach (var i in toRemove)
{
return IndexFromContainer(container);
items.Remove(i);
}
return default;
}
private TreeViewItem ContainerFromIndex(IndexPath index)
{
TreeViewItem treeViewItem = null;
for (var i = 0; i < index.GetSize(); ++i)
foreach (var i in toAdd)
{
var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
if (treeViewItem == null)
{
return null;
}
items.Add(i);
}
return treeViewItem;
}
}
}

2
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls.Utils
var radiusY = keypoints.RightTop.Y - boundRect.TopRight.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(keypoints.RightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
context.ArcTo(keypoints.RightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
}
// Right

156
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Controls.Utils
{
internal interface ICollectionChangedListener
{
void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
}
internal class CollectionChangedEventManager
{
public static CollectionChangedEventManager Instance { get; } = new CollectionChangedEventManager();
private ConditionalWeakTable<INotifyCollectionChanged, Entry> _entries =
new ConditionalWeakTable<INotifyCollectionChanged, Entry>();
private CollectionChangedEventManager()
{
}
public void AddListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
{
collection = collection ?? throw new ArgumentNullException(nameof(collection));
listener = listener ?? throw new ArgumentNullException(nameof(listener));
Dispatcher.UIThread.VerifyAccess();
if (!_entries.TryGetValue(collection, out var entry))
{
entry = new Entry(collection);
_entries.Add(collection, entry);
}
foreach (var l in entry.Listeners)
{
if (l.TryGetTarget(out var target) && target == listener)
{
throw new InvalidOperationException(
"Collection listener already added for this collection/listener combination.");
}
}
entry.Listeners.Add(new WeakReference<ICollectionChangedListener>(listener));
}
public void RemoveListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
{
collection = collection ?? throw new ArgumentNullException(nameof(collection));
listener = listener ?? throw new ArgumentNullException(nameof(listener));
Dispatcher.UIThread.VerifyAccess();
if (_entries.TryGetValue(collection, out var entry))
{
var listeners = entry.Listeners;
for (var i = 0; i < listeners.Count; ++i)
{
if (listeners[i].TryGetTarget(out var target) && target == listener)
{
listeners.RemoveAt(i);
if (listeners.Count == 0)
{
entry.Dispose();
_entries.Remove(collection);
}
return;
}
}
}
throw new InvalidOperationException(
"Collection listener not registered for this collection/listener combination.");
}
private class Entry : IWeakSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{
private INotifyCollectionChanged _collection;
public Entry(INotifyCollectionChanged collection)
{
_collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
}
public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose()
{
WeakSubscriptionManager.Unsubscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,
NotifyCollectionChangedEventArgs args,
List<WeakReference<ICollectionChangedListener>> listeners)
{
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.PreChanged(incc, args);
}
}
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.Changed(incc, args);
}
}
foreach (var l in listeners)
{
if (l.TryGetTarget(out var target))
{
target.PostChanged(incc, args);
}
}
}
var l = Listeners.ToList();
if (Dispatcher.UIThread.CheckAccess())
{
Notify(_collection, e, l);
}
else
{
var eCapture = e;
Dispatcher.UIThread.Post(() => Notify(_collection, eCapture, l));
}
}
}
}
}

211
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -4,6 +4,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
#nullable enable
@ -12,121 +13,118 @@ namespace Avalonia.Controls.Utils
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync
internal class SelectedItemsSync : IDisposable
{
private IList? _items;
private ISelectionModel _selectionModel;
private IList _selectedItems;
private bool _updatingItems;
private bool _updatingModel;
private bool _initializeOnSourceAssignment;
public SelectedItemsSync(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
Model = model;
_selectionModel = model ?? throw new ArgumentNullException(nameof(model));
_selectedItems = new AvaloniaList<object?>();
SyncSelectedItemsWithSelectionModel();
SubscribeToSelectedItems(_selectedItems);
SubscribeToSelectionModel(model);
}
public ISelectionModel Model { get; private set; }
public IList GetOrCreateItems()
public ISelectionModel SelectionModel
{
if (_items == null)
get => _selectionModel;
set
{
var items = new AvaloniaList<object>(Model.SelectedItems);
items.CollectionChanged += ItemsCollectionChanged;
Model.SelectionChanged += SelectionModelSelectionChanged;
_items = items;
if (_selectionModel != value)
{
value = value ?? throw new ArgumentNullException(nameof(value));
UnsubscribeFromSelectionModel(_selectionModel);
_selectionModel = value;
SubscribeToSelectionModel(_selectionModel);
SyncSelectedItemsWithSelectionModel();
}
}
return _items;
}
public void SetItems(IList? items)
public IList SelectedItems
{
items ??= new AvaloniaList<object>();
if (items.IsFixedSize)
get => _selectedItems;
set
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
value ??= new AvaloniaList<object?>();
if (_items is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= ItemsCollectionChanged;
}
if (_selectedItems != value)
{
if (value.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
if (_items == null)
{
Model.SelectionChanged += SelectionModelSelectionChanged;
UnsubscribeFromSelectedItems(_selectedItems);
_selectedItems = value;
SubscribeToSelectedItems(_selectedItems);
SyncSelectionModelWithSelectedItems();
}
}
}
public void Dispose()
{
UnsubscribeFromSelectedItems(_selectedItems);
UnsubscribeFromSelectionModel(_selectionModel);
}
private void SyncSelectedItemsWithSelectionModel()
{
_updatingItems = true;
try
{
_updatingModel = true;
_items = items;
_selectedItems.Clear();
if (Model.Source is object)
if (_selectionModel.Source is object)
{
using (Model.Update())
foreach (var i in _selectionModel.SelectedItems)
{
Model.ClearSelection();
Add(items);
_selectedItems.Add(i);
}
}
else if (!_initializeOnSourceAssignment)
{
Model.PropertyChanged += SelectionModelPropertyChanged;
_initializeOnSourceAssignment = true;
}
if (_items is INotifyCollectionChanged incc2)
{
incc2.CollectionChanged += ItemsCollectionChanged;
}
}
finally
{
_updatingModel = false;
_updatingItems = false;
}
}
public void SetModel(ISelectionModel model)
private void SyncSelectionModelWithSelectedItems()
{
model = model ?? throw new ArgumentNullException(nameof(model));
_updatingModel = true;
if (_items != null)
try
{
Model.PropertyChanged -= SelectionModelPropertyChanged;
Model.SelectionChanged -= SelectionModelSelectionChanged;
Model = model;
Model.SelectionChanged += SelectionModelSelectionChanged;
_initializeOnSourceAssignment = false;
try
if (_selectionModel.Source is object)
{
_updatingItems = true;
_items.Clear();
foreach (var i in model.SelectedItems)
using (_selectionModel.BatchUpdate())
{
_items.Add(i);
SelectionModel.Clear();
Add(_selectedItems);
}
}
finally
{
_updatingItems = false;
}
}
finally
{
_updatingModel = false;
}
}
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_items == null)
if (_selectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
@ -135,18 +133,18 @@ namespace Avalonia.Controls.Utils
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Model.Source, i);
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
Model.Deselect(index);
SelectionModel.Deselect(index);
}
}
}
try
{
using var operation = Model.Update();
using var operation = SelectionModel.BatchUpdate();
_updatingModel = true;
@ -163,8 +161,8 @@ namespace Avalonia.Controls.Utils
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Model.ClearSelection();
Add(_items);
SelectionModel.Clear();
Add(_selectedItems);
break;
}
}
@ -178,46 +176,37 @@ namespace Avalonia.Controls.Utils
{
foreach (var i in newItems)
{
var index = IndexOf(Model.Source, i);
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
Model.Select(index);
SelectionModel.Select(index);
}
}
}
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_initializeOnSourceAssignment &&
_items != null &&
e.PropertyName == nameof(SelectionModel.Source))
if (e.PropertyName == nameof(ISelectionModel.Source))
{
try
if (_selectedItems.Count > 0)
{
_updatingModel = true;
Add(_items);
_initializeOnSourceAssignment = false;
SyncSelectionModelWithSelectedItems();
}
finally
else
{
_updatingModel = false;
SyncSelectedItemsWithSelectionModel();
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel)
if (_updatingModel || _selectionModel.Source is null)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
}
try
{
var deselected = e.DeselectedItems.ToList();
@ -227,12 +216,12 @@ namespace Avalonia.Controls.Utils
foreach (var i in deselected)
{
_items.Remove(i);
_selectedItems.Remove(i);
}
foreach (var i in selected)
{
_items.Add(i);
_selectedItems.Add(i);
}
}
finally
@ -241,7 +230,43 @@ namespace Avalonia.Controls.Utils
}
}
private static int IndexOf(object source, object item)
private void SelectionModelSourceReset(object sender, EventArgs e)
{
SyncSelectionModelWithSelectedItems();
}
private void SubscribeToSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
}
private void SubscribeToSelectionModel(ISelectionModel model)
{
model.PropertyChanged += SelectionModelPropertyChanged;
model.SelectionChanged += SelectionModelSelectionChanged;
model.SourceReset += SelectionModelSourceReset;
}
private void UnsubscribeFromSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromSelectionModel(ISelectionModel model)
{
model.PropertyChanged -= SelectionModelPropertyChanged;
model.SelectionChanged -= SelectionModelSelectionChanged;
model.SourceReset -= SelectionModelSourceReset;
}
private static int IndexOf(object? source, object? item)
{
if (source is IList l)
{

189
src/Avalonia.Controls/Utils/SelectionTreeHelper.cs

@ -1,189 +0,0 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls.Utils
{
internal static class SelectionTreeHelper
{
public static void TraverseIndexPath(
SelectionNode root,
IndexPath path,
bool realizeChildren,
Action<SelectionNode, IndexPath, int, int> nodeAction)
{
var node = root;
for (int depth = 0; depth < path.GetSize(); depth++)
{
int childIndex = path.GetAt(depth);
nodeAction(node, path, depth, childIndex);
if (depth < path.GetSize() - 1)
{
node = node.GetAt(childIndex, realizeChildren, path)!;
}
}
}
public static void Traverse(
SelectionNode root,
bool realizeChildren,
Action<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = new IndexPath(null);
pendingNodes.Add(new TreeWalkNodeInfo(root, current));
while (pendingNodes.Count > 0)
{
var nextNode = pendingNodes.Last();
pendingNodes.RemoveAt(pendingNodes.Count - 1);
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
for (int i = count - 1; i >= 0; i--)
{
var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path);
var childPath = nextNode.Path.CloneWithChildIndex(i);
if (child != null)
{
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node));
}
}
// Queue the children first and then perform the action. This way
// the action can remove the children in the action if necessary
nodeAction(nextNode);
}
}
public static void TraverseRangeRealizeChildren(
SelectionNode root,
IndexPath start,
IndexPath end,
Action<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = start;
// Build up the stack to account for the depth first walk up to the
// start index path.
TraverseIndexPath(
root,
start,
true,
(node, path, depth, childIndex) =>
{
var currentPath = StartPath(path, depth);
bool isStartPath = IsSubSet(start, currentPath);
bool isEndPath = IsSubSet(end, currentPath);
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
var child = node.GetAt(i, true, end);
if (child != null)
{
var childPath = currentPath.CloneWithChildIndex(i);
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node));
}
}
});
// From the start index path, do a depth first walk as long as the
// current path is less than the end path.
while (pendingNodes.Count > 0)
{
var info = pendingNodes.Last();
pendingNodes.RemoveAt(pendingNodes.Count - 1);
int depth = info.Path.GetSize();
bool isStartPath = IsSubSet(start, info.Path);
bool isEndPath = IsSubSet(end, info.Path);
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
var child = info.Node.GetAt(i, true, end);
if (child != null)
{
var childPath = info.Path.CloneWithChildIndex(i);
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node));
}
}
nodeAction(info);
if (info.Path.CompareTo(end) == 0)
{
// We reached the end index path. stop iterating.
break;
}
}
}
private static bool IsSubSet(IndexPath path, IndexPath subset)
{
var subsetSize = subset.GetSize();
if (path.GetSize() < subsetSize)
{
return false;
}
for (int i = 0; i < subsetSize; i++)
{
if (path.GetAt(i) != subset.GetAt(i))
{
return false;
}
}
return true;
}
private static IndexPath StartPath(IndexPath path, int length)
{
var subPath = new List<int>();
for (int i = 0; i < length; i++)
{
subPath.Add(path.GetAt(i));
}
return new IndexPath(subPath);
}
public struct TreeWalkNodeInfo
{
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent)
{
node = node ?? throw new ArgumentNullException(nameof(node));
Node = node;
Path = indexPath;
ParentNode = parent;
}
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath)
{
node = node ?? throw new ArgumentNullException(nameof(node));
Node = node;
Path = indexPath;
ParentNode = null;
}
public SelectionNode Node { get; }
public IndexPath Path { get; }
public SelectionNode? ParentNode { get; }
};
}
}

31
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -12,12 +12,13 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter)
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
_control = control;
TreePage = treePage;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
@ -25,7 +26,6 @@ namespace Avalonia.Diagnostics.ViewModels
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
@ -43,19 +43,9 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public DataGridCollectionView PropertiesView { get; }
public TreePageViewModel TreePage { get; }
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public DataGridCollectionView PropertiesView { get; }
public AvaloniaPropertyViewModel SelectedProperty
{
@ -81,7 +71,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (o is AvaloniaObject ao)
{
return AvaloniaPropertyRegistry.Instance.GetRegistered(ao)
.Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
.Union(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType()))
.Select(x => new AvaloniaPropertyViewModel(ao, x));
}
else
@ -137,9 +127,14 @@ namespace Avalonia.Diagnostics.ViewModels
private bool FilterProperty(object arg)
{
if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property)
if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
{
return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
if (TreePage.UseRegexFilter)
{
return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
}
return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
}
return true;

16
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -18,12 +18,13 @@ namespace Avalonia.Diagnostics.ViewModels
private int _selectedTab;
private string _focusedControl;
private string _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
public MainViewModel(IControl root)
{
_root = root;
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
_events = new EventsPageViewModel(root);
UpdateFocusedControl();
@ -34,6 +35,17 @@ namespace Avalonia.Diagnostics.ViewModels
Console = new ConsoleViewModel(UpdateConsoleContext);
}
public bool ShouldVisualizeMarginPadding
{
get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
public void ToggleVisualizeMarginPadding()
{
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
}
public ConsoleViewModel Console { get; }
public ViewModelBase Content

21
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -83,27 +83,6 @@ namespace Avalonia.Diagnostics.ViewModels
private set;
}
public IndexPath Index
{
get
{
var indices = new List<int>();
var child = this;
var parent = Parent;
while (parent is object)
{
indices.Add(IndexOf(parent.Children, child));
child = child.Parent;
parent = parent.Parent;
}
indices.Add(0);
indices.Reverse();
return new IndexPath(indices);
}
}
public void Dispose()
{
_classesSubscription.Dispose();

105
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,48 +1,40 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase, IDisposable
internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
{
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
private TreeNode _selectedNode;
private ControlDetailsViewModel _details;
private string _propertyFilter;
private string _propertyFilter = string.Empty;
private bool _useRegexFilter;
public TreePageViewModel(TreeNode[] nodes)
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
MainView = mainView;
Nodes = nodes;
Selection = new SelectionModel
{
SingleSelect = true,
Source = Nodes
};
}
Selection.SelectionChanged += (s, e) =>
{
SelectedNode = (TreeNode)Selection.SelectedItem;
};
}
public MainViewModel MainView { get; }
public TreeNode[] Nodes { get; protected set; }
public SelectionModel Selection { get; }
public TreeNode SelectedNode
{
get => _selectedNode;
private set
{
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
new ControlDetailsViewModel(this, value.Visual) :
null;
}
}
@ -62,6 +54,63 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public Regex FilterRegex { get; set; }
private void UpdateFilterRegex()
{
void ClearError()
{
if (_errors.Remove(nameof(PropertyFilter)))
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
if (UseRegexFilter)
{
try
{
FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
ClearError();
}
catch (Exception exception)
{
_errors[nameof(PropertyFilter)] = exception.Message;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
else
{
ClearError();
}
}
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set
{
if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public void Dispose()
{
foreach (var node in Nodes)
@ -103,8 +152,8 @@ namespace Avalonia.Diagnostics.ViewModels
if (node != null)
{
SelectedNode = node;
ExpandNode(node.Parent);
Selection.SelectedIndex = node.Index;
}
}
@ -138,5 +187,17 @@ namespace Avalonia.Diagnostics.ViewModels
return null;
}
public IEnumerable GetErrors(string propertyName)
{
if (_errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}
}

45
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -2,24 +2,31 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
BorderThickness="0"
Text="{Binding PropertyFilter}"
Watermark="Filter properties"/>
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<TextBox Grid.Row="0" Grid.Column="0"
BorderThickness="0"
Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties" />
<CheckBox Grid.Row="0"
Grid.Column="1"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}"/>
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1" Grid.ColumnSpan="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}" />
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

9
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -16,6 +16,15 @@
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">

2
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -6,7 +6,7 @@
<TreeView Name="tree"
BorderThickness="0"
Items="{Binding Nodes}"
Selection="{Binding Selection}">
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">

71
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -1,7 +1,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
@ -11,45 +11,78 @@ namespace Avalonia.Diagnostics.Views
{
internal class TreePageView : UserControl
{
private Control _adorner;
private readonly Panel _adorner;
private AdornerLayer _currentLayer;
private TreeView _tree;
public TreePageView()
{
this.InitializeComponent();
InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
{
ClipToBounds = false,
Children =
{
//Padding frame
new Border { BorderBrush = new SolidColorBrush(Colors.Green, 0.5) },
//Content frame
new Border { Background = new SolidColorBrush(Color.FromRgb(160, 197, 232), 0.5) },
//Margin frame
new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) }
},
};
}
protected void AddAdorner(object sender, PointerEventArgs e)
{
var node = (TreeNode)((Control)sender).DataContext;
var layer = AdornerLayer.GetAdornerLayer(node.Visual);
var visual = (Visual)node.Visual;
_currentLayer = AdornerLayer.GetAdornerLayer(visual);
if (layer != null)
if (_currentLayer == null ||
_currentLayer.Children.Contains(_adorner))
{
if (_adorner != null)
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
}
return;
}
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Visual,
};
_currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual);
var vm = (TreePageViewModel) DataContext;
layer.Children.Add(_adorner);
if (vm.MainView.ShouldVisualizeMarginPadding)
{
var paddingBorder = (Border)_adorner.Children[0];
paddingBorder.BorderThickness = visual.GetValue(PaddingProperty);
var contentBorder = (Border)_adorner.Children[1];
contentBorder.Margin = visual.GetValue(PaddingProperty);
var marginBorder = (Border)_adorner.Children[2];
marginBorder.BorderThickness = visual.GetValue(MarginProperty);
marginBorder.Margin = InvertThickness(visual.GetValue(MarginProperty));
}
}
private static Thickness InvertThickness(Thickness input)
{
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
}
protected void RemoveAdorner(object sender, PointerEventArgs e)
{
if (_adorner != null)
foreach (var border in _adorner.Children.OfType<Border>())
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
border.Margin = default;
border.Padding = default;
border.BorderThickness = default;
}
_currentLayer?.Children.Remove(_adorner);
_currentLayer = null;
}
private void InitializeComponent()

7
src/Avalonia.Input/Gestures.cs

@ -96,8 +96,11 @@ namespace Avalonia.Input
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{
var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
e.Source.RaiseEvent(new RoutedEventArgs(et));
if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right)
{
var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
e.Source.RaiseEvent(new RoutedEventArgs(et));
}
}
}
}

3
src/Avalonia.Native/Avalonia.Native.csproj

@ -7,6 +7,7 @@
<CastXmlPath Condition="Exists('/usr/bin/castxml')">/usr/bin/castxml</CastXmlPath>
<CastXmlPath Condition="Exists('/usr/local/bin/castxml')">/usr/local/bin/castxml</CastXmlPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SharpGenGenerateConsumerBindMapping>false</SharpGenGenerateConsumerBindMapping>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release' AND '$([MSBuild]::IsOSPlatform(OSX))' == 'true'">
@ -18,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<SharpGenMapping Include="Mappings.xml" />
<!--<SharpGenMapping Include="Mappings.xml" />-->
<PackageReference Include="SharpGenTools.Sdk" Version="1.2.1" PrivateAssets="all" />
<PackageReference Include="SharpGen.Runtime" Version="1.2.1" />
<PackageReference Include="SharpGen.Runtime.COM" Version="1.2.0" />

17
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -110,11 +110,20 @@ namespace Avalonia.Native
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
;
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory));
if (_options.UseGpu)
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>()
.ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay()));
{
try
{
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>()
.ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay()));
}
catch (Exception)
{
// ignored
}
}
}
public IWindowImpl CreateWindow()

628
src/Avalonia.Native/Generated/Enumerations.cs

@ -0,0 +1,628 @@
// <auto-generated/>
namespace Avalonia.Native.Interop
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnDragDropEffects</unmanaged>
/// <unmanaged-short>AvnDragDropEffects</unmanaged-short>
public enum AvnDragDropEffects : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>None</unmanaged>
/// <unmanaged-short>None</unmanaged-short>
None = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Copy</unmanaged>
/// <unmanaged-short>Copy</unmanaged-short>
Copy = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Move</unmanaged>
/// <unmanaged-short>Move</unmanaged-short>
Move = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Link</unmanaged>
/// <unmanaged-short>Link</unmanaged-short>
Link = unchecked ((System.Int32)(4))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnDragEventType</unmanaged>
/// <unmanaged-short>AvnDragEventType</unmanaged-short>
public enum AvnDragEventType : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Enter</unmanaged>
/// <unmanaged-short>Enter</unmanaged-short>
Enter = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Over</unmanaged>
/// <unmanaged-short>Over</unmanaged-short>
Over = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Leave</unmanaged>
/// <unmanaged-short>Leave</unmanaged-short>
Leave = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Drop</unmanaged>
/// <unmanaged-short>Drop</unmanaged-short>
Drop = unchecked ((System.Int32)(3))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnExtendClientAreaChromeHints</unmanaged>
/// <unmanaged-short>AvnExtendClientAreaChromeHints</unmanaged-short>
public enum AvnExtendClientAreaChromeHints : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnNoChrome</unmanaged>
/// <unmanaged-short>AvnNoChrome</unmanaged-short>
AvnNoChrome = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnSystemChrome</unmanaged>
/// <unmanaged-short>AvnSystemChrome</unmanaged-short>
AvnSystemChrome = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnPreferSystemChrome</unmanaged>
/// <unmanaged-short>AvnPreferSystemChrome</unmanaged-short>
AvnPreferSystemChrome = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnOSXThickTitleBar</unmanaged>
/// <unmanaged-short>AvnOSXThickTitleBar</unmanaged-short>
AvnOSXThickTitleBar = unchecked ((System.Int32)(8)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnDefaultChrome</unmanaged>
/// <unmanaged-short>AvnDefaultChrome</unmanaged-short>
AvnDefaultChrome = unchecked ((System.Int32)(1))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnInputModifiers</unmanaged>
/// <unmanaged-short>AvnInputModifiers</unmanaged-short>
public enum AvnInputModifiers : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnInputModifiersNone</unmanaged>
/// <unmanaged-short>AvnInputModifiersNone</unmanaged-short>
AvnInputModifiersNone = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Alt</unmanaged>
/// <unmanaged-short>Alt</unmanaged-short>
Alt = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Control</unmanaged>
/// <unmanaged-short>Control</unmanaged-short>
Control = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Shift</unmanaged>
/// <unmanaged-short>Shift</unmanaged-short>
Shift = unchecked ((System.Int32)(4)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Windows</unmanaged>
/// <unmanaged-short>Windows</unmanaged-short>
Windows = unchecked ((System.Int32)(8)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>LeftMouseButton</unmanaged>
/// <unmanaged-short>LeftMouseButton</unmanaged-short>
LeftMouseButton = unchecked ((System.Int32)(16)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>RightMouseButton</unmanaged>
/// <unmanaged-short>RightMouseButton</unmanaged-short>
RightMouseButton = unchecked ((System.Int32)(32)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>MiddleMouseButton</unmanaged>
/// <unmanaged-short>MiddleMouseButton</unmanaged-short>
MiddleMouseButton = unchecked ((System.Int32)(64)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton1MouseButton</unmanaged>
/// <unmanaged-short>XButton1MouseButton</unmanaged-short>
XButton1MouseButton = unchecked ((System.Int32)(128)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton2MouseButton</unmanaged>
/// <unmanaged-short>XButton2MouseButton</unmanaged-short>
XButton2MouseButton = unchecked ((System.Int32)(256))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnMenuItemToggleType</unmanaged>
/// <unmanaged-short>AvnMenuItemToggleType</unmanaged-short>
public enum AvnMenuItemToggleType : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>None</unmanaged>
/// <unmanaged-short>None</unmanaged-short>
None = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CheckMark</unmanaged>
/// <unmanaged-short>CheckMark</unmanaged-short>
CheckMark = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Radio</unmanaged>
/// <unmanaged-short>Radio</unmanaged-short>
Radio = unchecked ((System.Int32)(2))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnPixelFormat</unmanaged>
/// <unmanaged-short>AvnPixelFormat</unmanaged-short>
public enum AvnPixelFormat : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>kAvnRgb565</unmanaged>
/// <unmanaged-short>kAvnRgb565</unmanaged-short>
KAvnRgb565 = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>kAvnRgba8888</unmanaged>
/// <unmanaged-short>kAvnRgba8888</unmanaged-short>
KAvnRgba8888 = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>kAvnBgra8888</unmanaged>
/// <unmanaged-short>kAvnBgra8888</unmanaged-short>
KAvnBgra8888 = unchecked ((System.Int32)(2))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnRawKeyEventType</unmanaged>
/// <unmanaged-short>AvnRawKeyEventType</unmanaged-short>
public enum AvnRawKeyEventType : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>KeyDown</unmanaged>
/// <unmanaged-short>KeyDown</unmanaged-short>
KeyDown = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>KeyUp</unmanaged>
/// <unmanaged-short>KeyUp</unmanaged-short>
KeyUp = unchecked ((System.Int32)(1))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnRawMouseEventType</unmanaged>
/// <unmanaged-short>AvnRawMouseEventType</unmanaged-short>
public enum AvnRawMouseEventType : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>LeaveWindow</unmanaged>
/// <unmanaged-short>LeaveWindow</unmanaged-short>
LeaveWindow = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>LeftButtonDown</unmanaged>
/// <unmanaged-short>LeftButtonDown</unmanaged-short>
LeftButtonDown = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>LeftButtonUp</unmanaged>
/// <unmanaged-short>LeftButtonUp</unmanaged-short>
LeftButtonUp = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>RightButtonDown</unmanaged>
/// <unmanaged-short>RightButtonDown</unmanaged-short>
RightButtonDown = unchecked ((System.Int32)(3)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>RightButtonUp</unmanaged>
/// <unmanaged-short>RightButtonUp</unmanaged-short>
RightButtonUp = unchecked ((System.Int32)(4)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>MiddleButtonDown</unmanaged>
/// <unmanaged-short>MiddleButtonDown</unmanaged-short>
MiddleButtonDown = unchecked ((System.Int32)(5)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>MiddleButtonUp</unmanaged>
/// <unmanaged-short>MiddleButtonUp</unmanaged-short>
MiddleButtonUp = unchecked ((System.Int32)(6)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton1Down</unmanaged>
/// <unmanaged-short>XButton1Down</unmanaged-short>
XButton1Down = unchecked ((System.Int32)(7)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton1Up</unmanaged>
/// <unmanaged-short>XButton1Up</unmanaged-short>
XButton1Up = unchecked ((System.Int32)(8)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton2Down</unmanaged>
/// <unmanaged-short>XButton2Down</unmanaged-short>
XButton2Down = unchecked ((System.Int32)(9)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>XButton2Up</unmanaged>
/// <unmanaged-short>XButton2Up</unmanaged-short>
XButton2Up = unchecked ((System.Int32)(10)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Move</unmanaged>
/// <unmanaged-short>Move</unmanaged-short>
Move = unchecked ((System.Int32)(11)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Wheel</unmanaged>
/// <unmanaged-short>Wheel</unmanaged-short>
Wheel = unchecked ((System.Int32)(12)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>NonClientLeftButtonDown</unmanaged>
/// <unmanaged-short>NonClientLeftButtonDown</unmanaged-short>
NonClientLeftButtonDown = unchecked ((System.Int32)(13)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>TouchBegin</unmanaged>
/// <unmanaged-short>TouchBegin</unmanaged-short>
TouchBegin = unchecked ((System.Int32)(14)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>TouchUpdate</unmanaged>
/// <unmanaged-short>TouchUpdate</unmanaged-short>
TouchUpdate = unchecked ((System.Int32)(15)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>TouchEnd</unmanaged>
/// <unmanaged-short>TouchEnd</unmanaged-short>
TouchEnd = unchecked ((System.Int32)(16)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>TouchCancel</unmanaged>
/// <unmanaged-short>TouchCancel</unmanaged-short>
TouchCancel = unchecked ((System.Int32)(17))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnStandardCursorType</unmanaged>
/// <unmanaged-short>AvnStandardCursorType</unmanaged-short>
public enum AvnStandardCursorType : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorArrow</unmanaged>
/// <unmanaged-short>CursorArrow</unmanaged-short>
CursorArrow = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorIbeam</unmanaged>
/// <unmanaged-short>CursorIbeam</unmanaged-short>
CursorIbeam = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorWait</unmanaged>
/// <unmanaged-short>CursorWait</unmanaged-short>
CursorWait = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorCross</unmanaged>
/// <unmanaged-short>CursorCross</unmanaged-short>
CursorCross = unchecked ((System.Int32)(3)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorUpArrow</unmanaged>
/// <unmanaged-short>CursorUpArrow</unmanaged-short>
CursorUpArrow = unchecked ((System.Int32)(4)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorSizeWestEast</unmanaged>
/// <unmanaged-short>CursorSizeWestEast</unmanaged-short>
CursorSizeWestEast = unchecked ((System.Int32)(5)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorSizeNorthSouth</unmanaged>
/// <unmanaged-short>CursorSizeNorthSouth</unmanaged-short>
CursorSizeNorthSouth = unchecked ((System.Int32)(6)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorSizeAll</unmanaged>
/// <unmanaged-short>CursorSizeAll</unmanaged-short>
CursorSizeAll = unchecked ((System.Int32)(7)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorNo</unmanaged>
/// <unmanaged-short>CursorNo</unmanaged-short>
CursorNo = unchecked ((System.Int32)(8)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorHand</unmanaged>
/// <unmanaged-short>CursorHand</unmanaged-short>
CursorHand = unchecked ((System.Int32)(9)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorAppStarting</unmanaged>
/// <unmanaged-short>CursorAppStarting</unmanaged-short>
CursorAppStarting = unchecked ((System.Int32)(10)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorHelp</unmanaged>
/// <unmanaged-short>CursorHelp</unmanaged-short>
CursorHelp = unchecked ((System.Int32)(11)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorTopSide</unmanaged>
/// <unmanaged-short>CursorTopSide</unmanaged-short>
CursorTopSide = unchecked ((System.Int32)(12)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorBottomSize</unmanaged>
/// <unmanaged-short>CursorBottomSize</unmanaged-short>
CursorBottomSize = unchecked ((System.Int32)(13)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorLeftSide</unmanaged>
/// <unmanaged-short>CursorLeftSide</unmanaged-short>
CursorLeftSide = unchecked ((System.Int32)(14)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorRightSide</unmanaged>
/// <unmanaged-short>CursorRightSide</unmanaged-short>
CursorRightSide = unchecked ((System.Int32)(15)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorTopLeftCorner</unmanaged>
/// <unmanaged-short>CursorTopLeftCorner</unmanaged-short>
CursorTopLeftCorner = unchecked ((System.Int32)(16)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorTopRightCorner</unmanaged>
/// <unmanaged-short>CursorTopRightCorner</unmanaged-short>
CursorTopRightCorner = unchecked ((System.Int32)(17)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorBottomLeftCorner</unmanaged>
/// <unmanaged-short>CursorBottomLeftCorner</unmanaged-short>
CursorBottomLeftCorner = unchecked ((System.Int32)(18)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorBottomRightCorner</unmanaged>
/// <unmanaged-short>CursorBottomRightCorner</unmanaged-short>
CursorBottomRightCorner = unchecked ((System.Int32)(19)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorDragMove</unmanaged>
/// <unmanaged-short>CursorDragMove</unmanaged-short>
CursorDragMove = unchecked ((System.Int32)(20)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorDragCopy</unmanaged>
/// <unmanaged-short>CursorDragCopy</unmanaged-short>
CursorDragCopy = unchecked ((System.Int32)(21)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorDragLink</unmanaged>
/// <unmanaged-short>CursorDragLink</unmanaged-short>
CursorDragLink = unchecked ((System.Int32)(22)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>CursorNone</unmanaged>
/// <unmanaged-short>CursorNone</unmanaged-short>
CursorNone = unchecked ((System.Int32)(23))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnWindowEdge</unmanaged>
/// <unmanaged-short>AvnWindowEdge</unmanaged-short>
public enum AvnWindowEdge : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeNorthWest</unmanaged>
/// <unmanaged-short>WindowEdgeNorthWest</unmanaged-short>
WindowEdgeNorthWest = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeNorth</unmanaged>
/// <unmanaged-short>WindowEdgeNorth</unmanaged-short>
WindowEdgeNorth = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeNorthEast</unmanaged>
/// <unmanaged-short>WindowEdgeNorthEast</unmanaged-short>
WindowEdgeNorthEast = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeWest</unmanaged>
/// <unmanaged-short>WindowEdgeWest</unmanaged-short>
WindowEdgeWest = unchecked ((System.Int32)(3)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeEast</unmanaged>
/// <unmanaged-short>WindowEdgeEast</unmanaged-short>
WindowEdgeEast = unchecked ((System.Int32)(4)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeSouthWest</unmanaged>
/// <unmanaged-short>WindowEdgeSouthWest</unmanaged-short>
WindowEdgeSouthWest = unchecked ((System.Int32)(5)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeSouth</unmanaged>
/// <unmanaged-short>WindowEdgeSouth</unmanaged-short>
WindowEdgeSouth = unchecked ((System.Int32)(6)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WindowEdgeSouthEast</unmanaged>
/// <unmanaged-short>WindowEdgeSouthEast</unmanaged-short>
WindowEdgeSouthEast = unchecked ((System.Int32)(7))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnWindowState</unmanaged>
/// <unmanaged-short>AvnWindowState</unmanaged-short>
public enum AvnWindowState : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Normal</unmanaged>
/// <unmanaged-short>Normal</unmanaged-short>
Normal = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Minimized</unmanaged>
/// <unmanaged-short>Minimized</unmanaged-short>
Minimized = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Maximized</unmanaged>
/// <unmanaged-short>Maximized</unmanaged-short>
Maximized = unchecked ((System.Int32)(2)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>FullScreen</unmanaged>
/// <unmanaged-short>FullScreen</unmanaged-short>
FullScreen = unchecked ((System.Int32)(3))}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>SystemDecorations</unmanaged>
/// <unmanaged-short>SystemDecorations</unmanaged-short>
public enum SystemDecorations : System.Int32
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>SystemDecorationsNone</unmanaged>
/// <unmanaged-short>SystemDecorationsNone</unmanaged-short>
SystemDecorationsNone = unchecked ((System.Int32)(0)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>SystemDecorationsBorderOnly</unmanaged>
/// <unmanaged-short>SystemDecorationsBorderOnly</unmanaged-short>
SystemDecorationsBorderOnly = unchecked ((System.Int32)(1)),
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>SystemDecorationsFull</unmanaged>
/// <unmanaged-short>SystemDecorationsFull</unmanaged-short>
SystemDecorationsFull = unchecked ((System.Int32)(2))}
}

5
src/Avalonia.Native/Generated/Functions.cs

@ -0,0 +1,5 @@
// <auto-generated/>
namespace Avalonia.Native.Interop
{
}

3092
src/Avalonia.Native/Generated/Interfaces.cs

File diff suppressed because it is too large

202
src/Avalonia.Native/Generated/LocalInterop.cs

@ -0,0 +1,202 @@
// <auto-generated/>
namespace Avalonia.Native
{
internal static partial class LocalInterop
{
public static unsafe int CalliThisCallint(void *thisObject, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, void *param0, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid0(void *thisObject, Avalonia.Native.Interop.AvnPoint param0, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid0(void *thisObject, int param0, System.UInt32 param1, int param2, Avalonia.Native.Interop.AvnPoint param3, Avalonia.Native.Interop.AvnVector param4, void *methodPtr)
{
throw null;
}
public static unsafe System.Byte CalliThisCallSystemByte(void *thisObject, int param0, System.UInt32 param1, int param2, System.UInt32 param3, void *methodPtr)
{
throw null;
}
public static unsafe System.Byte CalliThisCallSystemByte(void *thisObject, System.UInt32 param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, double param0, void *methodPtr)
{
throw null;
}
public static unsafe Avalonia.Native.Interop.AvnDragDropEffects CalliThisCallAvaloniaNativeInteropAvnDragDropEffects0(void *thisObject, int param0, Avalonia.Native.Interop.AvnPoint param1, int param2, int param3, void *param4, void *param5, void *methodPtr)
{
throw null;
}
public static unsafe System.Byte CalliThisCallSystemByte(void *thisObject, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, int param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, void *param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, Avalonia.Native.Interop.AvnSize param0, Avalonia.Native.Interop.AvnSize param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, double param0, double param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, Avalonia.Native.Interop.AvnRect param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, int param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, Avalonia.Native.Interop.AvnPoint param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, Avalonia.Native.Interop.AvnPoint param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, void *param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, System.Byte param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, int param0, Avalonia.Native.Interop.AvnPoint param1, void *param2, void *param3, void *param4, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint0(void *thisObject, Avalonia.Native.Interop.AvnColor param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, double param0, void *methodPtr)
{
throw null;
}
public static unsafe System.IntPtr CalliThisCallSystemIntPtr(void *thisObject, void *methodPtr)
{
throw null;
}
public static unsafe System.IntPtr CalliThisCallSystemIntPtr(void *thisObject, int param0, int param1, void *param2, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, int param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, void *param0, void *param1, void *param2, void *param3, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, void *param0, void *param1, System.Byte param2, void *param3, void *param4, void *param5, void *param6, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, void *param0, void *param1, void *param2, void *param3, void *param4, void *param5, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, int param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, void *param0, void *param1, int param2, void *methodPtr)
{
throw null;
}
public static unsafe System.IntPtr CalliThisCallSystemIntPtr(void *thisObject, void *param0, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, void *param0, int param1, void *methodPtr)
{
throw null;
}
public static unsafe System.UInt32 CalliThisCallSystemUInt32(void *thisObject, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, System.UInt32 param0, void *param1, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, float param0, float param1, float param2, float param3, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, float param0, float param1, void *methodPtr)
{
throw null;
}
public static unsafe int CalliThisCallint(void *thisObject, void *param0, void *param1, void *param2, void *methodPtr)
{
throw null;
}
public static unsafe void CalliThisCallvoid(void *thisObject, int param0, System.Byte param1, void *methodPtr)
{
throw null;
}
}
}

246
src/Avalonia.Native/Generated/Structures.cs

@ -0,0 +1,246 @@
// <auto-generated/>
namespace Avalonia.Native.Interop
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnColor</unmanaged>
/// <unmanaged-short>AvnColor</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnColor
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Alpha</unmanaged>
/// <unmanaged-short>Alpha</unmanaged-short>
public System.Byte Alpha;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Red</unmanaged>
/// <unmanaged-short>Red</unmanaged-short>
public System.Byte Red;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Green</unmanaged>
/// <unmanaged-short>Green</unmanaged-short>
public System.Byte Green;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Blue</unmanaged>
/// <unmanaged-short>Blue</unmanaged-short>
public System.Byte Blue;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnFramebuffer</unmanaged>
/// <unmanaged-short>AvnFramebuffer</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnFramebuffer
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Data</unmanaged>
/// <unmanaged-short>Data</unmanaged-short>
public System.IntPtr Data;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Width</unmanaged>
/// <unmanaged-short>Width</unmanaged-short>
public System.Int32 Width;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Height</unmanaged>
/// <unmanaged-short>Height</unmanaged-short>
public System.Int32 Height;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Stride</unmanaged>
/// <unmanaged-short>Stride</unmanaged-short>
public System.Int32 Stride;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Dpi</unmanaged>
/// <unmanaged-short>Dpi</unmanaged-short>
public Avalonia.Native.Interop.AvnVector Dpi;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>PixelFormat</unmanaged>
/// <unmanaged-short>PixelFormat</unmanaged-short>
public Avalonia.Native.Interop.AvnPixelFormat PixelFormat;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnPixelSize</unmanaged>
/// <unmanaged-short>AvnPixelSize</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnPixelSize
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Width</unmanaged>
/// <unmanaged-short>Width</unmanaged-short>
public System.Int32 Width;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Height</unmanaged>
/// <unmanaged-short>Height</unmanaged-short>
public System.Int32 Height;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnPoint</unmanaged>
/// <unmanaged-short>AvnPoint</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnPoint
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>X</unmanaged>
/// <unmanaged-short>X</unmanaged-short>
public System.Double X;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Y</unmanaged>
/// <unmanaged-short>Y</unmanaged-short>
public System.Double Y;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnRect</unmanaged>
/// <unmanaged-short>AvnRect</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnRect
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>X</unmanaged>
/// <unmanaged-short>X</unmanaged-short>
public System.Double X;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Y</unmanaged>
/// <unmanaged-short>Y</unmanaged-short>
public System.Double Y;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Width</unmanaged>
/// <unmanaged-short>Width</unmanaged-short>
public System.Double Width;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Height</unmanaged>
/// <unmanaged-short>Height</unmanaged-short>
public System.Double Height;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnScreen</unmanaged>
/// <unmanaged-short>AvnScreen</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnScreen
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Bounds</unmanaged>
/// <unmanaged-short>Bounds</unmanaged-short>
public Avalonia.Native.Interop.AvnRect Bounds;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>WorkingArea</unmanaged>
/// <unmanaged-short>WorkingArea</unmanaged-short>
public Avalonia.Native.Interop.AvnRect WorkingArea;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>PixelDensity</unmanaged>
/// <unmanaged-short>PixelDensity</unmanaged-short>
public System.Single PixelDensity;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Primary</unmanaged>
/// <unmanaged-short>Primary</unmanaged-short>
public bool Primary
{
get => 0 != _Primary;
set => _Primary = (System.Byte)(value ? 1 : 0);
}
internal System.Byte _Primary;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnSize</unmanaged>
/// <unmanaged-short>AvnSize</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnSize
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Width</unmanaged>
/// <unmanaged-short>Width</unmanaged-short>
public System.Double Width;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Height</unmanaged>
/// <unmanaged-short>Height</unmanaged-short>
public System.Double Height;
}
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>AvnVector</unmanaged>
/// <unmanaged-short>AvnVector</unmanaged-short>
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 0, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
public partial struct AvnVector
{
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>X</unmanaged>
/// <unmanaged-short>X</unmanaged-short>
public System.Double X;
/// <summary>
/// No documentation.
/// </summary>
/// <unmanaged>Y</unmanaged>
/// <unmanaged-short>Y</unmanaged-short>
public System.Double Y;
}
}

27
src/Avalonia.Native/ScreenImpl.cs

@ -20,21 +20,26 @@ namespace Avalonia.Native
{
get
{
var count = ScreenCount;
var result = new Screen[count];
for(int i = 0; i < count; i++)
if (_native != null)
{
var screen = _native.GetScreen(i);
var count = ScreenCount;
var result = new Screen[count];
for (int i = 0; i < count; i++)
{
var screen = _native.GetScreen(i);
result[i] = new Screen(
screen.PixelDensity,
screen.Bounds.ToAvaloniaPixelRect(),
screen.WorkingArea.ToAvaloniaPixelRect(),
screen.Primary);
}
result[i] = new Screen(
screen.PixelDensity,
screen.Bounds.ToAvaloniaPixelRect(),
screen.WorkingArea.ToAvaloniaPixelRect(),
screen.Primary);
return result;
}
return result;
return Array.Empty<Screen>();
}
}

15
src/Avalonia.Native/WindowImplBase.cs

@ -94,8 +94,13 @@ namespace Avalonia.Native
{
get
{
var s = _native.GetClientSize();
return new Size(s.Width, s.Height);
if (_native != null)
{
var s = _native.GetClientSize();
return new Size(s.Width, s.Height);
}
return default;
}
}
@ -144,7 +149,6 @@ namespace Avalonia.Native
void IAvnWindowBaseEvents.Closed()
{
var n = _parent._native;
_parent._native = null;
try
{
_parent?.Closed?.Invoke();
@ -153,6 +157,7 @@ namespace Avalonia.Native
{
n?.Dispose();
}
_parent._mouse.Dispose();
}
@ -351,12 +356,12 @@ namespace Avalonia.Native
public Point PointToClient(PixelPoint point)
{
return _native.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint();
return _native?.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint() ?? default;
}
public PixelPoint PointToScreen(Point point)
{
return _native.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint();
return _native?.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint() ?? default;
}
public void Hide()

12
src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.ReactiveUI</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Pharmacist.Common" Version="1.8.1" />
</ItemGroup>
</Project>

2
src/Avalonia.Themes.Default/Expander.xaml

@ -86,7 +86,7 @@
<Style Selector="Expander /template/ ToggleButton#PART_toggle">
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="1">
<Border BorderThickness="1" Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto">
<Border Grid.Column="0" Width="20" Height="20" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Fill="{DynamicResource ThemeForegroundBrush}"

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

Loading…
Cancel
Save