Browse Source

Merge branch 'master' into fix/allow_opening_popup_when_not_attached

pull/4605/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
a182f106e3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 1
      .ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject
  3. 5
      .ncrunch/Sandbox.v3.ncrunchproject
  4. 53
      Avalonia.sln
  5. 3
      Avalonia.v3.ncrunchsolution
  6. 25
      azure-pipelines.yml
  7. 2
      build/NetFX.props
  8. 4
      build/SkiaSharp.props
  9. 1
      dirs.proj
  10. 2
      global.json
  11. 2
      native/Avalonia.Native/src/OSX/rendertarget.mm
  12. 55
      nukebuild/Build.cs
  13. 5
      nukebuild/BuildParameters.cs
  14. 1
      nukebuild/_build.csproj
  15. 5
      samples/ControlCatalog/MainView.xaml
  16. 5
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  17. 58
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  18. 35
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  19. 50
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  20. 1
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  21. 57
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  22. 8
      samples/RenderDemo/MainWindow.xaml
  23. 3
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  24. 48
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  25. 8
      samples/Sandbox/App.axaml
  26. 22
      samples/Sandbox/App.axaml.cs
  27. 4
      samples/Sandbox/MainWindow.axaml
  28. 20
      samples/Sandbox/MainWindow.axaml.cs
  29. 17
      samples/Sandbox/Program.cs
  30. 18
      samples/Sandbox/Sandbox.csproj
  31. 3
      src/Avalonia.Base/ApiCompatBaseline.txt
  32. 17
      src/Avalonia.Base/AvaloniaProperty.cs
  33. 48
      src/Avalonia.Base/AvaloniaProperty`1.cs
  34. 68
      src/Avalonia.Base/Collections/AvaloniaList.cs
  35. 17
      src/Avalonia.Base/DirectPropertyBase.cs
  36. 54
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  37. 7
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  38. 8
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  39. 2
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  40. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  41. 99
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  42. 5
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  43. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  44. 2
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  45. 2
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  46. 4
      src/Avalonia.Controls.DataGrid/EventArgs.cs
  47. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  48. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  49. 7
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  50. 96
      src/Avalonia.Controls/AutoCompleteBox.cs
  51. 2
      src/Avalonia.Controls/Button.cs
  52. 2
      src/Avalonia.Controls/ButtonSpinner.cs
  53. 2
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  54. 2
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  55. 2
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  56. 2
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  57. 2
      src/Avalonia.Controls/Chrome/TitleBar.cs
  58. 10
      src/Avalonia.Controls/ColumnDefinitions.cs
  59. 2
      src/Avalonia.Controls/DataValidationErrors.cs
  60. 4
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  61. 4
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  62. 3
      src/Avalonia.Controls/Expander.cs
  63. 9
      src/Avalonia.Controls/IconElement.cs
  64. 2
      src/Avalonia.Controls/ItemsControl.cs
  65. 1
      src/Avalonia.Controls/ListBox.cs
  66. 2
      src/Avalonia.Controls/ListBoxItem.cs
  67. 2
      src/Avalonia.Controls/MenuItem.cs
  68. 4
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  69. 2
      src/Avalonia.Controls/NativeMenu.Export.cs
  70. 2
      src/Avalonia.Controls/NativeMenuItem.cs
  71. 2
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  72. 2
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  73. 21
      src/Avalonia.Controls/PathIcon.cs
  74. 6
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  75. 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  76. 3
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  77. 2
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  78. 4
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  79. 339
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  80. 2
      src/Avalonia.Controls/Primitives/Thumb.cs
  81. 2
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  82. 2
      src/Avalonia.Controls/Primitives/Track.cs
  83. 2
      src/Avalonia.Controls/ProgressBar.cs
  84. 24
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  85. 36
      src/Avalonia.Controls/ScrollViewer.cs
  86. 278
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  87. 113
      src/Avalonia.Controls/Selection/SelectionModel.cs
  88. 2
      src/Avalonia.Controls/Slider.cs
  89. 7
      src/Avalonia.Controls/SplitView.cs
  90. 55
      src/Avalonia.Controls/TabControl.cs
  91. 2
      src/Avalonia.Controls/TabItem.cs
  92. 9
      src/Avalonia.Controls/TextBlock.cs
  93. 2
      src/Avalonia.Controls/TextBox.cs
  94. 4
      src/Avalonia.Controls/ToggleSwitch.cs
  95. 2
      src/Avalonia.Controls/ToolTip.cs
  96. 2
      src/Avalonia.Controls/TreeViewItem.cs
  97. 283
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  98. 6
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  99. 43
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  100. 51
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

1
.gitignore

@ -117,6 +117,7 @@ ClientBin/
*.[Pp]ublish.xml *.[Pp]ublish.xml
*.pfx *.pfx
*.publishsettings *.publishsettings
Events_Avalonia.cs
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

1
.ncrunch/Avalonia.Controls.UnitTests.net47.v3.ncrunchproject

@ -3,5 +3,6 @@
<HiddenComponentWarnings> <HiddenComponentWarnings>
<Value>MissingOrIgnoredProjectReference</Value> <Value>MissingOrIgnoredProjectReference</Value>
</HiddenComponentWarnings> </HiddenComponentWarnings>
<FixtureName>Avalonia.Controls.UnitTests.TimePickerTests</FixtureName>
</Settings> </Settings>
</ProjectConfiguration> </ProjectConfiguration>

5
.ncrunch/Sandbox.v3.ncrunchproject

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

53
Avalonia.sln

@ -222,6 +222,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr
EndProject 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}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@ -2012,6 +2016,54 @@ Global
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {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.ActiveCfg = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = 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
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -2070,6 +2122,7 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

3
Avalonia.v3.ncrunchsolution

@ -6,6 +6,9 @@
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value> <Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value>
</AdditionalFilesToIncludeForSolution> </AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>True</AllowParallelTestExecution> <AllowParallelTestExecution>True</AllowParallelTestExecution>
<CustomBuildProperties>
<Value>RunApiCompat = false</Value>
</CustomBuildProperties>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir> <ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<SolutionConfigured>True</SolutionConfigured> <SolutionConfigured>True</SolutionConfigured>
</Settings> </Settings>

25
azure-pipelines.yml

@ -35,16 +35,9 @@ jobs:
vmImage: 'macOS-10.14' vmImage: 'macOS-10.14'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.101' displayName: 'Use .NET Core SDK 3.1.401'
inputs: inputs:
packageType: sdk version: 3.1.401
version: 3.1.101
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 3.1.1'
inputs:
packageType: runtime
version: 3.1.1
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Mono 5.18' displayName: 'Install Mono 5.18'
@ -63,13 +56,6 @@ jobs:
xcodeVersion: '10' # Options: 8, 9, default, specifyPath xcodeVersion: '10' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./' args: '-derivedDataPath ./'
- task: CmdLine@2
displayName: 'Install CastXML'
inputs:
script: |
brew update
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
@ -88,7 +74,7 @@ jobs:
export PATH="$PATH:$HOME/.dotnet/tools" export PATH="$PATH:$HOME/.dotnet/tools"
dotnet --info dotnet --info
printenv printenv
nuke --target CiAzureOSX --configuration Release nuke --target CiAzureOSX --configuration Release --skip-previewer
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
@ -112,6 +98,11 @@ jobs:
pool: pool:
vmImage: 'windows-2019' vmImage: 'windows-2019'
steps: steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
version: 3.1.401
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:

2
build/NetFX.props

@ -1,7 +1,7 @@
<Project> <Project>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="All" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

4
build/SkiaSharp.props

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

1
dirs.proj

@ -7,6 +7,7 @@
<ProjectReference Remove="**/*.shproj" /> <ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" /> <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" /> <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<ProjectReference Remove="tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj" />
</ItemGroup> </ItemGroup>
<!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">--> <!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
<ItemGroup> <ItemGroup>

2
global.json

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "3.1.101" "version": "3.1.401"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43", "Microsoft.Build.Traversal": "1.0.43",

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

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

55
nukebuild/Build.cs

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using Nuke.Common; using Nuke.Common;
using Nuke.Common.Git; using Nuke.Common.Git;
@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm; using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities; using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections; using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
@ -124,6 +126,7 @@ partial class Build : NukeBuild
Target CompileHtmlPreviewer => _ => _ Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean) .DependsOn(Clean)
.OnlyWhenStatic(() => !Parameters.SkipPreviewer)
.Executes(() => .Executes(() =>
{ {
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
@ -135,11 +138,21 @@ partial class Build : NukeBuild
.SetWorkingDirectory(webappDir) .SetWorkingDirectory(webappDir)
.SetCommand("dist")); .SetCommand("dist"));
}); });
Target Compile => _ => _ Target CompileNative => _ => _
.DependsOn(Clean) .DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer) .OnlyWhenStatic(() => EnvironmentInfo.IsOsx)
.Executes(() => .Executes(() =>
{
var project = $"{RootDirectory}/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/";
var args = $"-project {project} -configuration {Parameters.Configuration} CONFIGURATION_BUILD_DIR={RootDirectory}/Build/Products/Release";
ProcessTasks.StartProcess("xcodebuild", args).AssertZeroExitCode();
});
Target Compile => _ => _
.DependsOn(Clean, CompileNative)
.DependsOn(CompileHtmlPreviewer)
.Executes(async () =>
{ {
if (Parameters.IsRunningOnWindows) if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c MsBuildCommon(Parameters.MSBuildSolution, c => c
@ -153,8 +166,44 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version) .AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration) .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) void RunCoreTest(string projectName)
{ {
Information($"Running tests from {projectName}"); Information($"Running tests from {projectName}");

5
nukebuild/BuildParameters.cs

@ -19,10 +19,14 @@ public partial class Build
[Parameter("force-nuget-version")] [Parameter("force-nuget-version")]
public string ForceNugetVersion { get; set; } public string ForceNugetVersion { get; set; }
[Parameter("skip-previewer")]
public bool SkipPreviewer { get; set; }
public class BuildParameters public class BuildParameters
{ {
public string Configuration { get; } public string Configuration { get; }
public bool SkipTests { get; } public bool SkipTests { get; }
public bool SkipPreviewer {get;}
public string MainRepo { get; } public string MainRepo { get; }
public string MasterBranch { get; } public string MasterBranch { get; }
public string RepositoryName { get; } public string RepositoryName { get; }
@ -63,6 +67,7 @@ public partial class Build
// ARGUMENTS // ARGUMENTS
Configuration = b.Configuration ?? "Release"; Configuration = b.Configuration ?? "Release";
SkipTests = b.SkipTests; SkipTests = b.SkipTests;
SkipPreviewer = b.SkipPreviewer;
// CONFIGURATION // CONFIGURATION
MainRepo = "https://github.com/AvaloniaUI/Avalonia"; MainRepo = "https://github.com/AvaloniaUI/Avalonia";

1
nukebuild/_build.csproj

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

5
samples/ControlCatalog/MainView.xaml

@ -45,7 +45,10 @@
<pages:ItemsRepeaterPage/> <pages:ItemsRepeaterPage/>
</TabItem> </TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem> <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem> <TabItem Header="ListBox"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:ListBoxPage/>
</TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem> <TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem> <TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem> <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>

5
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -51,6 +51,11 @@
Width="200" Width="200"
Margin="0,0,0,8" Margin="0,0,0,8"
FilterMode="None"/> FilterMode="None"/>
<TextBlock Text="Custom Autocomplete"/>
<AutoCompleteBox Name="CustomAutocompleteBox"
Width="200"
Margin="0,0,0,8"
FilterMode="None"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

58
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@ -92,13 +92,28 @@ namespace ControlCatalog.Pages
} }
public StateData[] States { get; private set; } public StateData[] States { get; private set; }
private LinkedList<string>[] BuildAllSentences()
{
return new string[]
{
"Hello world",
"No this is Patrick",
"Never gonna give you up",
"How does one patch KDE2 under FreeBSD"
}
.Select(x => new LinkedList<string>(x.Split(' ')))
.ToArray();
}
public LinkedList<string>[] Sentences { get; private set; }
public AutoCompleteBoxPage() public AutoCompleteBoxPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
States = BuildAllStates(); States = BuildAllStates();
Sentences = BuildAllSentences();
foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox"))
{ {
box.Items = States; box.Items = States;
} }
@ -116,6 +131,11 @@ namespace ControlCatalog.Pages
var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox"); var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
asyncBox.AsyncPopulator = PopulateAsync; asyncBox.AsyncPopulator = PopulateAsync;
var customAutocompleteBox = this.FindControl<AutoCompleteBox>("CustomAutocompleteBox");
customAutocompleteBox.Items = Sentences.SelectMany(x => x);
customAutocompleteBox.TextFilter = LastWordContains;
customAutocompleteBox.TextSelector = AppendWord;
} }
private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox() private IEnumerable<AutoCompleteBox> GetAllAutoCompleteBox()
{ {
@ -137,6 +157,42 @@ namespace ControlCatalog.Pages
.ToList(); .ToList();
} }
private bool LastWordContains(string searchText, string item)
{
var words = searchText.Split(' ');
var options = Sentences.Select(x => x.First).ToArray();
for (var i = 0; i < words.Length; ++i)
{
var word = words[i];
for (var j = 0; j < options.Length; ++j)
{
var option = options[j];
if (option == null)
continue;
if (i == words.Length - 1)
{
options[j] = option.Value.ToLower().Contains(word.ToLower()) ? option : null;
}
else
{
options[j] = option.Value.Equals(word, StringComparison.InvariantCultureIgnoreCase) ? option.Next : null;
}
}
}
return options.Any(x => x != null && x.Value == item);
}
private string AppendWord(string text, string item)
{
string[] parts = text.Split(' ');
if (parts.Length == 0)
return item;
parts[parts.Length - 1] = item;
return string.Join(" ", parts);
}
private void InitializeComponent() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);

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

@ -1,8 +1,12 @@
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using ControlCatalog.Models; using ControlCatalog.Models;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -11,12 +15,22 @@ namespace ControlCatalog.Pages
public DataGridPage() public DataGridPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer());
var collectionView1 = new DataGridCollectionView(Countries.All);
collectionView1.SortDescriptions.Add(dataGridSortDescription);
var dg1 = this.FindControl<DataGrid>("dataGrid1"); var dg1 = this.FindControl<DataGrid>("dataGrid1");
dg1.IsReadOnly = true; dg1.IsReadOnly = true;
dg1.LoadingRow += Dg1_LoadingRow; dg1.LoadingRow += Dg1_LoadingRow;
var collectionView1 = new DataGridCollectionView(Countries.All); dg1.Sorting += (s, a) =>
//collectionView.GroupDescriptions.Add(new PathGroupDescription("Region")); {
var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path;
if (property == dataGridSortDescription.PropertyPath
&& !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
{
collectionView1.SortDescriptions.Add(dataGridSortDescription);
}
};
dg1.Items = collectionView1; dg1.Items = collectionView1;
var dg2 = this.FindControl<DataGrid>("dataGridGrouping"); var dg2 = this.FindControl<DataGrid>("dataGridGrouping");
@ -53,5 +67,20 @@ namespace ControlCatalog.Pages
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private class ReversedStringComparer : IComparer<object>, IComparer
{
public int Compare(object x, object y)
{
if (x is string left && y is string right)
{
var reversedLeft = new string(left.Reverse().ToArray());
var reversedRight = new string(right.Reverse().ToArray());
return reversedLeft.CompareTo(reversedRight);
}
return Comparer.Default.Compare(x, y);
}
}
} }
} }

50
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -1,35 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage"> x:Class="ControlCatalog.Pages.ListBoxPage">
<StackPanel Orientation="Vertical" Spacing="4"> <DockPanel>
<TextBlock Classes="h1">ListBox</TextBlock> <StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock> <TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="True"
SelectionMode="{Binding SelectionMode}"
Width="250"
Height="350"/>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem>
<ComboBoxItem>Multiple</ComboBoxItem>
<ComboBoxItem>Toggle</ComboBoxItem>
<ComboBoxItem>AlwaysSelected</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> <StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
<CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4">
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
</StackPanel>
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode}"/>
</DockPanel>
</UserControl> </UserControl>

1
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.OpenGL.Controls;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.Threading; using Avalonia.Threading;
using static Avalonia.OpenGL.GlConsts; using static Avalonia.OpenGL.GlConsts;

57
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -10,22 +10,39 @@ namespace ControlCatalog.ViewModels
{ {
public class ListBoxPageViewModel : ReactiveObject public class ListBoxPageViewModel : ReactiveObject
{ {
private bool _multiple;
private bool _toggle;
private bool _alwaysSelected;
private bool _autoScrollToSelectedItem = true;
private int _counter; private int _counter;
private SelectionMode _selectionMode; private ObservableAsPropertyHelper<SelectionMode> _selectionMode;
public ListBoxPageViewModel() public ListBoxPageViewModel()
{ {
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem())); Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<string>(); Selection = new SelectionModel<string>();
Selection.Select(1); Selection.Select(1);
_selectionMode = this.WhenAnyValue(
x => x.Multiple,
x => x.Toggle,
x => x.AlwaysSelected,
(m, t, a) =>
(m ? SelectionMode.Multiple : 0) |
(t ? SelectionMode.Toggle : 0) |
(a ? SelectionMode.AlwaysSelected : 0))
.ToProperty(this, x => x.SelectionMode);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() => RemoveItemCommand = ReactiveCommand.Create(() =>
{ {
while (Selection.Count > 0) var items = Selection.SelectedItems.ToList();
foreach (var item in items)
{ {
Items.Remove(Selection.SelectedItems.First()); Items.Remove(item);
} }
}); });
@ -42,25 +59,37 @@ namespace ControlCatalog.ViewModels
} }
public ObservableCollection<string> Items { get; } public ObservableCollection<string> Items { get; }
public SelectionModel<string> Selection { get; } public SelectionModel<string> Selection { get; }
public SelectionMode SelectionMode => _selectionMode.Value;
public ReactiveCommand<Unit, Unit> AddItemCommand { get; } public bool Multiple
{
get => _multiple;
set => this.RaiseAndSetIfChanged(ref _multiple, value);
}
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; } public bool Toggle
{
get => _toggle;
set => this.RaiseAndSetIfChanged(ref _toggle, value);
}
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; } public bool AlwaysSelected
{
get => _alwaysSelected;
set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value);
}
public SelectionMode SelectionMode public bool AutoScrollToSelectedItem
{ {
get => _selectionMode; get => _autoScrollToSelectedItem;
set set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
{
Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
} }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
private string GenerateItem() => $"Item {_counter++.ToString()}"; private string GenerateItem() => $"Item {_counter++.ToString()}";
} }
} }

8
samples/RenderDemo/MainWindow.xaml

@ -3,8 +3,8 @@
x:Class="RenderDemo.MainWindow" x:Class="RenderDemo.MainWindow"
Title="AvaloniaUI Rendering Test" Title="AvaloniaUI Rendering Test"
xmlns:pages="clr-namespace:RenderDemo.Pages" xmlns:pages="clr-namespace:RenderDemo.Pages"
Width="800" Width="{Binding Width, Mode=TwoWay}"
Height="600"> Height="{Binding Height, Mode=TwoWay}">
<DockPanel> <DockPanel>
<Menu DockPanel.Dock="Top"> <Menu DockPanel.Dock="Top">
<MenuItem Header="Rendering"> <MenuItem Header="Rendering">
@ -24,6 +24,10 @@
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
<MenuItem Header="Tests">
<MenuItem Header="Resize window"
Command="{Binding ResizeWindow}"/>
</MenuItem>
</Menu> </Menu>
<TabControl Classes="sidebar"> <TabControl Classes="sidebar">
<TabItem Header="Animations"> <TabItem Header="Animations">

3
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -61,7 +61,6 @@ namespace RenderDemo.Pages
{ {
Foreground = Brushes.Black, Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
}; };
drawingGroup.Children.Add(glyphRunDrawing); drawingGroup.Children.Add(glyphRunDrawing);
@ -69,7 +68,7 @@ namespace RenderDemo.Pages
var geometryDrawing = new GeometryDrawing var geometryDrawing = new GeometryDrawing
{ {
Pen = new Pen(Brushes.Black), Pen = new Pen(Brushes.Black),
Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds } Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
}; };
drawingGroup.Children.Add(geometryDrawing); drawingGroup.Children.Add(geometryDrawing);

48
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@ -1,5 +1,6 @@
using System; using System.Reactive;
using System.Reactive; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
namespace RenderDemo.ViewModels namespace RenderDemo.ViewModels
@ -8,26 +9,61 @@ namespace RenderDemo.ViewModels
{ {
private bool drawDirtyRects = false; private bool drawDirtyRects = false;
private bool drawFps = true; private bool drawFps = true;
private double width = 800;
private double height = 600;
public MainWindowViewModel() public MainWindowViewModel()
{ {
ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects); ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps); ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps);
ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync);
} }
public bool DrawDirtyRects public bool DrawDirtyRects
{ {
get { return drawDirtyRects; } get => drawDirtyRects;
set { this.RaiseAndSetIfChanged(ref drawDirtyRects, value); } set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value);
} }
public bool DrawFps public bool DrawFps
{ {
get { return drawFps; } get => drawFps;
set { this.RaiseAndSetIfChanged(ref drawFps, value); } set => this.RaiseAndSetIfChanged(ref drawFps, value);
}
public double Width
{
get => width;
set => this.RaiseAndSetIfChanged(ref width, value);
}
public double Height
{
get => height;
set => this.RaiseAndSetIfChanged(ref height, value);
} }
public ReactiveCommand<Unit, bool> ToggleDrawDirtyRects { get; } public ReactiveCommand<Unit, bool> ToggleDrawDirtyRects { get; }
public ReactiveCommand<Unit, bool> ToggleDrawFps { get; } public ReactiveCommand<Unit, bool> ToggleDrawFps { get; }
public ReactiveCommand<Unit, Unit> ResizeWindow { get; }
private async Task ResizeWindowAsync()
{
for (int i = 0; i < 30; i++)
{
Width += 10;
Height += 5;
await Task.Delay(10);
}
await Task.Delay(10);
for (int i = 0; i < 30; i++)
{
Width -= 10;
Height -= 5;
await Task.Delay(10);
}
}
} }
} }

8
samples/Sandbox/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sandbox.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml"/>
</Application.Styles>
</Application>

22
samples/Sandbox/App.axaml.cs

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

4
samples/Sandbox/MainWindow.axaml

@ -0,0 +1,4 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow">
</Window>

20
samples/Sandbox/MainWindow.axaml.cs

@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sandbox
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

17
samples/Sandbox/Program.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.ReactiveUI;
namespace Sandbox
{
public class Program
{
static void Main(string[] args)
{
AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug()
.StartWithClassicDesktopLifetime(args);
}
}
}

18
samples/Sandbox/Sandbox.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

3
src/Avalonia.Base/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.Base:
CannotAddAbstractMembers : Member 'protected System.IObservable<Avalonia.AvaloniaPropertyChangedEventArgs> Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract.
Total Issues: 1

17
src/Avalonia.Base/AvaloniaProperty.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -18,7 +17,6 @@ namespace Avalonia
public static readonly object UnsetValue = new UnsetValueType(); public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId; private static int s_nextId;
private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
private readonly PropertyMetadata _defaultMetadata; private readonly PropertyMetadata _defaultMetadata;
private readonly Dictionary<Type, PropertyMetadata> _metadata; private readonly Dictionary<Type, PropertyMetadata> _metadata;
private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>(); private readonly Dictionary<Type, PropertyMetadata> _metadataCache = new Dictionary<Type, PropertyMetadata>();
@ -50,7 +48,6 @@ namespace Avalonia
throw new ArgumentException("'name' may not contain periods."); throw new ArgumentException("'name' may not contain periods.");
} }
_changed = new Subject<AvaloniaPropertyChangedEventArgs>();
_metadata = new Dictionary<Type, PropertyMetadata>(); _metadata = new Dictionary<Type, PropertyMetadata>();
Name = name; Name = name;
@ -77,7 +74,6 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(ownerType != null); Contract.Requires<ArgumentNullException>(ownerType != null);
_changed = source._changed;
_metadata = new Dictionary<Type, PropertyMetadata>(); _metadata = new Dictionary<Type, PropertyMetadata>();
Name = source.Name; Name = source.Name;
@ -139,7 +135,7 @@ namespace Avalonia
/// An observable that is fired when this property changes on any /// An observable that is fired when this property changes on any
/// <see cref="AvaloniaObject"/> instance. /// <see cref="AvaloniaObject"/> instance.
/// </value> /// </value>
public IObservable<AvaloniaPropertyChangedEventArgs> Changed => _changed; public IObservable<AvaloniaPropertyChangedEventArgs> Changed => GetChanged();
/// <summary> /// <summary>
/// Gets a method that gets called before and after the property starts being notified on an /// Gets a method that gets called before and after the property starts being notified on an
@ -474,15 +470,6 @@ namespace Avalonia
public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data) public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
where TData : struct; where TData : struct;
/// <summary>
/// Notifies the <see cref="Changed"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyChanged(AvaloniaPropertyChangedEventArgs e)
{
_changed.OnNext(e);
}
/// <summary> /// <summary>
/// Routes an untyped ClearValue call to a typed call. /// Routes an untyped ClearValue call to a typed call.
/// </summary> /// </summary>
@ -553,6 +540,8 @@ namespace Avalonia
_hasMetadataOverrides = true; _hasMetadataOverrides = true;
} }
protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
private PropertyMetadata GetMetadataWithOverrides(Type type) private PropertyMetadata GetMetadataWithOverrides(Type type)
{ {
if (type is null) if (type is null)

48
src/Avalonia.Base/AvaloniaProperty`1.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Reactive.Subjects;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -10,6 +11,8 @@ namespace Avalonia
/// <typeparam name="TValue">The value type of the property.</typeparam> /// <typeparam name="TValue">The value type of the property.</typeparam>
public abstract class AvaloniaProperty<TValue> : AvaloniaProperty public abstract class AvaloniaProperty<TValue> : AvaloniaProperty
{ {
private readonly Subject<AvaloniaPropertyChangedEventArgs<TValue>> _changed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class. /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
/// </summary> /// </summary>
@ -24,22 +27,61 @@ namespace Avalonia
Action<IAvaloniaObject, bool> notifying = null) Action<IAvaloniaObject, bool> notifying = null)
: base(name, typeof(TValue), ownerType, metadata, notifying) : base(name, typeof(TValue), ownerType, metadata, notifying)
{ {
_changed = new Subject<AvaloniaPropertyChangedEventArgs<TValue>>();
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class. /// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
/// </summary> /// </summary>
/// <param name="source">The property to copy.</param> /// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param> /// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param> /// <param name="metadata">Optional overridden metadata.</param>
[Obsolete("Use constructor with AvaloniaProperty<TValue> instead.", true)]
protected AvaloniaProperty( protected AvaloniaProperty(
AvaloniaProperty source, AvaloniaProperty source,
Type ownerType, Type ownerType,
PropertyMetadata metadata)
: this(source as AvaloniaProperty<TValue> ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty{TValue}"/> class.
/// </summary>
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param>
protected AvaloniaProperty(
AvaloniaProperty<TValue> source,
Type ownerType,
PropertyMetadata metadata) PropertyMetadata metadata)
: base(source, ownerType, metadata) : base(source, ownerType, metadata)
{ {
_changed = source._changed;
}
/// <summary>
/// Gets an observable that is fired when this property changes on any
/// <see cref="AvaloniaObject"/> instance.
/// </summary>
/// <value>
/// An observable that is fired when this property changes on any
/// <see cref="AvaloniaObject"/> instance.
/// </value>
public new IObservable<AvaloniaPropertyChangedEventArgs<TValue>> Changed => _changed;
/// <summary>
/// Notifies the <see cref="Changed"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyChanged(AvaloniaPropertyChangedEventArgs<TValue> e)
{
_changed.OnNext(e);
} }
protected override IObservable<AvaloniaPropertyChangedEventArgs> GetChanged() => Changed;
protected BindingValue<object> TryConvert(object value) protected BindingValue<object> TryConvert(object value)
{ {
if (value == UnsetValue) if (value == UnsetValue)

68
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -543,7 +543,73 @@ namespace Avalonia.Collections
/// <inheritdoc/> /// <inheritdoc/>
void ICollection.CopyTo(Array array, int index) void ICollection.CopyTo(Array array, int index)
{ {
_inner.CopyTo((T[])array, index); if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (array.Rank != 1)
{
throw new ArgumentException("Multi-dimensional arrays are not supported.");
}
if (array.GetLowerBound(0) != 0)
{
throw new ArgumentException("Non-zero lower bounds are not supported.");
}
if (index < 0)
{
throw new ArgumentException("Invalid index.");
}
if (array.Length - index < Count)
{
throw new ArgumentException("The target array is too small.");
}
if (array is T[] tArray)
{
_inner.CopyTo(tArray, index);
}
else
{
//
// Catch the obvious case assignment will fail.
// We can't find all possible problems by doing the check though.
// For example, if the element type of the Array is derived from T,
// we can't figure out if we can successfully copy the element beforehand.
//
Type targetType = array.GetType().GetElementType()!;
Type sourceType = typeof(T);
if (!(targetType.IsAssignableFrom(sourceType) || sourceType.IsAssignableFrom(targetType)))
{
throw new ArgumentException("Invalid array type");
}
//
// We can't cast array of value type to object[], so we don't support
// widening of primitive types here.
//
object[] objects = array as object[];
if (objects == null)
{
throw new ArgumentException("Invalid array type");
}
int count = _inner.Count;
try
{
for (int i = 0; i < count; i++)
{
objects[index++] = _inner[i];
}
}
catch (ArrayTypeMismatchException)
{
throw new ArgumentException("Invalid array type");
}
}
} }
/// <inheritdoc/> /// <inheritdoc/>

17
src/Avalonia.Base/DirectPropertyBase.cs

@ -32,15 +32,30 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class. /// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
/// </summary> /// </summary>
/// <param name="source">The property to copy.</param> /// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param> /// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param> /// <param name="metadata">Optional overridden metadata.</param>
[Obsolete("Use constructor with DirectPropertyBase<TValue> instead.", true)]
protected DirectPropertyBase( protected DirectPropertyBase(
AvaloniaProperty source, AvaloniaProperty source,
Type ownerType, Type ownerType,
PropertyMetadata metadata) PropertyMetadata metadata)
: this(source as DirectPropertyBase<TValue> ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
/// </summary>
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param>
protected DirectPropertyBase(
DirectPropertyBase<TValue> source,
Type ownerType,
PropertyMetadata metadata)
: base(source, ownerType, metadata) : base(source, ownerType, metadata)
{ {
} }

54
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@ -1,19 +1,21 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Utilities;
namespace Avalonia.Collections namespace Avalonia.Collections
{ {
public abstract class DataGridSortDescription public abstract class DataGridSortDescription
{ {
public virtual string PropertyPath => null; public virtual string PropertyPath => null;
public virtual bool Descending => false;
[Obsolete("Use Direction property to read or override sorting direction.")]
public virtual bool Descending => Direction == ListSortDirection.Descending;
public virtual ListSortDirection Direction => ListSortDirection.Ascending;
public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath); public bool HasPropertyPath => !String.IsNullOrEmpty(PropertyPath);
public abstract IComparer<object> Comparer { get; } public abstract IComparer<object> Comparer { get; }
@ -26,7 +28,7 @@ namespace Avalonia.Collections
return seq.ThenBy(o => o, Comparer); return seq.ThenBy(o => o, Comparer);
} }
internal virtual DataGridSortDescription SwitchSortDirection() public virtual DataGridSortDescription SwitchSortDirection()
{ {
return this; return this;
} }
@ -105,7 +107,7 @@ namespace Avalonia.Collections
private class DataGridPathSortDescription : DataGridSortDescription private class DataGridPathSortDescription : DataGridSortDescription
{ {
private readonly bool _descending; private readonly ListSortDirection _direction;
private readonly string _propertyPath; private readonly string _propertyPath;
private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer; private readonly Lazy<CultureSensitiveComparer> _cultureSensitiveComparer;
private readonly Lazy<IComparer<object>> _comparer; private readonly Lazy<IComparer<object>> _comparer;
@ -118,7 +120,7 @@ namespace Avalonia.Collections
{ {
if (_internalComparerTyped == null && _internalComparer != null) if (_internalComparerTyped == null && _internalComparer != null)
{ {
if (_internalComparerTyped is IComparer<object> c) if (_internalComparer is IComparer<object> c)
_internalComparerTyped = c; _internalComparerTyped = c;
else else
_internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y)); _internalComparerTyped = Comparer<object>.Create((x, y) => _internalComparer.Compare(x, y));
@ -130,19 +132,20 @@ namespace Avalonia.Collections
public override string PropertyPath => _propertyPath; public override string PropertyPath => _propertyPath;
public override IComparer<object> Comparer => _comparer.Value; public override IComparer<object> Comparer => _comparer.Value;
public override bool Descending => _descending; public override ListSortDirection Direction => _direction;
public DataGridPathSortDescription(string propertyPath, bool descending, CultureInfo culture) public DataGridPathSortDescription(string propertyPath, ListSortDirection direction, IComparer internalComparer, CultureInfo culture)
{ {
_propertyPath = propertyPath; _propertyPath = propertyPath;
_descending = descending; _direction = direction;
_cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture)); _cultureSensitiveComparer = new Lazy<CultureSensitiveComparer>(() => new CultureSensitiveComparer(culture ?? CultureInfo.CurrentCulture));
_internalComparer = internalComparer;
_comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y))); _comparer = new Lazy<IComparer<object>>(() => Comparer<object>.Create((x, y) => Compare(x, y)));
} }
private DataGridPathSortDescription(DataGridPathSortDescription inner, bool descending) private DataGridPathSortDescription(DataGridPathSortDescription inner, ListSortDirection direction)
{ {
_propertyPath = inner._propertyPath; _propertyPath = inner._propertyPath;
_descending = descending; _direction = direction;
_propertyType = inner._propertyType; _propertyType = inner._propertyType;
_cultureSensitiveComparer = inner._cultureSensitiveComparer; _cultureSensitiveComparer = inner._cultureSensitiveComparer;
_internalComparer = inner._internalComparer; _internalComparer = inner._internalComparer;
@ -201,7 +204,7 @@ namespace Avalonia.Collections
result = _internalComparer?.Compare(v1, v2) ?? 0; result = _internalComparer?.Compare(v1, v2) ?? 0;
if (_descending) if (Direction == ListSortDirection.Descending)
return -result; return -result;
else else
return result; return result;
@ -218,7 +221,7 @@ namespace Avalonia.Collections
} }
public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq) public override IOrderedEnumerable<object> OrderBy(IEnumerable<object> seq)
{ {
if(_descending) if (Direction == ListSortDirection.Descending)
{ {
return seq.OrderByDescending(o => GetValue(o), InternalComparer); return seq.OrderByDescending(o => GetValue(o), InternalComparer);
} }
@ -229,7 +232,7 @@ namespace Avalonia.Collections
} }
public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq)
{ {
if (_descending) if (Direction == ListSortDirection.Descending)
{ {
return seq.ThenByDescending(o => GetValue(o), InternalComparer); return seq.ThenByDescending(o => GetValue(o), InternalComparer);
} }
@ -239,15 +242,28 @@ namespace Avalonia.Collections
} }
} }
internal override DataGridSortDescription SwitchSortDirection() public override DataGridSortDescription SwitchSortDirection()
{ {
return new DataGridPathSortDescription(this, !_descending); var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
return new DataGridPathSortDescription(this, newDirection);
} }
} }
public static DataGridSortDescription FromPath(string propertyPath, bool descending = false, CultureInfo culture = null) public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction = ListSortDirection.Ascending, CultureInfo culture = null)
{
return new DataGridPathSortDescription(propertyPath, direction, null, culture);
}
[Obsolete("Use overload taking a ListSortDirection.")]
public static DataGridSortDescription FromPath(string propertyPath, bool descending, CultureInfo culture = null)
{
return new DataGridPathSortDescription(propertyPath, descending ? ListSortDirection.Descending : ListSortDirection.Ascending, null, culture);
}
public static DataGridSortDescription FromPath(string propertyPath, ListSortDirection direction, IComparer comparer)
{ {
return new DataGridPathSortDescription(propertyPath, descending, culture); return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
} }
} }

7
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -24,12 +24,14 @@ using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Controls.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Displays data in a customizable grid. /// Displays data in a customizable grid.
/// </summary> /// </summary>
[PseudoClasses(":invalid")]
public partial class DataGrid : TemplatedControl public partial class DataGrid : TemplatedControl
{ {
private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter"; private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter";
@ -1229,6 +1231,11 @@ namespace Avalonia.Controls
remove { AddHandler(SelectionChangedEvent, value); } remove { AddHandler(SelectionChangedEvent, value); }
} }
/// <summary>
/// Occurs when the <see cref="DataGridColumn"/> sorting request is triggered.
/// </summary>
public event EventHandler<DataGridColumnEventArgs> Sorting;
/// <summary> /// <summary>
/// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" /> /// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" />
/// object becomes available for reuse. /// object becomes available for reuse.

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

@ -49,8 +49,12 @@ namespace Avalonia.Controls
{ {
if(_binding is Avalonia.Data.Binding binding) if(_binding is Avalonia.Data.Binding binding)
{ {
// Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. if (binding.Mode == BindingMode.OneWayToSource)
if (!String.IsNullOrEmpty(binding.Path)) {
throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
}
if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
{ {
binding.Mode = BindingMode.TwoWay; binding.Mode = BindingMode.TwoWay;
} }

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

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Input; using Avalonia.Input;
@ -12,6 +13,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell. /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell.
/// </summary> /// </summary>
[PseudoClasses(":selected", ":current", ":edited", ":invalid")]
public class DataGridCell : ContentControl public class DataGridCell : ContentControl
{ {
private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine"; private const string DATAGRIDCELL_elementRightGridLine = "PART_RightGridLine";

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

@ -1047,4 +1047,4 @@ namespace Avalonia.Controls
} }
} }

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

@ -14,12 +14,14 @@ using Avalonia.Utilities;
using System; using System;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> column header. /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> column header.
/// </summary> /// </summary>
[PseudoClasses(":dragIndicator", ":pressed", ":sortascending", ":sortdescending")]
public class DataGridColumnHeader : ContentControl public class DataGridColumnHeader : ContentControl
{ {
private enum DragMode private enum DragMode
@ -161,13 +163,14 @@ namespace Avalonia.Controls
var sort = OwningColumn.GetSortDescription(); var sort = OwningColumn.GetSortDescription();
if (sort != null) if (sort != null)
{ {
CurrentSortingState = sort.Descending ? ListSortDirection.Descending : ListSortDirection.Ascending; CurrentSortingState = sort.Direction;
} }
} }
PseudoClasses.Set(":sortascending", PseudoClasses.Set(":sortascending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Ascending); CurrentSortingState == ListSortDirection.Ascending);
PseudoClasses.Set(":sortdescending", PseudoClasses.Set(":sortdescending",
CurrentSortingState.HasValue && CurrentSortingState.Value == ListSortDirection.Descending); CurrentSortingState == ListSortDirection.Descending);
} }
internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn) internal void UpdateSeparatorVisibility(DataGridColumn lastVisibleColumn)
@ -215,70 +218,76 @@ namespace Avalonia.Controls
internal void ProcessSort(KeyModifiers keyModifiers) internal void ProcessSort(KeyModifiers keyModifiers)
{ {
// if we can sort: // if we can sort:
// - DataConnection.AllowSort is true, and
// - AllowUserToSortColumns and CanSort are true, and // - AllowUserToSortColumns and CanSort are true, and
// - OwningColumn is bound, and // - OwningColumn is bound
// - SortDescriptionsCollection exists, and
// - the column's data type is comparable
// then try to sort // then try to sort
if (OwningColumn != null if (OwningColumn != null
&& OwningGrid != null && OwningGrid != null
&& OwningGrid.EditingRow == null && OwningGrid.EditingRow == null
&& OwningColumn != OwningGrid.ColumnsInternal.FillerColumn && OwningColumn != OwningGrid.ColumnsInternal.FillerColumn
&& OwningGrid.DataConnection.AllowSort
&& OwningGrid.CanUserSortColumns && OwningGrid.CanUserSortColumns
&& OwningColumn.CanUserSort && OwningColumn.CanUserSort)
&& OwningGrid.DataConnection.SortDescriptions != null)
{ {
DataGrid owningGrid = OwningGrid; var ea = new DataGridColumnEventArgs(OwningColumn);
OwningGrid.OnColumnSorting(ea);
DataGridSortDescription newSort; if (!ea.Handled && OwningGrid.DataConnection.AllowSort && OwningGrid.DataConnection.SortDescriptions != null)
{
// - DataConnection.AllowSort is true, and
// - SortDescriptionsCollection exists, and
// - the column's data type is comparable
KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift); DataGrid owningGrid = OwningGrid;
DataGridSortDescription newSort;
DataGridSortDescription sort = OwningColumn.GetSortDescription(); KeyboardHelper.GetMetaKeyState(keyModifiers, out bool ctrl, out bool shift);
IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
Debug.Assert(collectionView != null); DataGridSortDescription sort = OwningColumn.GetSortDescription();
using (collectionView.DeferRefresh()) IDataGridCollectionView collectionView = owningGrid.DataConnection.CollectionView;
{ Debug.Assert(collectionView != null);
// if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
{
owningGrid.DataConnection.SortDescriptions.Clear();
}
// if ctrl is held down, we only clear the sort directions using (collectionView.DeferRefresh())
if (!ctrl)
{ {
if (sort != null) // if shift is held down, we multi-sort, therefore if it isn't, we'll clear the sorts beforehand
if (!shift || owningGrid.DataConnection.SortDescriptions.Count == 0)
{ {
newSort = sort.SwitchSortDirection(); owningGrid.DataConnection.SortDescriptions.Clear();
}
// changing direction should not affect sort order, so we replace this column's // if ctrl is held down, we only clear the sort directions
// sort description instead of just adding it to the end of the collection if (!ctrl)
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort); {
if (oldIndex >= 0) if (sort != null)
{ {
owningGrid.DataConnection.SortDescriptions.Remove(sort); newSort = sort.SwitchSortDirection();
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
// changing direction should not affect sort order, so we replace this column's
// sort description instead of just adding it to the end of the collection
int oldIndex = owningGrid.DataConnection.SortDescriptions.IndexOf(sort);
if (oldIndex >= 0)
{
owningGrid.DataConnection.SortDescriptions.Remove(sort);
owningGrid.DataConnection.SortDescriptions.Insert(oldIndex, newSort);
}
else
{
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
else else
{ {
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort); owningGrid.DataConnection.SortDescriptions.Add(newSort);
} }
} }
else
{
string propertyName = OwningColumn.GetSortPropertyName();
// no-opt if we couldn't find a property to sort on
if (string.IsNullOrEmpty(propertyName))
{
return;
}
newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
} }
} }
} }

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

@ -33,6 +33,11 @@ namespace Avalonia.Controls
ColumnReordering?.Invoke(this, e); ColumnReordering?.Invoke(this, e);
} }
protected internal virtual void OnColumnSorting(DataGridColumnEventArgs e)
{
Sorting?.Invoke(this, e);
}
/// <summary> /// <summary>
/// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total
/// width is adjusted by the given amount, if possible. If the total desired adjustment amount /// width is adjusted by the given amount, if possible. If the total desired adjustment amount

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

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -20,6 +21,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row. /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
/// </summary> /// </summary>
[PseudoClasses(":selected", ":editing", ":invalid")]
public class DataGridRow : TemplatedControl public class DataGridRow : TemplatedControl
{ {

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

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
@ -13,6 +14,7 @@ using System.Reactive.Linq;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
[PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl public class DataGridRowGroupHeader : TemplatedControl
{ {
private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton"; private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton";

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

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
using Avalonia.Controls.Metadata;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media; using Avalonia.Media;
using System.Diagnostics; using System.Diagnostics;
@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header. /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header.
/// </summary> /// </summary>
[PseudoClasses(":invalid", ":selected", ":editing", ":current")]
public class DataGridRowHeader : ContentControl public class DataGridRowHeader : ContentControl
{ {
private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; private const string DATAGRIDROWHEADER_elementRootName = "PART_Root";

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

@ -289,7 +289,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events. /// Provides data for <see cref="T:Avalonia.Controls.DataGrid" /> column-related events.
/// </summary> /// </summary>
public class DataGridColumnEventArgs : EventArgs public class DataGridColumnEventArgs : HandledEventArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class. /// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumnEventArgs" /> class.
@ -566,4 +566,4 @@ namespace Avalonia.Controls
private set; private set;
} }
} }
} }

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Tree
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.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 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 void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not 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 '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 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. 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 Total Issues: 18

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

@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;
/// <summary>
/// Gets the arguments passed to the AppBuilder Start method.
/// </summary>
public string[] Args { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public ShutdownMode ShutdownMode { get; set; } public ShutdownMode ShutdownMode { get; set; }
@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown(); Shutdown();
} }
public void Shutdown(int exitCode = 0) public void Shutdown(int exitCode = 0)
{ {
@ -123,7 +125,11 @@ namespace Avalonia
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase<T>, new() where T : AppBuilderBase<T>, new()
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; var lifetime = new ClassicDesktopStyleApplicationLifetime()
{
Args = args,
ShutdownMode = shutdownMode
};
builder.SetupWithLifetime(lifetime); builder.SetupWithLifetime(lifetime);
return lifetime.Start(args); return lifetime.Start(args);
} }

7
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary> /// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{ {
/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
/// method.
/// </summary>
string[] Args { get; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly. /// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly.
/// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called. /// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called.

96
src/Avalonia.Controls/AutoCompleteBox.cs

@ -14,6 +14,7 @@ using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
@ -30,6 +31,7 @@ namespace Avalonia.Controls
/// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" /> /// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
/// event. /// event.
/// </summary> /// </summary>
[PseudoClasses(":dropdownopen")]
public class PopulatedEventArgs : EventArgs public class PopulatedEventArgs : EventArgs
{ {
/// <summary> /// <summary>
@ -225,6 +227,27 @@ namespace Avalonia.Controls
Custom = 13, Custom = 13,
} }
/// <summary>
/// Represents the selector used by the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control to
/// determine how the specified text should be modified with an item.
/// </summary>
/// <returns>
/// Modified text that will be used by the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" />.
/// </returns>
/// <param name="search">The string used as the basis for filtering.</param>
/// <param name="item">
/// The selected item that should be combined with the
/// <paramref name="search" /> parameter.
/// </param>
/// <typeparam name="T">
/// The type used for filtering the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" />.
/// This type can be either a string or an object.
/// </typeparam>
public delegate string AutoCompleteSelector<T>(string search, T item);
/// <summary> /// <summary>
/// Represents a control that provides a text box for user input and a /// Represents a control that provides a text box for user input and a
/// drop-down that contains possible matches based on the input in the text /// drop-down that contains possible matches based on the input in the text
@ -362,6 +385,9 @@ namespace Avalonia.Controls
private AutoCompleteFilterPredicate<object> _itemFilter; private AutoCompleteFilterPredicate<object> _itemFilter;
private AutoCompleteFilterPredicate<string> _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); private AutoCompleteFilterPredicate<string> _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
private AutoCompleteSelector<object> _itemSelector;
private AutoCompleteSelector<string> _textSelector;
public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent = public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); RoutedEvent.Register<SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
@ -528,6 +554,34 @@ namespace Avalonia.Controls
(o, v) => o.TextFilter = v, (o, v) => o.TextFilter = v,
unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
/// <summary>
/// Identifies the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemSelector" />
/// dependency property.
/// </summary>
/// <value>The identifier for the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemSelector" />
/// dependency property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<object>> ItemSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<object>>(
nameof(ItemSelector),
o => o.ItemSelector,
(o, v) => o.ItemSelector = v);
/// <summary>
/// Identifies the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextSelector" />
/// dependency property.
/// </summary>
/// <value>The identifier for the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.TextSelector" />
/// dependency property.</value>
public static readonly DirectProperty<AutoCompleteBox, AutoCompleteSelector<string>> TextSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, AutoCompleteSelector<string>>(
nameof(TextSelector),
o => o.TextSelector,
(o, v) => o.TextSelector = v);
/// <summary> /// <summary>
/// Identifies the /// Identifies the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" /> /// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
@ -1061,6 +1115,40 @@ namespace Avalonia.Controls
set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } set { SetAndRaise(TextFilterProperty, ref _textFilter, value); }
} }
/// <summary>
/// Gets or sets the custom method that combines the user-entered
/// text and one of the items specified by the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />.
/// </summary>
/// <value>
/// The custom method that combines the user-entered
/// text and one of the items specified by the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />.
/// </value>
public AutoCompleteSelector<object> ItemSelector
{
get { return _itemSelector; }
set { SetAndRaise(ItemSelectorProperty, ref _itemSelector, value); }
}
/// <summary>
/// Gets or sets the custom method that combines the user-entered
/// text and one of the items specified by the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
/// in a text-based way.
/// </summary>
/// <value>
/// The custom method that combines the user-entered
/// text and one of the items specified by the
/// <see cref="P:Avalonia.Controls.AutoCompleteBox.ItemsSource" />
/// in a text-based way.
/// </value>
public AutoCompleteSelector<string> TextSelector
{
get { return _textSelector; }
set { SetAndRaise(TextSelectorProperty, ref _textSelector, value); }
}
public Func<string, CancellationToken, Task<IEnumerable<object>>> AsyncPopulator public Func<string, CancellationToken, Task<IEnumerable<object>>> AsyncPopulator
{ {
get { return _asyncPopulator; } get { return _asyncPopulator; }
@ -2329,6 +2417,14 @@ namespace Avalonia.Controls
{ {
text = SearchText; text = SearchText;
} }
else if (TextSelector != null)
{
text = TextSelector(SearchText, FormatValue(newItem, true));
}
else if (ItemSelector != null)
{
text = ItemSelector(SearchText, newItem);
}
else else
{ {
text = FormatValue(newItem, true); text = FormatValue(newItem, true);

2
src/Avalonia.Controls/Button.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Controls.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -28,6 +29,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A button control. /// A button control.
/// </summary> /// </summary>
[PseudoClasses(":pressed")]
public class Button : ContentControl public class Button : ContentControl
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/ButtonSpinner.cs

@ -1,4 +1,5 @@
using System; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
@ -15,6 +16,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents a spinner control that includes two Buttons. /// Represents a spinner control that includes two Buttons.
/// </summary> /// </summary>
[PseudoClasses(":left", ":right")]
public class ButtonSpinner : Spinner public class ButtonSpinner : Spinner
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -3,6 +3,7 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved. // All other rights reserved.
using Avalonia.Controls.Metadata;
using Avalonia.Input; using Avalonia.Input;
using System; using System;
@ -12,6 +13,7 @@ namespace Avalonia.Controls.Primitives
/// Represents a button on a /// Represents a button on a
/// <see cref="T:Avalonia.Controls.Calendar" />. /// <see cref="T:Avalonia.Controls.Calendar" />.
/// </summary> /// </summary>
[PseudoClasses(":selected", ":inactive", ":btnfocused")]
public sealed class CalendarButton : Button public sealed class CalendarButton : Button
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -5,10 +5,12 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Controls.Metadata;
using Avalonia.Input; using Avalonia.Input;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
[PseudoClasses(":pressed", ":disabled", ":selected", ":inactive", ":today", ":blackout", ":dayfocused")]
public sealed class CalendarDayButton : Button public sealed class CalendarDayButton : Button
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using Avalonia.Controls.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -18,6 +19,7 @@ namespace Avalonia.Controls.Primitives
/// Represents the currently displayed month or year on a /// Represents the currently displayed month or year on a
/// <see cref="T:Avalonia.Controls.Calendar" />. /// <see cref="T:Avalonia.Controls.Calendar" />.
/// </summary> /// </summary>
[PseudoClasses(":calendardisabled")]
public sealed class CalendarItem : TemplatedControl public sealed class CalendarItem : TemplatedControl
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
#nullable enable #nullable enable
@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome
/// <summary> /// <summary>
/// Draws window minimize / maximize / close buttons in a <see cref="TitleBar"/> when managed client decorations are enabled. /// Draws window minimize / maximize / close buttons in a <see cref="TitleBar"/> when managed client decorations are enabled.
/// </summary> /// </summary>
[PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
public class CaptionButtons : TemplatedControl public class CaptionButtons : TemplatedControl
{ {
private CompositeDisposable? _disposables; private CompositeDisposable? _disposables;

2
src/Avalonia.Controls/Chrome/TitleBar.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
#nullable enable #nullable enable
@ -9,6 +10,7 @@ namespace Avalonia.Controls.Chrome
/// <summary> /// <summary>
/// Draws a titlebar when managed client decorations are enabled. /// Draws a titlebar when managed client decorations are enabled.
/// </summary> /// </summary>
[PseudoClasses(":minimized", ":normal", ":maximized", ":fullscreen")]
public class TitleBar : TemplatedControl public class TitleBar : TemplatedControl
{ {
private CompositeDisposable? _disposables; private CompositeDisposable? _disposables;

10
src/Avalonia.Controls/ColumnDefinitions.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Collections; using Avalonia.Collections;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -13,7 +14,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ColumnDefinitions"/> class. /// Initializes a new instance of the <see cref="ColumnDefinitions"/> class.
/// </summary> /// </summary>
public ColumnDefinitions() : base () public ColumnDefinitions()
{ {
} }
@ -27,6 +28,11 @@ namespace Avalonia.Controls
AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x))); AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
} }
public override string ToString()
{
return string.Join(",", this.Select(x => x.Width));
}
/// <summary> /// <summary>
/// Parses a string representation of column definitions collection. /// Parses a string representation of column definitions collection.
/// </summary> /// </summary>
@ -34,4 +40,4 @@ namespace Avalonia.Controls
/// <returns>The <see cref="ColumnDefinitions"/>.</returns> /// <returns>The <see cref="ColumnDefinitions"/>.</returns>
public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s); public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s);
} }
} }

2
src/Avalonia.Controls/DataValidationErrors.cs

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
@ -14,6 +15,7 @@ namespace Avalonia.Controls
/// <remarks> /// <remarks>
/// You will probably only want to create instances inside of control templates. /// You will probably only want to create instances inside of control templates.
/// </remarks> /// </remarks>
[PseudoClasses(":error")]
public class DataValidationErrors : ContentControl public class DataValidationErrors : ContentControl
{ {
/// <summary> /// <summary>

4
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -11,6 +12,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control to allow the user to select a date /// A control to allow the user to select a date
/// </summary> /// </summary>
[PseudoClasses(":hasnodate")]
public class DatePicker : TemplatedControl public class DatePicker : TemplatedControl
{ {
/// <summary> /// <summary>

4
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using System; using System;
@ -9,6 +10,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control to allow the user to select a time /// A control to allow the user to select a time
/// </summary> /// </summary>
[PseudoClasses(":hasnotime")]
public class TimePicker : TemplatedControl public class TimePicker : TemplatedControl
{ {
/// <summary> /// <summary>

3
src/Avalonia.Controls/Expander.cs

@ -1,6 +1,6 @@
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -12,6 +12,7 @@ namespace Avalonia.Controls
Right Right
} }
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl public class Expander : HeaderedContentControl
{ {
public static readonly StyledProperty<IPageTransition> ContentTransitionProperty = public static readonly StyledProperty<IPageTransition> ContentTransitionProperty =

9
src/Avalonia.Controls/IconElement.cs

@ -0,0 +1,9 @@
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
public abstract class IconElement : TemplatedControl
{
}
}

2
src/Avalonia.Controls/ItemsControl.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -18,6 +19,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Displays a collection of items. /// Displays a collection of items.
/// </summary> /// </summary>
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
{ {
/// <summary> /// <summary>

1
src/Avalonia.Controls/ListBox.cs

@ -163,6 +163,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer"); Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
} }
} }

2
src/Avalonia.Controls/ListBoxItem.cs

@ -1,3 +1,4 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Input; using Avalonia.Input;
@ -6,6 +7,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A selectable item in a <see cref="ListBox"/>. /// A selectable item in a <see cref="ListBox"/>.
/// </summary> /// </summary>
[PseudoClasses(":pressed", ":selected")]
public class ListBoxItem : ContentControl, ISelectable public class ListBoxItem : ContentControl, ISelectable
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/MenuItem.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -20,6 +21,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A menu item control. /// A menu item control.
/// </summary> /// </summary>
[PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
{ {
/// <summary> /// <summary>

4
src/Avalonia.Controls/Mixins/SelectableMixin.cs

@ -48,7 +48,7 @@ namespace Avalonia.Controls.Mixins
if (sender != null) if (sender != null)
{ {
((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); ((IPseudoClasses)sender.Classes).Set(":selected", x.NewValue.GetValueOrDefault());
sender.RaiseEvent(new RoutedEventArgs sender.RaiseEvent(new RoutedEventArgs
{ {
@ -58,4 +58,4 @@ namespace Avalonia.Controls.Mixins
}); });
} }
} }
} }

2
src/Avalonia.Controls/NativeMenu.Export.cs

@ -77,7 +77,7 @@ namespace Avalonia.Controls
{ {
if (args.Sender is TopLevel tl) if (args.Sender is TopLevel tl)
{ {
GetInfo(tl).Exporter?.SetNativeMenu((NativeMenu)args.NewValue); GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
} }
}); });
} }

2
src/Avalonia.Controls/NativeMenuItem.cs

@ -23,7 +23,7 @@ namespace Avalonia.Controls
MenuProperty.Changed.Subscribe(args => MenuProperty.Changed.Subscribe(args =>
{ {
var item = (NativeMenuItem)args.Sender; var item = (NativeMenuItem)args.Sender;
var value = (NativeMenu)args.NewValue; var value = args.NewValue.GetValueOrDefault();
if (value.Parent != null && value.Parent != item) if (value.Parent != null && value.Parent != item)
throw new InvalidOperationException("NativeMenu already has a parent"); throw new InvalidOperationException("NativeMenu already has a parent");
value.Parent = item; value.Parent = item;

2
src/Avalonia.Controls/Notifications/NotificationCard.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls.Metadata;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
@ -9,6 +10,7 @@ namespace Avalonia.Controls.Notifications
/// <summary> /// <summary>
/// Control that represents and displays a notification. /// Control that represents and displays a notification.
/// </summary> /// </summary>
[PseudoClasses(":error", ":information", ":success", ":warning")]
public class NotificationCard : ContentControl public class NotificationCard : ContentControl
{ {
private bool _isClosed; private bool _isClosed;

2
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -7,12 +7,14 @@ using Avalonia.Controls.Primitives;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Controls.Metadata;
namespace Avalonia.Controls.Notifications namespace Avalonia.Controls.Notifications
{ {
/// <summary> /// <summary>
/// An <see cref="INotificationManager"/> that displays notifications in a <see cref="Window"/>. /// An <see cref="INotificationManager"/> that displays notifications in a <see cref="Window"/>.
/// </summary> /// </summary>
[PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")]
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest
{ {
private IList _items; private IList _items;

21
src/Avalonia.Controls/PathIcon.cs

@ -0,0 +1,21 @@
using Avalonia.Media;
namespace Avalonia.Controls
{
public class PathIcon : IconElement
{
static PathIcon()
{
AffectsRender<PathIcon>(DataProperty);
}
public static readonly StyledProperty<Geometry> DataProperty =
AvaloniaProperty.Register<PathIcon, Geometry>(nameof(Data));
public Geometry Data
{
get { return GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
}
}

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

@ -82,7 +82,7 @@ namespace Avalonia.Controls.Presenters
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Constraint = constraint, Constraint = constraint,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize, FontSize = FontSize,
Text = text ?? string.Empty, Text = text ?? string.Empty,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Text = "X", Text = "X",
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize, FontSize = FontSize,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Constraint = availableSize, Constraint = availableSize,

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

@ -126,7 +126,7 @@ namespace Avalonia.Controls.Primitives
if (shapedTextCharacters.GlyphRun.Characters.End < textPosition) if (shapedTextCharacters.GlyphRun.Characters.End < textPosition)
{ {
currentX += shapedTextCharacters.GlyphRun.Bounds.Width; currentX += shapedTextCharacters.Size.Width;
continue; continue;
} }
@ -143,7 +143,7 @@ namespace Avalonia.Controls.Primitives
width = 0.0; width = 0.0;
} }
return new Rect(currentX, currentY, width, shapedTextCharacters.GlyphRun.Bounds.Height); return new Rect(currentX, currentY, width, shapedTextCharacters.Size.Height);
} }
} }

3
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
@ -13,7 +14,7 @@ namespace Avalonia.Controls.Primitives
/// (<see cref="PopupRoot"/>) or an <see cref="OverlayPopupHost"/> which is created /// (<see cref="PopupRoot"/>) or an <see cref="OverlayPopupHost"/> which is created
/// on an <see cref="OverlayLayer"/>. /// on an <see cref="OverlayLayer"/>.
/// </remarks> /// </remarks>
public interface IPopupHost : IDisposable public interface IPopupHost : IDisposable, IFocusScope
{ {
/// <summary> /// <summary>
/// Sets the control to display in the popup. /// Sets the control to display in the popup.

2
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -221,7 +221,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom)) if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
{ {
unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y); unconstrainedRect = unconstrainedRect.WithHeight(bounds.Bottom - unconstrainedRect.Y);
} }
if (IsValid(unconstrainedRect)) if (IsValid(unconstrainedRect))

4
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -4,6 +4,7 @@ using Avalonia.Interactivity;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Controls.Metadata;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -21,6 +22,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// A scrollbar control. /// A scrollbar control.
/// </summary> /// </summary>
[PseudoClasses(":vertical", ":horizontal")]
public class ScrollBar : RangeBase public class ScrollBar : RangeBase
{ {
/// <summary> /// <summary>
@ -141,7 +143,7 @@ namespace Avalonia.Controls.Primitives
_ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
}; };
SetValue(IsVisibleProperty, isVisible, BindingPriority.Style); SetValue(IsVisibleProperty, isVisible);
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)

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

@ -6,7 +6,6 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="SelectedItems"/> property. /// Defines the <see cref="SelectedItems"/> property.
/// </summary> /// </summary>
protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty = protected static readonly DirectProperty<SelectingItemsControl, IList?> SelectedItemsProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>( AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList?>(
nameof(SelectedItems), nameof(SelectedItems),
o => o.SelectedItems, o => o.SelectedItems,
(o, v) => o.SelectedItems = v); (o, v) => o.SelectedItems = v);
@ -111,12 +110,13 @@ namespace Avalonia.Controls.Primitives
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
private static readonly IList Empty = Array.Empty<object>(); private static readonly IList Empty = Array.Empty<object>();
private SelectedItemsSync? _selectedItemsSync;
private ISelectionModel? _selection; private ISelectionModel? _selection;
private int _oldSelectedIndex; private int _oldSelectedIndex;
private object? _oldSelectedItem; private object? _oldSelectedItem;
private int _initializing; private IList? _oldSelectedItems;
private bool _ignoreContainerSelectionChanged; private bool _ignoreContainerSelectionChanged;
private UpdateState? _updateState;
private bool _hasScrolledToSelectedItem;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -149,8 +149,27 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public int SelectedIndex public int SelectedIndex
{ {
get => Selection.SelectedIndex; get
set => Selection.SelectedIndex = value; {
// When a Begin/EndInit/DataContext update is in place we return the value to be
// updated here, even though it's not yet active and the property changed notification
// has not yet been raised. If we don't do this then the old value will be written back
// to the source when two-way bound, and the update value will be lost.
return _updateState?.SelectedIndex.HasValue == true ?
_updateState.SelectedIndex.Value :
Selection.SelectedIndex;
}
set
{
if (_updateState is object)
{
_updateState.SelectedIndex = value;
}
else
{
Selection.SelectedIndex = value;
}
}
} }
/// <summary> /// <summary>
@ -158,17 +177,67 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public object? SelectedItem public object? SelectedItem
{ {
get => Selection.SelectedItem; get
set => Selection.SelectedItem = value; {
// See SelectedIndex setter for more information.
return _updateState?.SelectedItem.HasValue == true ?
_updateState.SelectedItem.Value :
Selection.SelectedItem;
}
set
{
if (_updateState is object)
{
_updateState.SelectedItem = value;
}
else
{
Selection.SelectedItem = value;
}
}
} }
/// <summary> /// <summary>
/// Gets or sets the selected items. /// Gets or sets the selected items.
/// </summary> /// </summary>
protected IList SelectedItems /// <remarks>
/// By default returns a collection that can be modified in order to manipulate the control
/// selection, however this property will return null if <see cref="Selection"/> is
/// re-assigned; you should only use _either_ Selection or SelectedItems.
/// </remarks>
protected IList? SelectedItems
{ {
get => SelectedItemsSync.SelectedItems; get
set => SelectedItemsSync.SelectedItems = value; {
// See SelectedIndex setter for more information.
if (_updateState?.SelectedItems.HasValue == true)
{
return _updateState.SelectedItems.Value;
}
else if (Selection is InternalSelectionModel ism)
{
var result = ism.WritableSelectedItems;
_oldSelectedItems = result;
return result;
}
return null;
}
set
{
if (_updateState is object)
{
_updateState.SelectedItems = new Optional<IList?>(value);
}
else if (Selection is InternalSelectionModel i)
{
i.WritableSelectedItems = value;
}
else
{
throw new InvalidOperationException("Cannot set both Selection and SelectedItems.");
}
}
} }
/// <summary> /// <summary>
@ -178,19 +247,30 @@ namespace Avalonia.Controls.Primitives
{ {
get get
{ {
if (_selection is null) if (_updateState?.Selection.HasValue == true)
{ {
_selection = CreateDefaultSelectionModel(); return _updateState.Selection.Value;
InitializeSelectionModel(_selection);
} }
else
{
if (_selection is null)
{
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection; return _selection;
}
} }
set set
{ {
value ??= CreateDefaultSelectionModel(); value ??= CreateDefaultSelectionModel();
if (_selection != value) if (_updateState is object)
{
_updateState.Selection = new Optional<ISelectionModel>(value);
}
else if (_selection != value)
{ {
if (value.Source != null && value.Source != Items) if (value.Source != null && value.Source != Items)
{ {
@ -212,6 +292,15 @@ namespace Avalonia.Controls.Primitives
} }
InitializeSelectionModel(_selection); InitializeSelectionModel(_selection);
if (_oldSelectedItems != SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems;
}
} }
} }
} }
@ -234,20 +323,18 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
/// <inheritdoc/> /// <inheritdoc/>
public override void BeginInit() public override void BeginInit()
{ {
base.BeginInit(); base.BeginInit();
++_initializing; BeginUpdating();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void EndInit() public override void EndInit()
{ {
base.EndInit(); base.EndInit();
--_initializing; EndUpdating();
} }
/// <summary> /// <summary>
@ -295,6 +382,28 @@ namespace Avalonia.Controls.Primitives
} }
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AutoScrollToSelectedItemIfNecessary();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e)
{
LayoutUpdated -= ExecuteScrollWhenLayoutUpdated;
AutoScrollToSelectedItemIfNecessary();
}
if (AutoScrollToSelectedItem)
{
LayoutUpdated += ExecuteScrollWhenLayoutUpdated;
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e) protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{ {
@ -351,30 +460,14 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate() protected override void OnDataContextBeginUpdate()
{ {
base.OnDataContextBeginUpdate(); base.OnDataContextBeginUpdate();
++_initializing; BeginUpdating();
if (_selection is object)
{
_selection.Source = null;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnDataContextEndUpdate() protected override void OnDataContextEndUpdate()
{ {
base.OnDataContextEndUpdate(); base.OnDataContextEndUpdate();
--_initializing; EndUpdating();
if (_selection is object && _initializing == 0)
{
_selection.Source = Items;
if (Items is null)
{
_selection.Clear();
_selectedItemsSync?.SelectedItems?.Clear();
}
}
} }
protected override void OnInitialized() protected override void OnInitialized()
@ -398,8 +491,7 @@ namespace Avalonia.Controls.Primitives
if (ItemCount > 0 && if (ItemCount > 0 &&
Match(keymap.SelectAll) && Match(keymap.SelectAll) &&
(((SelectionMode & SelectionMode.Multiple) != 0) || SelectionMode.HasFlag(SelectionMode.Multiple))
(SelectionMode & SelectionMode.Toggle) != 0))
{ {
Selection.SelectAll(); Selection.SelectAll();
e.Handled = true; e.Handled = true;
@ -411,9 +503,11 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ItemsProperty && if (change.Property == AutoScrollToSelectedItemProperty)
_initializing == 0 && {
_selection is object) AutoScrollToSelectedItemIfNecessary();
}
if (change.Property == ItemsProperty && _updateState is null && _selection is object)
{ {
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>(); var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
_selection.Source = newValue; _selection.Source = newValue;
@ -601,23 +695,30 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{ {
if (Selection.AnchorIndex > 0) _hasScrolledToSelectedItem = false;
{ AutoScrollToSelectedItemIfNecessary();
ScrollIntoView(Selection.AnchorIndex);
}
} }
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)
{ {
RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
_oldSelectedIndex = SelectedIndex; _oldSelectedIndex = SelectedIndex;
} }
else if (e.PropertyName == nameof(ISelectionModel.SelectedItem)) else if (e.PropertyName == nameof(ISelectionModel.SelectedItem) && _oldSelectedItem != SelectedItem)
{ {
RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
_oldSelectedItem = SelectedItem; _oldSelectedItem = SelectedItem;
} }
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems;
}
} }
/// <summary> /// <summary>
@ -674,6 +775,19 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void AutoScrollToSelectedItemIfNecessary()
{
if (AutoScrollToSelectedItem &&
!_hasScrolledToSelectedItem &&
Presenter is object &&
Selection.AnchorIndex >= 0 &&
((IVisual)this).IsAttachedToVisualTree)
{
ScrollIntoView(Selection.AnchorIndex);
_hasScrolledToSelectedItem = true;
}
}
/// <summary> /// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>. /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary> /// </summary>
@ -734,14 +848,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void MarkContainersUnselected()
{
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(container.ContainerControl, false);
}
}
/// <summary> /// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary> /// </summary>
@ -757,23 +863,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <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)
{
var index = IndexOf(Items, item);
if (index != -1)
{
MarkItemSelected(index, selected);
}
return index;
}
private void UpdateContainerSelection() private void UpdateContainerSelection()
{ {
if (Presenter?.Panel is IPanel panel) if (Presenter?.Panel is IPanel panel)
@ -789,7 +878,7 @@ namespace Avalonia.Controls.Primitives
private ISelectionModel CreateDefaultSelectionModel() private ISelectionModel CreateDefaultSelectionModel()
{ {
return new SelectionModel<object> return new InternalSelectionModel
{ {
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
}; };
@ -797,7 +886,7 @@ namespace Avalonia.Controls.Primitives
private void InitializeSelectionModel(ISelectionModel model) private void InitializeSelectionModel(ISelectionModel model)
{ {
if (_initializing == 0) if (_updateState is null)
{ {
model.Source = Items; model.Source = Items;
} }
@ -825,9 +914,6 @@ namespace Avalonia.Controls.Primitives
UpdateContainerSelection(); UpdateContainerSelection();
_selectedItemsSync ??= new SelectedItemsSync(model);
_selectedItemsSync.SelectionModel = model;
if (SelectedIndex != -1) if (SelectedIndex != -1)
{ {
RaiseEvent(new SelectionChangedEventArgs( RaiseEvent(new SelectionChangedEventArgs(
@ -845,5 +931,96 @@ namespace Avalonia.Controls.Primitives
model.SelectionChanged -= OnSelectionModelSelectionChanged; model.SelectionChanged -= OnSelectionModelSelectionChanged;
} }
} }
private void BeginUpdating()
{
_updateState ??= new UpdateState();
_updateState.UpdateCount++;
}
private void EndUpdating()
{
if (_updateState is object && --_updateState.UpdateCount == 0)
{
var state = _updateState;
_updateState = null;
if (state.Selection.HasValue)
{
Selection = state.Selection.Value;
}
if (state.SelectedItems.HasValue)
{
SelectedItems = state.SelectedItems.Value;
}
Selection.Source = Items;
if (Items is null)
{
Selection.Clear();
}
if (state.SelectedIndex.HasValue)
{
SelectedIndex = state.SelectedIndex.Value;
}
else if (state.SelectedItem.HasValue)
{
SelectedItem = state.SelectedItem.Value;
}
}
}
// When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
// defer changes to the selection model because we have no idea in which order properties
// will be set. Consider:
//
// - Both Items and SelectedItem are bound
// - The DataContext changes
// - The binding for SelectedItem updates first, producing an item
// - Items is searched to find the index of the new selected item
// - However Items isn't yet updated; the item is not found
// - SelectedIndex is incorrectly set to -1
//
// This logic cannot be encapsulated in SelectionModel because the selection model can also
// be bound, consider:
//
// - Both Items and Selection are bound
// - The DataContext changes
// - The binding for Items updates first
// - The new items are assigned to Selection.Source
// - The binding for Selection updates, producing a new SelectionModel
// - Both the old and new SelectionModels have the incorrect Source
private class UpdateState
{
private Optional<int> _selectedIndex;
private Optional<object?> _selectedItem;
public int UpdateCount { get; set; }
public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
_selectedItem = default;
}
}
public Optional<object?> SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
_selectedIndex = default;
}
}
}
} }
} }

2
src/Avalonia.Controls/Primitives/Thumb.cs

@ -1,9 +1,11 @@
using System; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
[PseudoClasses(":pressed")]
public class Thumb : TemplatedControl public class Thumb : TemplatedControl
{ {
public static readonly RoutedEvent<VectorEventArgs> DragStartedEvent = public static readonly RoutedEvent<VectorEventArgs> DragStartedEvent =

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

@ -1,4 +1,5 @@
using System; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -7,6 +8,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states.
/// </summary> /// </summary>
[PseudoClasses(":checked", ":unchecked", ":indeterminate")]
public class ToggleButton : Button public class ToggleButton : Button
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Primitives/Track.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
@ -12,6 +13,7 @@ using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
[PseudoClasses(":vertical", ":horizontal")]
public class Track : Control public class Track : Control
{ {
public static readonly DirectProperty<Track, double> MinimumProperty = public static readonly DirectProperty<Track, double> MinimumProperty =

2
src/Avalonia.Controls/ProgressBar.cs

@ -1,4 +1,5 @@
using System; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
@ -8,6 +9,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control used to indicate the progress of an operation. /// A control used to indicate the progress of an operation.
/// </summary> /// </summary>
[PseudoClasses(":vertical", ":horizontal", ":indeterminate")]
public class ProgressBar : RangeBase public class ProgressBar : RangeBase
{ {
public class ProgressBarTemplateProperties : AvaloniaObject public class ProgressBarTemplateProperties : AvaloniaObject

24
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -7,10 +7,10 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -681,8 +681,15 @@ namespace Avalonia.Controls
if (oldValue != null) if (oldValue != null)
{ {
oldValue.UninitializeForContext(LayoutContext); oldValue.UninitializeForContext(LayoutContext);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
// Walk through all the elements and make sure they are cleared // Walk through all the elements and make sure they are cleared
foreach (var element in Children) foreach (var element in Children)
@ -699,8 +706,15 @@ namespace Avalonia.Controls
if (newValue != null) if (newValue != null)
{ {
newValue.InitializeForContext(LayoutContext); newValue.InitializeForContext(LayoutContext);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout; WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
} }
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;

36
src/Avalonia.Controls/ScrollViewer.cs

@ -448,6 +448,38 @@ namespace Avalonia.Controls
Offset += new Vector(_smallChange.Width, 0); Offset += new Vector(_smallChange.Width, 0);
} }
/// <summary>
/// Scrolls the content upward by one page.
/// </summary>
public void PageUp()
{
VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0);
}
/// <summary>
/// Scrolls the content downward by one page.
/// </summary>
public void PageDown()
{
VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum);
}
/// <summary>
/// Scrolls the content left by one page.
/// </summary>
public void PageLeft()
{
HorizontalScrollBarValue = Math.Max(_offset.X - _viewport.Width, 0);
}
/// <summary>
/// Scrolls the content tight by one page.
/// </summary>
public void PageRight()
{
HorizontalScrollBarValue = Math.Min(_offset.X + _viewport.Width, HorizontalScrollBarMaximum);
}
/// <summary> /// <summary>
/// Scrolls to the top-left corner of the content. /// Scrolls to the top-left corner of the content.
/// </summary> /// </summary>
@ -623,12 +655,12 @@ namespace Avalonia.Controls
{ {
if (e.Key == Key.PageUp) if (e.Key == Key.PageUp)
{ {
VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0); PageUp();
e.Handled = true; e.Handled = true;
} }
else if (e.Key == Key.PageDown) else if (e.Key == Key.PageDown)
{ {
VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum); PageDown();
e.Handled = true; e.Handled = true;
} }
} }

278
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -0,0 +1,278 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class InternalSelectionModel : SelectionModel<object?>
{
private IList? _writableSelectedItems;
private bool _ignoreModelChanges;
private bool _ignoreSelectedItemsChanges;
public InternalSelectionModel()
{
SelectionChanged += OnSelectionChanged;
SourceReset += OnSourceReset;
}
[AllowNull]
public IList WritableSelectedItems
{
get
{
if (_writableSelectedItems is null)
{
_writableSelectedItems = new AvaloniaList<object?>();
SubscribeToSelectedItems();
}
return _writableSelectedItems;
}
set
{
value ??= new AvaloniaList<object?>();
if (value.IsFixedSize)
{
throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems.");
}
if (_writableSelectedItems != value)
{
UnsubscribeFromSelectedItems();
_writableSelectedItems = value;
SyncFromSelectedItems();
SubscribeToSelectedItems();
if (ItemsView is null)
{
SetInitSelectedItems(value);
}
RaisePropertyChanged(nameof(WritableSelectedItems));
}
}
}
private protected override void SetSource(IEnumerable? value)
{
if (Source == value)
{
return;
}
object?[]? oldSelection = null;
if (Source is object && value is object)
{
oldSelection = new object?[WritableSelectedItems.Count];
WritableSelectedItems.CopyTo(oldSelection, 0);
}
try
{
_ignoreSelectedItemsChanges = true;
base.SetSource(value);
}
finally
{
_ignoreSelectedItemsChanges = false;
}
if (oldSelection is null)
{
SyncToSelectedItems();
}
else
{
foreach (var i in oldSelection)
{
var index = ItemsView!.IndexOf(i);
Select(index);
}
}
}
private void SyncToSelectedItems()
{
if (_writableSelectedItems is object)
{
try
{
_ignoreSelectedItemsChanges = true;
_writableSelectedItems.Clear();
foreach (var i in base.SelectedItems)
{
_writableSelectedItems.Add(i);
}
}
finally
{
_ignoreSelectedItemsChanges = false;
}
}
}
private void SyncFromSelectedItems()
{
if (Source is null || _writableSelectedItems is null)
{
return;
}
try
{
_ignoreModelChanges = true;
using (BatchUpdate())
{
Clear();
Add(_writableSelectedItems);
}
}
finally
{
_ignoreModelChanges = false;
}
}
private void SubscribeToSelectedItems()
{
if (_writableSelectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnSelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromSelectedItems()
{
if (_writableSelectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnSelectedItemsCollectionChanged;
}
}
private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_ignoreModelChanges)
{
return;
}
try
{
var items = WritableSelectedItems;
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_ignoreSelectedItemsChanges = true;
foreach (var i in deselected)
{
items.Remove(i);
}
foreach (var i in selected)
{
items.Add(i);
}
}
finally
{
_ignoreSelectedItemsChanges = false;
}
}
private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems();
private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_ignoreSelectedItemsChanges)
{
return;
}
if (_writableSelectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Source, i);
if (index != -1)
{
Deselect(index);
}
}
}
try
{
using var operation = BatchUpdate();
_ignoreModelChanges = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Clear();
Add(_writableSelectedItems);
break;
}
}
finally
{
_ignoreModelChanges = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(Source, i);
if (index != -1)
{
Select(index);
}
}
}
private static int IndexOf(object? source, object? item)
{
if (source is IList l)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

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

@ -20,8 +20,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>? _selectedItems; private SelectedItems<T>? _selectedItems;
private SelectedItems<T>.Untyped? _selectedItemsUntyped; private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged; private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
[AllowNull] private T _initSelectedItem = default; private IList? _initSelectedItems;
private bool _hasInitSelectedItem;
public SelectionModel() public SelectionModel()
{ {
@ -82,7 +81,19 @@ namespace Avalonia.Controls.Selection
[MaybeNull, AllowNull] [MaybeNull, AllowNull]
public T SelectedItem public T SelectedItem
{ {
get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; get
{
if (ItemsView is object)
{
return GetItemAt(_selectedIndex);
}
else if (_initSelectedItems is object && _initSelectedItems.Count > 0)
{
return (T)_initSelectedItems[0];
}
return default;
}
set set
{ {
if (ItemsView is object) if (ItemsView is object)
@ -92,8 +103,9 @@ namespace Avalonia.Controls.Selection
else else
{ {
Clear(); Clear();
_initSelectedItem = value; #pragma warning disable CS8601
_hasInitSelectedItem = true; SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
} }
} }
} }
@ -102,9 +114,10 @@ namespace Avalonia.Controls.Selection
{ {
get get
{ {
if (ItemsView is null && _hasInitSelectedItem) if (ItemsView is null && _initSelectedItems is object)
{ {
return new[] { _initSelectedItem }; return _initSelectedItems is IReadOnlyList<T> i ?
i : _initSelectedItems.Cast<T>().ToList();
} }
return _selectedItems ??= new SelectedItems<T>(this); return _selectedItems ??= new SelectedItems<T>(this);
@ -229,12 +242,7 @@ namespace Avalonia.Controls.Selection
{ {
using var update = BatchUpdate(); using var update = BatchUpdate();
var o = update.Operation; var o = update.Operation;
var range = CoerceRange(start, end); var range = new IndexRange(Math.Max(0, start), end);
if (range.Begin == -1)
{
return;
}
if (RangesEnabled) if (RangesEnabled)
{ {
@ -258,8 +266,7 @@ namespace Avalonia.Controls.Selection
o.SelectedIndex = -1; o.SelectedIndex = -1;
} }
_initSelectedItem = default; _initSelectedItems = null;
_hasInitSelectedItem = false;
} }
public void SelectAll() => SelectRange(0, int.MaxValue); public void SelectAll() => SelectRange(0, int.MaxValue);
@ -270,7 +277,7 @@ namespace Avalonia.Controls.Selection
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void SetSource(IEnumerable? value) private protected virtual void SetSource(IEnumerable? value)
{ {
if (base.Source != value) if (base.Source != value)
{ {
@ -292,11 +299,14 @@ namespace Avalonia.Controls.Selection
{ {
update.Operation.IsSourceUpdate = true; update.Operation.IsSourceUpdate = true;
if (_hasInitSelectedItem) if (_initSelectedItems is object && ItemsView is object)
{ {
SelectedItem = _initSelectedItem; foreach (T i in _initSelectedItems)
_initSelectedItem = default; {
_hasInitSelectedItem = false; Select(ItemsView.IndexOf(i));
}
_initSelectedItems = null;
} }
else else
{ {
@ -345,7 +355,9 @@ namespace Avalonia.Controls.Selection
LostSelection(this, EventArgs.Empty); LostSelection(this, EventArgs.Empty);
} }
CommitOperation(update.Operation); // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that
// let to this method being called will raise them if necessary.
CommitOperation(update.Operation, raisePropertyChanged: false);
} }
private protected override CollectionChangeState OnItemsAdded(int index, IList items) private protected override CollectionChangeState OnItemsAdded(int index, IList items)
@ -430,6 +442,11 @@ namespace Avalonia.Controls.Selection
RaisePropertyChanged(nameof(SelectedIndex)); RaisePropertyChanged(nameof(SelectedIndex));
} }
if (e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex)
{
RaisePropertyChanged(nameof(SelectedItem));
}
if (oldAnchorIndex != _anchorIndex) if (oldAnchorIndex != _anchorIndex)
{ {
RaisePropertyChanged(nameof(AnchorIndex)); RaisePropertyChanged(nameof(AnchorIndex));
@ -459,6 +476,16 @@ namespace Avalonia.Controls.Selection
return true; return true;
} }
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
protected override void OnSourceCollectionChangeFinished() protected override void OnSourceCollectionChangeFinished()
{ {
if (_operation is object) if (_operation is object)
@ -532,8 +559,7 @@ namespace Avalonia.Controls.Selection
o.SelectedIndex = o.AnchorIndex = start; o.SelectedIndex = o.AnchorIndex = start;
} }
_initSelectedItem = default; _initSelectedItems = null;
_hasInitSelectedItem = false;
} }
[return: MaybeNull] [return: MaybeNull]
@ -611,7 +637,7 @@ namespace Avalonia.Controls.Selection
} }
} }
private void CommitOperation(Operation operation) private void CommitOperation(Operation operation, bool raisePropertyChanged = true)
{ {
try try
{ {
@ -679,23 +705,34 @@ namespace Avalonia.Controls.Selection
} }
} }
if (oldSelectedIndex != _selectedIndex) if (raisePropertyChanged)
{ {
indexesChanged = true; if (oldSelectedIndex != _selectedIndex)
RaisePropertyChanged(nameof(SelectedIndex)); {
RaisePropertyChanged(nameof(SelectedItem)); indexesChanged = true;
} RaisePropertyChanged(nameof(SelectedIndex));
}
if (oldAnchorIndex != _anchorIndex) if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate)
{ {
indexesChanged = true; RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(AnchorIndex)); }
}
if (indexesChanged) if (oldAnchorIndex != _anchorIndex)
{ {
RaisePropertyChanged(nameof(SelectedIndexes)); indexesChanged = true;
RaisePropertyChanged(nameof(SelectedItems)); RaisePropertyChanged(nameof(AnchorIndex));
}
if (indexesChanged)
{
RaisePropertyChanged(nameof(SelectedIndexes));
}
if (indexesChanged || operation.IsSourceUpdate)
{
RaisePropertyChanged(nameof(SelectedItems));
}
} }
} }
finally finally

2
src/Avalonia.Controls/Slider.cs

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
@ -39,6 +40,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that lets the user select from a range of values by moving a Thumb control along a Track. /// A control that lets the user select from a range of values by moving a Thumb control along a Track.
/// </summary> /// </summary>
[PseudoClasses(":vertical", ":horizontal", ":pressed")]
public class Slider : RangeBase public class Slider : RangeBase
{ {
/// <summary> /// <summary>

7
src/Avalonia.Controls/SplitView.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -73,6 +74,10 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control with two views: A collapsible pane and an area for content /// A control with two views: A collapsible pane and an area for content
/// </summary> /// </summary>
[PseudoClasses(":open", ":closed")]
[PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")]
[PseudoClasses(":left", ":right")]
[PseudoClasses(":lightdismiss")]
public class SplitView : TemplatedControl public class SplitView : TemplatedControl
{ {
/* /*

55
src/Avalonia.Controls/TabControl.cs

@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -66,7 +67,7 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty); AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e)); SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
} }
/// <summary> /// <summary>
@ -145,55 +146,27 @@ namespace Avalonia.Controls
protected override void OnContainersMaterialized(ItemContainerEventArgs e) protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{ {
base.OnContainersMaterialized(e); base.OnContainersMaterialized(e);
UpdateSelectedContent();
if (SelectedContent != null || SelectedIndex == -1)
{
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
} }
private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) protected override void OnContainersRecycled(ItemContainerEventArgs e)
{ {
var index = (int)e.NewValue; base.OnContainersRecycled(e);
UpdateSelectedContent();
if (index == -1)
{
SelectedContentTemplate = null;
SelectedContent = null;
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
} }
private void UpdateSelectedContent(IContentControl item) private void UpdateSelectedContent()
{ {
if (SelectedContentTemplate != item.ContentTemplate) if (SelectedIndex == -1)
{ {
SelectedContentTemplate = item.ContentTemplate; SelectedContent = SelectedContentTemplate = null;
} }
else
if (SelectedContent != item.Content)
{ {
SelectedContent = item.Content; var container = SelectedItem as IContentControl ??
ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl;
SelectedContentTemplate = container?.ContentTemplate;
SelectedContent = container?.Content;
} }
} }

2
src/Avalonia.Controls/TabItem.cs

@ -1,3 +1,4 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -6,6 +7,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>. /// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>.
/// </summary> /// </summary>
[PseudoClasses(":pressed", ":selected")]
public class TabItem : HeaderedContentControl, ISelectable public class TabItem : HeaderedContentControl, ISelectable
{ {
/// <summary> /// <summary>

9
src/Avalonia.Controls/TextBlock.cs

@ -138,7 +138,7 @@ namespace Avalonia.Controls
FontStyleProperty, TextWrappingProperty, FontFamilyProperty, FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty); TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
Observable.Merge(TextProperty.Changed, ForegroundProperty.Changed, Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextTrimmingProperty.Changed, FontSizeProperty.Changed, TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed, FontStyleProperty.Changed, FontWeightProperty.Changed,
@ -434,7 +434,10 @@ namespace Avalonia.Controls
var padding = Padding; var padding = Padding;
TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top)); using (context.PushPostTransform(Matrix.CreateTranslation(padding.Left + offsetX, padding.Top)))
{
TextLayout.Draw(context);
}
} }
/// <summary> /// <summary>
@ -452,7 +455,7 @@ namespace Avalonia.Controls
return new TextLayout( return new TextLayout(
text ?? string.Empty, text ?? string.Empty,
FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), new Typeface(FontFamily, FontStyle, FontWeight),
FontSize, FontSize,
Foreground, Foreground,
TextAlignment, TextAlignment,

2
src/Avalonia.Controls/TextBox.cs

@ -13,9 +13,11 @@ using Avalonia.Metadata;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Controls.Metadata;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
[PseudoClasses(":empty")]
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{ {
public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current

4
src/Avalonia.Controls/ToggleSwitch.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Presenters; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
@ -8,6 +9,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A Toggle Switch control. /// A Toggle Switch control.
/// </summary> /// </summary>
[PseudoClasses(":dragging")]
public class ToggleSwitch : ToggleButton public class ToggleSwitch : ToggleButton
{ {
private Panel _knobsPanel; private Panel _knobsPanel;

2
src/Avalonia.Controls/ToolTip.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -14,6 +15,7 @@ namespace Avalonia.Controls
/// To add a tooltip to a control, use the <see cref="TipProperty"/> attached property, /// To add a tooltip to a control, use the <see cref="TipProperty"/> attached property,
/// assigning the content that you want displayed. /// assigning the content that you want displayed.
/// </remarks> /// </remarks>
[PseudoClasses(":open")]
public class ToolTip : ContentControl public class ToolTip : ContentControl
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/TreeViewItem.cs

@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -11,6 +12,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// An item in a <see cref="TreeView"/>. /// An item in a <see cref="TreeView"/>.
/// </summary> /// </summary>
[PseudoClasses(":pressed", ":selected")]
public class TreeViewItem : HeaderedItemsControl, ISelectable public class TreeViewItem : HeaderedItemsControl, ISelectable
{ {
/// <summary> /// <summary>

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

@ -1,283 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
#nullable enable
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync : IDisposable
{
private ISelectionModel _selectionModel;
private IList _selectedItems;
private bool _updatingItems;
private bool _updatingModel;
public SelectedItemsSync(ISelectionModel model)
{
_selectionModel = model ?? throw new ArgumentNullException(nameof(model));
_selectedItems = new AvaloniaList<object?>();
SyncSelectedItemsWithSelectionModel();
SubscribeToSelectedItems(_selectedItems);
SubscribeToSelectionModel(model);
}
public ISelectionModel SelectionModel
{
get => _selectionModel;
set
{
if (_selectionModel != value)
{
value = value ?? throw new ArgumentNullException(nameof(value));
UnsubscribeFromSelectionModel(_selectionModel);
_selectionModel = value;
SubscribeToSelectionModel(_selectionModel);
SyncSelectedItemsWithSelectionModel();
}
}
}
public IList SelectedItems
{
get => _selectedItems;
set
{
value ??= new AvaloniaList<object?>();
if (_selectedItems != value)
{
if (value.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
UnsubscribeFromSelectedItems(_selectedItems);
_selectedItems = value;
SubscribeToSelectedItems(_selectedItems);
SyncSelectionModelWithSelectedItems();
}
}
}
public void Dispose()
{
UnsubscribeFromSelectedItems(_selectedItems);
UnsubscribeFromSelectionModel(_selectionModel);
}
private void SyncSelectedItemsWithSelectionModel()
{
_updatingItems = true;
try
{
_selectedItems.Clear();
if (_selectionModel.Source is object)
{
foreach (var i in _selectionModel.SelectedItems)
{
_selectedItems.Add(i);
}
}
}
finally
{
_updatingItems = false;
}
}
private void SyncSelectionModelWithSelectedItems()
{
_updatingModel = true;
try
{
if (_selectionModel.Source is object)
{
using (_selectionModel.BatchUpdate())
{
SelectionModel.Clear();
Add(_selectedItems);
}
}
}
finally
{
_updatingModel = false;
}
}
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_selectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
SelectionModel.Deselect(index);
}
}
}
try
{
using var operation = SelectionModel.BatchUpdate();
_updatingModel = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
SelectionModel.Clear();
Add(_selectedItems);
break;
}
}
finally
{
_updatingModel = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
SelectionModel.Select(index);
}
}
}
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ISelectionModel.Source))
{
if (_selectedItems.Count > 0)
{
SyncSelectionModelWithSelectedItems();
}
else
{
SyncSelectedItemsWithSelectionModel();
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel || _selectionModel.Source is null)
{
return;
}
try
{
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_updatingItems = true;
foreach (var i in deselected)
{
_selectedItems.Remove(i);
}
foreach (var i in selected)
{
_selectedItems.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
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)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

6
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -38,11 +38,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
{ {
if (listenUri.Scheme != "http") if (listenUri.Scheme != "http")
throw new ArgumentException("listenUri"); throw new ArgumentException("URI scheme is not HTTP.", nameof(listenUri));
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() _resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( .Where(r => r.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase)
&& r.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
.ToDictionary(
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
r => r =>
{ {

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

@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class MainViewModel : ViewModelBase, IDisposable internal class MainViewModel : ViewModelBase, IDisposable
{ {
private readonly IControl _root; private readonly TopLevel _root;
private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree; private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events; private readonly EventsPageViewModel _events;
@ -19,8 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
private string _focusedControl; private string _focusedControl;
private string _pointerOverElement; private string _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
public MainViewModel(IControl root) public MainViewModel(TopLevel root)
{ {
_root = root; _root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -40,12 +42,42 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding; get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
} }
public bool ShouldVisualizeDirtyRects
{
get => _shouldVisualizeDirtyRects;
set
{
_root.Renderer.DrawDirtyRects = value;
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
}
public void ToggleVisualizeDirtyRects()
{
ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
}
public void ToggleVisualizeMarginPadding() public void ToggleVisualizeMarginPadding()
{ {
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
} }
public bool ShowFpsOverlay
{
get => _showFpsOverlay;
set
{
_root.Renderer.DrawFps = value;
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
}
}
public void ToggleFpsOverlay()
{
ShowFpsOverlay = !ShowFpsOverlay;
}
public ConsoleViewModel Console { get; } public ConsoleViewModel Console { get; }
public ViewModelBase Content public ViewModelBase Content
@ -128,10 +160,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var tree = Content as TreePageViewModel; var tree = Content as TreePageViewModel;
if (tree != null) tree?.SelectControl(control);
{
tree.SelectControl(control);
}
} }
public void Dispose() public void Dispose()
@ -140,6 +169,8 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false;
_root.Renderer.DrawFps = false;
} }
private void UpdateFocusedControl() private void UpdateFocusedControl()

51
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -8,8 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels
internal abstract class PropertyViewModel : ViewModelBase internal abstract class PropertyViewModel : ViewModelBase
{ {
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = new[] { typeof(string) }; private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; } public abstract object Key { get; }
public abstract string Name { get; } public abstract string Name { get; }
@ -26,35 +26,46 @@ namespace Avalonia.Diagnostics.ViewModels
} }
var converter = TypeDescriptor.GetConverter(value); var converter = TypeDescriptor.GetConverter(value);
return converter?.ConvertToString(value) ?? value.ToString();
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
return value.ToString();
}
return converter.ConvertToString(value);
} }
protected static object ConvertFromString(string s, Type targetType) private static object InvokeParse(string s, Type targetType)
{ {
var converter = TypeDescriptor.GetConverter(targetType); var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
if (converter != null && converter.CanConvertFrom(typeof(string))) if (method != null)
{ {
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
} }
else
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
if (method != null)
{ {
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); return method.Invoke(null, new object[] { s });
}
if (method != null) throw new InvalidCastException("Unable to convert value.");
{ }
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); protected static object ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
if (method != null) if (converter.CanConvertFrom(typeof(string)))
{ {
return method.Invoke(null, new object[] { s }); return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
} }
throw new InvalidCastException("Unable to convert value."); return InvokeParse(s, targetType);
} }
} }
} }

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

Loading…
Cancel
Save