Browse Source

Merge branch 'master' into feature/ios-build

pull/4586/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
c793f29fd3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      Avalonia.sln
  2. 2
      NuGet.Config
  3. 12
      build/ApiDiff.props
  4. 2
      build/ReactiveUI.props
  5. 2
      build/SharedVersion.props
  6. 4
      build/SkiaSharp.props
  7. 16
      native/Avalonia.Native/src/OSX/Screens.mm
  8. 37
      native/Avalonia.Native/src/OSX/window.mm
  9. 4
      nukebuild/Build.cs
  10. 8
      readme.md
  11. 1
      samples/BindingDemo/MainWindow.xaml
  12. 17
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  13. 11
      samples/ControlCatalog/App.xaml
  14. 18
      samples/ControlCatalog/App.xaml.cs
  15. 4
      samples/ControlCatalog/MainView.xaml
  16. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  17. 8
      samples/ControlCatalog/Models/GDPValueConverter.cs
  18. 12
      samples/ControlCatalog/Models/Person.cs
  19. 4
      samples/ControlCatalog/Pages/ButtonPage.xaml
  20. 5
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  21. 5
      samples/ControlCatalog/Pages/DataGridPage.xaml
  22. 9
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  23. 2
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  24. 7
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  25. 68
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  26. 37
      samples/ControlCatalog/Pages/MenuPage.xaml
  27. 13
      samples/ControlCatalog/Pages/ScreenPage.cs
  28. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  29. 3
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  30. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  31. 66
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  32. 36
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  33. 34
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  34. 3
      samples/RenderDemo/MainWindow.xaml
  35. 92
      samples/RenderDemo/Pages/WriteableBitmapPage.cs
  36. 11
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  37. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  38. 3
      src/Avalonia.Animation/Avalonia.Animation.csproj
  39. 3
      src/Avalonia.Base/Avalonia.Base.csproj
  40. 4
      src/Avalonia.Base/AvaloniaObject.cs
  41. 6
      src/Avalonia.Base/AvaloniaProperty.cs
  42. 41
      src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs
  43. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  44. 230
      src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
  45. 25
      src/Avalonia.Base/DirectProperty.cs
  46. 39
      src/Avalonia.Base/DirectPropertyBase.cs
  47. 2
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  48. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  49. 61
      src/Avalonia.Base/Threading/AvaloniaScheduler.cs
  50. 2
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  51. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  52. 80
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  53. 13
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  54. 17
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  55. 2
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  56. 64
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  57. 22
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  58. 25
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  59. 2
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  60. 4
      src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs
  61. 23
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  62. 645
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  63. 18
      src/Avalonia.Controls/ApiCompatBaseline.txt
  64. 17
      src/Avalonia.Controls/AppBuilderBase.cs
  65. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  66. 6
      src/Avalonia.Controls/Avalonia.Controls.csproj
  67. 7
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  68. 6
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  69. 126
      src/Avalonia.Controls/Chrome/TitleBar.cs
  70. 22
      src/Avalonia.Controls/ComboBox.cs
  71. 11
      src/Avalonia.Controls/ContextMenu.cs
  72. 16
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  73. 40
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  74. 51
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  75. 38
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  76. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  77. 1
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  78. 4
      src/Avalonia.Controls/IMenuElement.cs
  79. 6
      src/Avalonia.Controls/IMenuItem.cs
  80. 249
      src/Avalonia.Controls/ISelectionModel.cs
  81. 200
      src/Avalonia.Controls/IndexPath.cs
  82. 32
      src/Avalonia.Controls/ItemsControl.cs
  83. 135
      src/Avalonia.Controls/ItemsSourceView.cs
  84. 10
      src/Avalonia.Controls/ListBox.cs
  85. 5
      src/Avalonia.Controls/Menu.cs
  86. 8
      src/Avalonia.Controls/MenuBase.cs
  87. 70
      src/Avalonia.Controls/MenuItem.cs
  88. 12
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  89. 6
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  90. 5
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  91. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  92. 22
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  93. 15
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  94. 7
      src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
  95. 61
      src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs
  96. 143
      src/Avalonia.Controls/Primitives/Popup.cs
  97. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  98. 4
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  99. 541
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  100. 53
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs

13
Avalonia.sln

@ -121,10 +121,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
build\AndroidWorkarounds.props = build\AndroidWorkarounds.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
build\BuildTargets.targets = build\BuildTargets.targets
build\CoreLibraries.props = build\CoreLibraries.props
build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\iOSWorkarounds.props = build\iOSWorkarounds.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@ -134,17 +137,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\NetCore.props = build\NetCore.props
build\NetFX.props = build\NetFX.props
build\ReactiveUI.props = build\ReactiveUI.props
build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props
build\Rx.props = build\Rx.props
build\SampleApp.props = build\SampleApp.props
build\SharedVersion.props = build\SharedVersion.props
build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props
build\SourceLink.props = build\SourceLink.props
build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\XUnit.props = build\XUnit.props
build\ApiDiff.props = build\ApiDiff.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
ProjectSection(SolutionItems) = preProject
build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
build\BuildTargets.targets = build\BuildTargets.targets
build\LegacyProject.targets = build\LegacyProject.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}"

2
NuGet.Config

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
</packageSources>
</configuration>

12
build/ApiDiff.props

@ -0,0 +1,12 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApiContractPackageVersion>0.10.0-preview3</ApiContractPackageVersion>
<NugetPackageName Condition="'$(PackageId)' != ''">$(PackageId)</NugetPackageName>
<NugetPackageName Condition="'$(PackageId)' == ''">Avalonia</NugetPackageName>
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="$(NugetPackageName)" Version="[$(ApiContractPackageVersion)]" />
<PackageReference Include="Microsoft.DotNet.ApiCompat" Version="5.0.0-beta.20372.2" PrivateAssets="All" />
<ResolvedMatchingContract Include="$(NuGetPackageRoot)\$(NugetPackageName.ToLower())\$(ApiContractPackageVersion)\lib\$(TargetFramework)\$(AssemblyName).dll" />
</ItemGroup>
</Project>

2
build/ReactiveUI.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="10.3.6" />
<PackageReference Include="ReactiveUI" Version="11.5.17" />
</ItemGroup>
</Project>

2
build/SharedVersion.props

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product>Avalonia</Product>
<Version>0.9.999</Version>
<Version>0.10.999</Version>
<Copyright>Copyright 2020 &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

4
build/SkiaSharp.props

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

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

@ -4,6 +4,14 @@ class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
{
public:
FORWARD_IUNKNOWN()
private:
CGFloat PrimaryDisplayHeight()
{
return NSMaxY([[[NSScreen screens] firstObject] frame]);
}
public:
virtual HRESULT GetScreenCount (int* ret) override
{
@autoreleasepool
@ -25,15 +33,15 @@ class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
auto screen = [[NSScreen screens] objectAtIndex:index];
ret->Bounds.X = [screen frame].origin.x;
ret->Bounds.Y = [screen frame].origin.y;
ret->Bounds.Height = [screen frame].size.height;
ret->Bounds.Width = [screen frame].size.width;
ret->Bounds.X = [screen frame].origin.x;
ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height;
ret->WorkingArea.X = [screen visibleFrame].origin.x;
ret->WorkingArea.Y = [screen visibleFrame].origin.y;
ret->WorkingArea.Height = [screen visibleFrame].size.height;
ret->WorkingArea.Width = [screen visibleFrame].size.width;
ret->WorkingArea.X = [screen visibleFrame].origin.x;
ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height;
ret->PixelDensity = [screen backingScaleFactor];

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

@ -27,6 +27,7 @@ public:
AvnPoint lastPositionSet;
NSString* _lastTitle;
IAvnMenu* _mainMenu;
bool _shown;
WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
@ -230,6 +231,29 @@ public:
{
@autoreleasepool
{
auto maxSize = [Window maxSize];
auto minSize = [Window minSize];
if (x < minSize.width)
{
x = minSize.width;
}
if (y < minSize.height)
{
y = minSize.height;
}
if (x > maxSize.width)
{
x = maxSize.width;
}
if (y > maxSize.height)
{
y = maxSize.height;
}
[Window setContentSize:NSSize{x, y}];
return S_OK;
@ -1291,10 +1315,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_parent->UpdateCursor();
auto fsize = [self convertSizeToBacking: [self frame].size];
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget];
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height)
{
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget];
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height});
}
}
- (void)updateLayer

4
nukebuild/Build.cs

@ -128,7 +128,9 @@ partial class Build : NukeBuild
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
NpmTasks.NpmInstall(c => c
.SetWorkingDirectory(webappDir)
.SetArgumentConfigurator(a => a.Add("--silent")));
NpmTasks.NpmRun(c => c
.SetWorkingDirectory(webappDir)
.SetCommand("dist"));

8
readme.md

@ -1,4 +1,4 @@
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
@ -16,7 +16,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 Getting Started
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://avaloniaui.net/docs/quickstart/create-new-project).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@ -47,7 +47,7 @@ We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using
## Documentation
Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers.
Documentation can be found on our website at https://avaloniaui.net/docs/. We also have a [tutorial](https://avaloniaui.net/docs/tutorial/) over there for newcomers.
## Building and Using
@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md).
## Contributors
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)].
This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers

1
samples/BindingDemo/MainWindow.xaml

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

17
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

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

11
samples/ControlCatalog/App.xaml

@ -1,17 +1,16 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
<Application.Styles>
<Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Medium" />
</Style>
<Style Selector="TextBlock.h2">
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="FontSize" Value="14" />
</Style>
<Style Selector="TextBlock.h3">
<Setter Property="FontSize" Value="{DynamicResource FontSizeSmall}"/>
<Setter Property="FontSize" Value="12" />
</Style>
<StyleInclude Source="/SideBar.xaml"/>
</Application.Styles>

18
samples/ControlCatalog/App.xaml.cs

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

4
samples/ControlCatalog/MainView.xaml

@ -1,9 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.MainView"
Foreground="{DynamicResource ThemeForegroundBrush}"
FontSize="{DynamicResource FontSizeNormal}">
x:Class="ControlCatalog.MainView">
<Grid>
<Grid.Styles>
<Style Selector="TextBlock.h2">

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -63,8 +63,18 @@ namespace ControlCatalog
// TODO: iOS does not support dynamically loading assemblies
// so we must refer to this resource DLL statically. For
// now I am doing that here. But we need a better solution!!
var theme = new Avalonia.Themes.Default.DefaultTheme();
theme.TryGetResource("Button", out _);
// Note, theme swiching probably will not work in runtime for iOS.
if (Application.Current.Styles.Contains(App.FluentDark)
|| Application.Current.Styles.Contains(App.FluentLight))
{
var theme = new Avalonia.Themes.Fluent.FluentTheme();
theme.TryGetResource("Button", out _);
}
else
{
var theme = new Avalonia.Themes.Default.DefaultTheme();
theme.TryGetResource("Button", out _);
}
AvaloniaXamlLoader.Load(this);
}
}

8
samples/ControlCatalog/Models/GDPValueConverter.cs

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

12
samples/ControlCatalog/Models/Person.cs

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

4
samples/ControlCatalog/Pages/ButtonPage.xaml

@ -20,6 +20,10 @@
<Style.Resources>
<SolidColorBrush x:Key="ThemeBorderMidBrush">Red</SolidColorBrush>
<SolidColorBrush x:Key="ThemeControlHighBrush">DarkRed</SolidColorBrush>
<SolidColorBrush x:Key="ButtonBorderBrush">Red</SolidColorBrush>
<SolidColorBrush x:Key="ButtonBackground">DarkRed</SolidColorBrush>
<SolidColorBrush x:Key="ButtonBackgroundPointerOver">Red</SolidColorBrush>
<SolidColorBrush x:Key="ButtonBackgroundPressed">OrangeRed</SolidColorBrush>
</Style.Resources>
</Style>
</Button.Styles>

5
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -14,13 +14,14 @@
Padding="48,48,48,48">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Standard _Menu Item"/>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon">
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>

5
samples/ControlCatalog/Pages/DataGridPage.xaml

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

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

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

2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -43,7 +43,7 @@
<Button x:Name="scrollToLast">Scroll to Last</Button>
<Button x:Name="scrollToRandom">Scroll to Random</Button>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<Border BorderThickness="1" BorderBrush="{DynamicResource SystemControlHighlightBaseMediumLowBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">

7
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,12 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="True"
SelectionMode="{Binding SelectionMode}"
Width="250"
Height="350"/>
<Button Command="{Binding AddItemCommand}">Add</Button>

68
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@ -1,10 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@ -13,72 +9,12 @@ namespace ControlCatalog.Pages
public ListBoxPage()
{
InitializeComponent();
DataContext = new PageViewModel();
DataContext = new ListBoxPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class PageViewModel : ReactiveObject
{
private int _counter;
private SelectionMode _selectionMode;
public PageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
SelectedItems = new ObservableCollection<string>();
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
{
Items.Remove(SelectedItems[0]);
}
});
SelectRandomItemCommand = ReactiveCommand.Create(() =>
{
var random = new Random();
SelectedItem = Items[random.Next(Items.Count - 1)];
});
}
public ObservableCollection<string> Items { get; }
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
}
public ObservableCollection<string> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private string GenerateItem() => $"Item {_counter++.ToString()}";
}
}
}

37
samples/ControlCatalog/Pages/MenuPage.xaml

@ -16,11 +16,17 @@
<TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
<MenuItem Header="Submenu _2 with Submenu">
<MenuItem Header="Submenu Level 2" />
</MenuItem>
<MenuItem Header="Submenu _3 with Submenu Disabled" IsEnabled="False">
<MenuItem Header="Submenu Level 2" />
</MenuItem>
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
@ -52,6 +58,33 @@
</Menu.Styles>
</Menu>
</StackPanel>
<StackPanel>
<TextBlock Classes="h3" Margin="4 8">Mixed</TextBlock>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_New" CommandParameter="{Binding}" InputGesture="Ctrl+N"/>
<Separator/>
<MenuItem Header="_Open..." InputGesture="Ctrl+O"/>
<Separator/>
<MenuItem Header="Execu_te Script..." />
<Separator/>
<MenuItem Header="_Recent" Items="{Binding RecentItems}">
<MenuItem.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
</Style>
</MenuItem.Styles>
</MenuItem>
<Separator/>
<MenuItem Header="E_xit" InputGesture="Alt+F4"/>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About"/>
</MenuItem>
</Menu>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

13
samples/ControlCatalog/Pages/ScreenPage.cs

@ -29,7 +29,8 @@ namespace ControlCatalog.Pages
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
Pen p = new Pen(Brushes.Black);
var drawBrush = Brushes.Green;
Pen p = new Pen(drawBrush);
if (screens != null)
foreach (Screen screen in screens)
{
@ -53,19 +54,19 @@ namespace ControlCatalog.Pages
};
text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text);
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
text.Text = $"Scaling: {screen.PixelDensity * 100}%";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
text.Text = $"Primary: {screen.Primary}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10));

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -12,7 +12,7 @@
<StackPanel.Styles>
<Style Selector="Border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightBaseMediumLowBrush}"/>
<Setter Property="Padding" Value="2"/>
</Style>
</StackPanel.Styles>
@ -49,7 +49,7 @@
<StackPanel.Styles>
<Style Selector="Border">
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightBaseMediumLowBrush}"/>
<Setter Property="Padding" Value="2"/>
</Style>
</StackPanel.Styles>

3
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -20,6 +20,7 @@
<TextBox Width="200"
Watermark="Password Box"
Classes="revealPasswordButton"
UseFloatingWatermark="True"
PasswordChar="*"
Text="Password" />
@ -37,6 +38,8 @@
Text="Multiline TextBox with TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox AcceptsReturn="True" Width="200" Height="125"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox Classes="clearButton" Text="Clear Content" Width="200" FontWeight="Normal" FontStyle="Normal" Watermark="Watermark" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Classes="h2">resm fonts</TextBlock>

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

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

66
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -0,0 +1,66 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class ListBoxPageViewModel : ReactiveObject
{
private int _counter;
private SelectionMode _selectionMode;
public ListBoxPageViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<string>();
Selection.Select(1);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (Selection.Count > 0)
{
Items.Remove(Selection.SelectedItems.First());
}
});
SelectRandomItemCommand = ReactiveCommand.Create(() =>
{
var random = new Random();
using (Selection.BatchUpdate())
{
Selection.Clear();
Selection.Select(random.Next(Items.Count - 1));
}
});
}
public ObservableCollection<string> Items { get; }
public SelectionModel<string> Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private string GenerateItem() => $"Item {_counter++.ToString()}";
}
}

36
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@ -17,6 +17,23 @@ namespace ControlCatalog.ViewModels
SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
var recentItems = new[]
{
new MenuItemViewModel
{
Header = "File1.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File1.txt"
},
new MenuItemViewModel
{
Header = "File2.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File2.txt"
},
};
RecentItems = recentItems;
MenuItems = new[]
{
new MenuItemViewModel
@ -24,27 +41,13 @@ namespace ControlCatalog.ViewModels
Header = "_File",
Items = new[]
{
new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
new MenuItemViewModel { Header = "O_pen...", Command = OpenCommand },
new MenuItemViewModel { Header = "Save", Command = SaveCommand },
new MenuItemViewModel { Header = "-" },
new MenuItemViewModel
{
Header = "Recent",
Items = new[]
{
new MenuItemViewModel
{
Header = "File1.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File1.txt"
},
new MenuItemViewModel
{
Header = "File2.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File2.txt"
},
}
Items = recentItems
},
}
},
@ -61,6 +64,7 @@ namespace ControlCatalog.ViewModels
}
public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
public IReadOnlyList<MenuItemViewModel> RecentItems { get; set; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public ReactiveCommand<string, Unit> OpenRecentCommand { get; }

34
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

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

3
samples/RenderDemo/MainWindow.xaml

@ -44,6 +44,9 @@
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
<TabItem Header="WriteableBitmap">
<pages:WriteableBitmapPage/>
</TabItem>
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>

92
samples/RenderDemo/Pages/WriteableBitmapPage.cs

@ -0,0 +1,92 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Threading;
namespace RenderDemo.Pages
{
public class WriteableBitmapPage : Control
{
private WriteableBitmap _unpremulBitmap;
private WriteableBitmap _premulBitmap;
private readonly Stopwatch _st = Stopwatch.StartNew();
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_unpremulBitmap = new WriteableBitmap(new PixelSize(256, 256), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Unpremul);
_premulBitmap = new WriteableBitmap(new PixelSize(256, 256), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
base.OnAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_unpremulBitmap?.Dispose();
_unpremulBitmap = null;
_premulBitmap?.Dispose();
_unpremulBitmap = null;
}
public override void Render(DrawingContext context)
{
void FillPixels(WriteableBitmap bitmap, byte fillAlpha, bool premul)
{
using (var fb = bitmap.Lock())
{
var data = new int[fb.Size.Width * fb.Size.Height];
for (int y = 0; y < fb.Size.Height; y++)
{
for (int x = 0; x < fb.Size.Width; x++)
{
var color = new Color(fillAlpha, 0, 255, 0);
if (premul)
{
byte r = (byte) (color.R * color.A / 255);
byte g = (byte) (color.G * color.A / 255);
byte b = (byte) (color.B * color.A / 255);
color = new Color(fillAlpha, r, g, b);
}
data[y * fb.Size.Width + x] = (int) color.ToUint32();
}
}
Marshal.Copy(data, 0, fb.Address, fb.Size.Width * fb.Size.Height);
}
}
base.Render(context);
byte alpha = (byte)((_st.ElapsedMilliseconds / 10) % 256);
FillPixels(_unpremulBitmap, alpha, false);
FillPixels(_premulBitmap, alpha, true);
context.FillRectangle(Brushes.Red, new Rect(0, 0, 256 * 3, 256));
context.DrawImage(_unpremulBitmap,
new Rect(0, 0, 256, 256),
new Rect(0, 0, 256, 256));
context.DrawImage(_premulBitmap,
new Rect(0, 0, 256, 256),
new Rect(256, 0, 256, 256));
context.FillRectangle(new ImmutableSolidColorBrush(Colors.Lime, alpha / 255d), new Rect(512, 0, 256, 256));
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
}
}

11
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

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

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -126,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible;
}
public double Scaling => 1;
public double RenderScaling => 1;
void Draw()
{

3
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ApiDiff.props" />
</Project>

3
src/Avalonia.Base/Avalonia.Base.csproj

@ -3,7 +3,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Avalonia.Base</AssemblyName>
<RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj"/>
@ -13,4 +13,5 @@
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
</Project>

4
src/Avalonia.Base/AvaloniaObject.cs

@ -806,7 +806,9 @@ namespace Avalonia
break;
}
if (p.IsDataValidationEnabled)
var metadata = p.GetMetadata(GetType());
if (metadata.EnableDataValidation == true)
{
UpdateDataValidation(property, value);
}

6
src/Avalonia.Base/AvaloniaProperty.cs

@ -369,14 +369,14 @@ namespace Avalonia
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
enableDataValidation: enableDataValidation);
var result = new DirectProperty<TOwner, TValue>(
name,
getter,
setter,
metadata,
enableDataValidation);
metadata);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}

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

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

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

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

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

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

25
src/Avalonia.Base/DirectProperty.cs

@ -23,16 +23,12 @@ namespace Avalonia
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
public DirectProperty(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter,
DirectPropertyMetadata<TValue> metadata,
bool enableDataValidation)
: base(name, typeof(TOwner), metadata, enableDataValidation)
DirectPropertyMetadata<TValue> metadata)
: base(name, typeof(TOwner), metadata)
{
Contract.Requires<ArgumentNullException>(getter != null);
@ -47,16 +43,12 @@ namespace Avalonia
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">Optional overridden metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
private DirectProperty(
DirectPropertyBase<TValue> source,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter,
DirectPropertyMetadata<TValue> metadata,
bool enableDataValidation)
: base(source, typeof(TOwner), metadata, enableDataValidation)
DirectPropertyMetadata<TValue> metadata)
: base(source, typeof(TOwner), metadata)
{
Contract.Requires<ArgumentNullException>(getter != null);
@ -107,7 +99,8 @@ namespace Avalonia
{
var metadata = new DirectPropertyMetadata<TValue>(
unsetValue: unsetValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
enableDataValidation: enableDataValidation);
metadata.Merge(GetMetadata<TOwner>(), this);
@ -115,8 +108,7 @@ namespace Avalonia
(DirectPropertyBase<TValue>)this,
getter,
setter,
metadata,
enableDataValidation);
metadata);
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;
@ -155,8 +147,7 @@ namespace Avalonia
this,
getter,
setter,
metadata,
enableDataValidation);
metadata);
AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;

39
src/Avalonia.Base/DirectPropertyBase.cs

@ -23,17 +23,12 @@ namespace Avalonia
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
protected DirectPropertyBase(
string name,
Type ownerType,
PropertyMetadata metadata,
bool enableDataValidation)
PropertyMetadata metadata)
: base(name, ownerType, metadata)
{
IsDataValidationEnabled = enableDataValidation;
}
/// <summary>
@ -42,17 +37,12 @@ namespace Avalonia
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
/// <param name="metadata">Optional overridden metadata.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
protected DirectPropertyBase(
AvaloniaProperty source,
Type ownerType,
PropertyMetadata metadata,
bool enableDataValidation)
PropertyMetadata metadata)
: base(source, ownerType, metadata)
{
IsDataValidationEnabled = enableDataValidation;
}
/// <summary>
@ -60,11 +50,6 @@ namespace Avalonia
/// </summary>
public abstract Type Owner { get; }
/// <summary>
/// Gets a value that indicates whether data validation is enabled for the property.
/// </summary>
public bool IsDataValidationEnabled { get; }
/// <summary>
/// Gets the value of the property on the instance.
/// </summary>
@ -102,6 +87,26 @@ namespace Avalonia
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata<T>(DirectPropertyMetadata<TValue> metadata) where T : IAvaloniaObject
{
base.OverrideMetadata(typeof(T), metadata);
}
/// <summary>
/// Overrides the metadata for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata(Type type, DirectPropertyMetadata<TValue> metadata)
{
base.OverrideMetadata(type, metadata);
}
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data)
{

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

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

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

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

61
src/Avalonia.Base/Threading/AvaloniaScheduler.cs

@ -9,6 +9,16 @@ namespace Avalonia.Threading
/// </summary>
public class AvaloniaScheduler : LocalScheduler
{
/// <summary>
/// Users can schedule actions on the dispatcher thread while being on the correct thread already.
/// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases.
/// To prevent this we are limiting amount of reentrant calls to <see cref="Schedule{TState}"/> before we will
/// schedule on a dispatcher anyway.
/// </summary>
private const int MaxReentrantSchedules = 32;
private int _reentrancyGuard;
/// <summary>
/// The instance of the <see cref="AvaloniaScheduler"/>.
/// </summary>
@ -24,31 +34,58 @@ namespace Avalonia.Threading
/// <inheritdoc/>
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
{
var composite = new CompositeDisposable(2);
IDisposable PostOnDispatcher()
{
var composite = new CompositeDisposable(2);
var cancellation = new CancellationDisposable();
Dispatcher.UIThread.Post(() =>
{
if (!cancellation.Token.IsCancellationRequested)
{
composite.Add(action(this, state));
}
}, DispatcherPriority.DataBind);
composite.Add(cancellation);
return composite;
}
if (dueTime == TimeSpan.Zero)
{
if (!Dispatcher.UIThread.CheckAccess())
{
var cancellation = new CancellationDisposable();
Dispatcher.UIThread.Post(() =>
{
if (!cancellation.Token.IsCancellationRequested)
{
composite.Add(action(this, state));
}
}, DispatcherPriority.DataBind);
composite.Add(cancellation);
return PostOnDispatcher();
}
else
{
return action(this, state);
if (_reentrancyGuard >= MaxReentrantSchedules)
{
return PostOnDispatcher();
}
try
{
_reentrancyGuard++;
return action(this, state);
}
finally
{
_reentrancyGuard--;
}
}
}
else
{
var composite = new CompositeDisposable(2);
composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime));
return composite;
}
return composite;
}
}
}

2
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -128,7 +128,7 @@ namespace Avalonia.Build.Tasks
asm.MainModule.Types.Add(typeDef);
var builder = typeSystem.CreateTypeBuilder(typeDef);
foreach (var res in group.Resources.Where(CheckXamlName))
foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant()))
{
try
{

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -22,4 +22,5 @@
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
</Project>

80
src/Avalonia.Controls.DataGrid/DataGrid.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

18
src/Avalonia.Controls/ApiCompatBaseline.txt

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

17
src/Avalonia.Controls/AppBuilderBase.cs

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

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1647,7 +1647,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
private void DropDownPopup_Closed(object sender, EventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@ -1655,11 +1655,6 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{

6
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -1,7 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
@ -14,4 +17,5 @@
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\ApiDiff.props" />
</Project>

7
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -889,17 +889,12 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
private void PopUp_Closed(object sender, PopupClosedEventArgs e)
private void PopUp_Closed(object sender, EventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}

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

@ -38,12 +38,10 @@ namespace Avalonia.Controls.Chrome
{
if (_disposables != null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer?.Children.Remove(this);
_disposables.Dispose();
_disposables = null;
_hostWindow = null;
}
}

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

@ -12,106 +12,86 @@ namespace Avalonia.Controls.Chrome
public class TitleBar : TemplatedControl
{
private CompositeDisposable? _disposables;
private readonly Window? _hostWindow;
private CaptionButtons? _captionButtons;
public TitleBar(Window hostWindow)
private void UpdateSize(Window window)
{
_hostWindow = hostWindow;
}
public TitleBar()
{
}
public void Attach()
{
if (_disposables == null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer?.Children.Add(this);
if (_hostWindow != null)
{
_disposables = new CompositeDisposable
{
_hostWindow.GetObservable(Window.WindowDecorationMarginProperty)
.Subscribe(x => UpdateSize()),
_hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty)
.Subscribe(x => UpdateSize()),
_hostWindow.GetObservable(Window.OffScreenMarginProperty)
.Subscribe(x => UpdateSize()),
_hostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
})
};
_captionButtons?.Attach(_hostWindow);
}
UpdateSize();
}
}
private void UpdateSize()
{
if (_hostWindow != null)
if (window != null)
{
Margin = new Thickness(
_hostWindow.OffScreenMargin.Left,
_hostWindow.OffScreenMargin.Top,
_hostWindow.OffScreenMargin.Right,
_hostWindow.OffScreenMargin.Bottom);
window.OffScreenMargin.Left,
window.OffScreenMargin.Top,
window.OffScreenMargin.Right,
window.OffScreenMargin.Bottom);
if (_hostWindow.WindowState != WindowState.FullScreen)
if (window.WindowState != WindowState.FullScreen)
{
Height = _hostWindow.WindowDecorationMargin.Top;
Height = window.WindowDecorationMargin.Top;
if (_captionButtons != null)
{
_captionButtons.Height = Height;
}
}
IsVisible = window.PlatformImpl.NeedsManagedDecorations;
}
}
public void Detach()
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_disposables != null)
{
var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow);
layer?.Children.Remove(this);
base.OnApplyTemplate(e);
_disposables.Dispose();
_disposables = null;
_captionButtons?.Detach();
_captionButtons = e.NameScope.Get<CaptionButtons>("PART_CaptionButtons");
_captionButtons?.Detach();
if (VisualRoot is Window window)
{
_captionButtons?.Attach(window);
UpdateSize(window);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnApplyTemplate(e);
_captionButtons = e.NameScope.Get<CaptionButtons>("PART_CaptionButtons");
base.OnAttachedToVisualTree(e);
if (_hostWindow != null)
if (VisualRoot is Window window)
{
_captionButtons.Attach(_hostWindow);
_disposables = new CompositeDisposable
{
window.GetObservable(Window.WindowDecorationMarginProperty)
.Subscribe(x => UpdateSize(window)),
window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty)
.Subscribe(x => UpdateSize(window)),
window.GetObservable(Window.OffScreenMarginProperty)
.Subscribe(x => UpdateSize(window)),
window.GetObservable(Window.ExtendClientAreaChromeHintsProperty)
.Subscribe(x => UpdateSize(window)),
window.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
}),
window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
.Subscribe(x => UpdateSize(window))
};
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
UpdateSize();
_disposables?.Dispose();
_captionButtons?.Detach();
_captionButtons = null;
}
}
}

22
src/Avalonia.Controls/ComboBox.cs

@ -290,24 +290,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
/// <summary>
/// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
/// that caused the popup to close.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method can be overridden to control whether the event that caused the popup to close
/// is swallowed or passed through.
/// </remarks>
protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@ -318,13 +300,11 @@ namespace Avalonia.Controls
}
}
private void PopupClosed(object sender, PopupClosedEventArgs e)
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
PopupClosedOverride(e);
if (CanFocus(this))
{
Focus();

11
src/Avalonia.Controls/ContextMenu.cs

@ -265,7 +265,8 @@ namespace Avalonia.Controls
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
StaysOpen = false
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
};
_popup.Opened += PopupOpened;
@ -278,10 +279,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
if (PlacementTarget is null && _popup.PlacementTarget != control)
{
_popup.PlacementTarget = control;
}
_popup.Child = this;
IsOpen = true;
_popup.IsOpen = true;
RaiseEvent(new RoutedEventArgs
{

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

@ -88,7 +88,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>(nameof(SelectedDate),
x => x.SelectedDate, (x, v) => x.SelectedDate = v);
//Template Items
// Template Items
private Button _flyoutButton;
private TextBlock _dayText;
private TextBlock _monthText;
@ -359,10 +359,14 @@ namespace Avalonia.Controls
}
}
Grid.SetColumn(_spacer1, 1);
Grid.SetColumn(_spacer2, 3);
_spacer1.IsVisible = columnIndex > 1;
_spacer2.IsVisible = columnIndex > 2;
var isSpacer1Visible = columnIndex > 1;
var isSpacer2Visible = columnIndex > 2;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
_spacer1.IsVisible = isSpacer1Visible;
_spacer2.IsVisible = isSpacer2Visible;
}
private void SetSelectedDateText()
@ -398,7 +402,7 @@ namespace Avalonia.Controls
var deltaY = _presenter.GetOffsetForPopup();
//The extra 5 px I think is related to default popup placement behavior
// The extra 5 px I think is related to default popup placement behavior
_popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);

40
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -77,7 +77,7 @@ namespace Avalonia.Controls
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>(x =>
x.YearVisible, (x, v) => x.YearVisible = v);
//Template Items
// Template Items
private Grid _pickerContainer;
private Button _acceptButton;
private Button _dismissButton;
@ -107,7 +107,7 @@ namespace Avalonia.Controls
private bool _yearVisible = true;
private DateTimeOffset _syncDate;
private GregorianCalendar _calendar;
private readonly GregorianCalendar _calendar;
private bool _suppressUpdateSelection;
public DatePickerPresenter()
@ -234,7 +234,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
//These are requirements, so throw if not found
// These are requirements, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
_monthHost = e.NameScope.Get<Panel>("MonthHost");
_dayHost = e.NameScope.Get<Panel>("DayHost");
@ -326,7 +326,7 @@ namespace Avalonia.Controls
/// </summary>
private void InitPicker()
{
//OnApplyTemplate must've been called before we can init here...
// OnApplyTemplate must've been called before we can init here...
if (_pickerContainer == null)
return;
@ -344,12 +344,11 @@ namespace Avalonia.Controls
SetGrid();
//Date should've been set when we reach this point
// Date should've been set when we reach this point
var dt = Date;
if (DayVisible)
{
GregorianCalendar gc = new GregorianCalendar();
var maxDays = gc.GetDaysInMonth(dt.Year, dt.Month);
var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
_daySelector.MaximumValue = maxDays;
_daySelector.MinimumValue = 1;
_daySelector.SelectedValue = dt.Day;
@ -357,10 +356,17 @@ namespace Avalonia.Controls
}
if (MonthVisible)
{
_monthSelector.SelectedValue = dt.Month;
_monthSelector.FormatDate = dt.Date;
}
if (YearVisible)
{
_yearSelector.SelectedValue = dt.Year;
_yearSelector.FormatDate = dt.Date;
}
_suppressUpdateSelection = false;
SetInitialFocus();
@ -407,10 +413,14 @@ namespace Avalonia.Controls
}
}
Grid.SetColumn(_spacer1, 1);
Grid.SetColumn(_spacer2, 3);
_spacer1.IsVisible = columnIndex > 1;
_spacer2.IsVisible = columnIndex > 2;
var isSpacer1Visible = columnIndex > 1;
var isSpacer2Visible = columnIndex > 2;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0);
Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0);
_spacer1.IsVisible = isSpacer1Visible;
_spacer2.IsVisible = isSpacer2Visible;
}
private void SetInitialFocus()
@ -433,12 +443,12 @@ namespace Avalonia.Controls
}
}
private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnDismissButtonClicked(object sender, RoutedEventArgs e)
{
OnDismiss();
}
private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnAcceptButtonClicked(object sender, RoutedEventArgs e)
{
Date = _syncDate;
OnConfirmed();
@ -471,7 +481,7 @@ namespace Avalonia.Controls
_syncDate = newDate;
//We don't need to update the days if not displaying day, not february
// We don't need to update the days if not displaying day, not february
if (!DayVisible || _syncDate.Month != 2)
return;

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

@ -15,7 +15,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="MinuteIncrement"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, int> MinuteIncrementProperty =
AvaloniaProperty.RegisterDirect<TimePicker, int>(nameof(MinuteIncrement),
AvaloniaProperty.RegisterDirect<TimePicker, int>(nameof(MinuteIncrement),
x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v);
/// <summary>
@ -34,17 +34,17 @@ namespace Avalonia.Controls
/// Defines the <see cref="ClockIdentifier"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, string> ClockIdentifierProperty =
AvaloniaProperty.RegisterDirect<TimePicker, string>(nameof(ClockIdentifier),
AvaloniaProperty.RegisterDirect<TimePicker, string>(nameof(ClockIdentifier),
x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v);
/// <summary>
/// Defines the <see cref="SelectedTime"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime),
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime),
x => x.SelectedTime, (x, v) => x.SelectedTime = v);
//Template Items
// Template Items
private TimePickerPresenter _presenter;
private Button _flyoutButton;
private Border _firstPickerHost;
@ -52,7 +52,7 @@ namespace Avalonia.Controls
private Border _thirdPickerHost;
private TextBlock _hourText;
private TextBlock _minuteText;
public TextBlock _periodText;
private TextBlock _periodText;
private Rectangle _firstSplitter;
private Rectangle _secondSplitter;
private Grid _contentGrid;
@ -145,7 +145,7 @@ namespace Avalonia.Controls
if (_flyoutButton != null)
_flyoutButton.Click -= OnFlyoutButtonClicked;
if(_presenter != null)
if (_presenter != null)
{
_presenter.Confirmed -= OnConfirmed;
_presenter.Dismissed -= OnDismissPicker;
@ -170,7 +170,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Find<Popup>("Popup");
_presenter = e.NameScope.Find<TimePickerPresenter>("PickerPresenter");
if (_flyoutButton != null)
_flyoutButton.Click += OnFlyoutButtonClicked;
@ -185,7 +184,6 @@ namespace Avalonia.Controls
_presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty];
_presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty];
}
}
private void SetGrid()
@ -195,30 +193,19 @@ namespace Avalonia.Controls
bool use24HourClock = ClockIdentifier == "24HourClock";
if (!use24HourClock)
{
_contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*");
_thirdPickerHost.IsVisible = true;
_secondSplitter.IsVisible = true;
var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *";
_contentGrid.ColumnDefinitions = new ColumnDefinitions(columnsD);
Grid.SetColumn(_firstPickerHost, 0);
Grid.SetColumn(_secondPickerHost, 2);
Grid.SetColumn(_thirdPickerHost, 4);
_thirdPickerHost.IsVisible = !use24HourClock;
_secondSplitter.IsVisible = !use24HourClock;
Grid.SetColumn(_firstSplitter, 1);
Grid.SetColumn(_secondSplitter, 3);
}
else
{
_contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*");
_thirdPickerHost.IsVisible = false;
_secondSplitter.IsVisible = false;
Grid.SetColumn(_firstPickerHost, 0);
Grid.SetColumn(_secondPickerHost, 2);
Grid.SetColumn(_firstPickerHost, 0);
Grid.SetColumn(_secondPickerHost, 2);
Grid.SetColumn(_thirdPickerHost, use24HourClock ? 0 : 4);
Grid.SetColumn(_firstSplitter, 1);
}
Grid.SetColumn(_firstSplitter, 1);
Grid.SetColumn(_secondSplitter, use24HourClock ? 0 : 3);
}
private void SetSelectedTimeText()
@ -237,14 +224,13 @@ namespace Avalonia.Controls
hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
newTime = new TimeSpan(hr, newTime.Minutes, 0);
}
_hourText.Text = newTime.ToString("%h");
_hourText.Text = newTime.ToString("%h");
_minuteText.Text = newTime.ToString("mm");
PseudoClasses.Set(":hasnotime", false);
_periodText.Text = time.Value.Hours >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator :
CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
}
else
{
@ -262,7 +248,7 @@ namespace Avalonia.Controls
SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(oldTime, newTime));
}
private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnFlyoutButtonClicked(object sender, Interactivity.RoutedEventArgs e)
{
_presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
@ -270,7 +256,7 @@ namespace Avalonia.Controls
var deltaY = _presenter.GetOffsetForPopup();
//The extra 5 px I think is related to default popup placement behavior
// The extra 5 px I think is related to default popup placement behavior
_popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
@ -287,6 +273,5 @@ namespace Avalonia.Controls
_popup.Close();
SelectedTime = _presenter.Time;
}
}
}

38
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@ -39,7 +39,7 @@ namespace Avalonia.Controls
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
//TemplateItems
// TemplateItems
private Grid _pickerContainer;
private Button _acceptButton;
private Button _dismissButton;
@ -55,8 +55,8 @@ namespace Avalonia.Controls
private Button _minuteDownButton;
private Button _periodDownButton;
//Backing Fields
private TimeSpan _Time;
// Backing Fields
private TimeSpan _time;
private int _minuteIncrement = 1;
private string _clockIdentifier = "12HourClock";
@ -83,7 +83,7 @@ namespace Avalonia.Controls
get => _clockIdentifier;
set
{
if (string.IsNullOrEmpty(value) || value == "" || !(value == "12HourClock" || value == "24HourClock"))
if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock"))
throw new ArgumentException("Invalid ClockIdentifier");
SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
InitPicker();
@ -95,10 +95,10 @@ namespace Avalonia.Controls
/// </summary>
public TimeSpan Time
{
get => _Time;
get => _time;
set
{
SetAndRaise(TimeProperty, ref _Time, value);
SetAndRaise(TimeProperty, ref _time, value);
InitPicker();
}
}
@ -213,26 +213,24 @@ namespace Avalonia.Controls
private void SetGrid()
{
if (ClockIdentifier == "12HourClock")
{
_pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*");
_spacer2.IsVisible = true;
_periodHost.IsVisible = true;
}
else
{
_pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*");
_spacer2.IsVisible = false;
_periodHost.IsVisible = false;
}
bool use24HourClock = ClockIdentifier == "24HourClock";
var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *";
_pickerContainer.ColumnDefinitions = new ColumnDefinitions(columnsD);
_spacer2.IsVisible = !use24HourClock;
_periodHost.IsVisible = !use24HourClock;
Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
}
private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnDismissButtonClicked(object sender, RoutedEventArgs e)
{
OnDismiss();
}
private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void OnAcceptButtonClicked(object sender, RoutedEventArgs e)
{
OnConfirmed();
}

2
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -35,7 +35,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
}
}
public double Scaling
public double RenderScaling
{
get { return _scaling; }
set

1
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -142,7 +142,6 @@ namespace Avalonia.Controls.Generators
private readonly IDataTemplate _inner;
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
public IControl Build(object param) => _inner.Build(param);
public bool SupportsRecycling => _inner.SupportsRecycling;
public bool Match(object data) => _inner.Match(data);
public InstancedBinding ItemsSelector(object item) => null;
}

4
src/Avalonia.Controls/IMenuElement.cs

@ -1,6 +1,8 @@
using System.Collections.Generic;
using Avalonia.Input;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -11,7 +13,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
IMenuItem SelectedItem { get; set; }
IMenuItem? SelectedItem { get; set; }
/// <summary>
/// Gets the submenu items.

6
src/Avalonia.Controls/IMenuItem.cs

@ -1,4 +1,6 @@
namespace Avalonia.Controls
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Represents a <see cref="MenuItem"/>.
@ -29,7 +31,7 @@
/// <summary>
/// Gets the parent <see cref="IMenuElement"/>.
/// </summary>
new IMenuElement Parent { get; }
new IMenuElement? Parent { get; }
/// <summary>
/// Raises a click event on the menu item.

249
src/Avalonia.Controls/ISelectionModel.cs

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

200
src/Avalonia.Controls/IndexPath.cs

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

32
src/Avalonia.Controls/ItemsControl.cs

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

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

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

10
src/Avalonia.Controls/ListBox.cs

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

5
src/Avalonia.Controls/Menu.cs

@ -1,9 +1,12 @@
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -14,6 +17,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
private LightDismissOverlayLayer? _overlay;
/// <summary>
/// Initializes a new instance of the <see cref="Menu"/> class.
/// </summary>

8
src/Avalonia.Controls/MenuBase.cs

@ -8,6 +8,8 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -51,9 +53,7 @@ namespace Avalonia.Controls
/// <param name="interactionHandler">The menu interaction handler.</param>
public MenuBase(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);
InteractionHandler = interactionHandler;
InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler));
}
/// <summary>
@ -77,7 +77,7 @@ namespace Avalonia.Controls
IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler;
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
IMenuItem? IMenuElement.SelectedItem
{
get
{

70
src/Avalonia.Controls/MenuItem.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
@ -12,6 +13,8 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -22,7 +25,7 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
public static readonly DirectProperty<MenuItem, ICommand?> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
@ -94,10 +97,9 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel());
private ICommand _command;
private ICommand? _command;
private bool _commandCanExecute = true;
private Popup _popup;
private IDisposable _gridHack;
/// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class.
@ -119,6 +121,32 @@ namespace Avalonia.Controls
public MenuItem()
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
//
// In addition to the hack from WPF, we also make sure to return null when we have
// no parent. If we don't do this, inheritance falls back to the logical tree,
// causing the shared size scope in the parent MenuItem to be used, breaking
// menu layout.
var parentSharedSizeScope = this.GetObservable(VisualParentProperty)
.SelectMany(x =>
{
var parent = x as Control;
return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
Observable.Return<DefinitionBase.SharedSizeScope>(null);
});
this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
}
/// <summary>
@ -166,7 +194,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the command associated with the menu item.
/// </summary>
public ICommand Command
public ICommand? Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
@ -246,7 +274,7 @@ namespace Avalonia.Controls
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
@ -254,7 +282,7 @@ namespace Avalonia.Controls
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
IMenuItem? IMenuElement.SelectedItem
{
get
{
@ -323,32 +351,6 @@ namespace Avalonia.Controls
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
_gridHack?.Dispose();
_gridHack = null;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.GetVisualParent() is IControl parent)
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
_gridHack = Bind(
DefinitionBase.PrivateSharedSizeScopeProperty,
parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
}
}
/// <summary>
@ -551,7 +553,7 @@ namespace Avalonia.Controls
/// <param name="e">The property change event.</param>
private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
if ((bool)e.NewValue!)
{
Focus();
}
@ -563,7 +565,7 @@ namespace Avalonia.Controls
/// <param name="e">The property change event.</param>
private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue;
var value = (bool)e.NewValue!;
if (value)
{

12
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -3,7 +3,6 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -235,7 +234,9 @@ namespace Avalonia.Controls.Platform
// If the the parent is an IMenu which successfully moved its selection,
// and the current menu is open then close the current menu and open the
// new menu.
if (item.IsSubMenuOpen && item.Parent is IMenu)
if (item.IsSubMenuOpen &&
item.Parent is IMenu &&
item.Parent.SelectedItem is object)
{
item.Close();
Open(item.Parent.SelectedItem, true);
@ -363,6 +364,11 @@ namespace Avalonia.Controls.Platform
}
else
{
if (item.IsTopLevel && item.Parent is IMainMenu mainMenu)
{
mainMenu.Open();
}
Open(item, false);
}
@ -385,7 +391,7 @@ namespace Avalonia.Controls.Platform
{
if (e.Source == Menu)
{
Menu.MoveSelection(NavigationDirection.First, true);
Menu?.MoveSelection(NavigationDirection.First, true);
}
}

6
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -23,10 +23,10 @@ namespace Avalonia.Platform
Size ClientSize { get; }
/// <summary>
/// Gets the scaling factor for the toplevel.
/// Gets the scaling factor for the toplevel. This is used for rendering.
/// </summary>
double Scaling { get; }
double RenderScaling { get; }
/// <summary>
/// The list of native platform's surfaces that can be consumed by rendering subsystems.
/// </summary>

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

@ -13,6 +13,11 @@ namespace Avalonia.Platform
/// Hides the window.
/// </summary>
void Hide();
/// <summary>
/// Gets the scaling factor for Window positioning and sizing.
/// </summary>
double DesktopScaling { get; }
/// <summary>
/// Gets the position of the window in device pixels.

21
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -86,7 +86,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
private IDataTemplate _dataTemplate;
private IRecyclingDataTemplate _recyclingDataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
/// <summary>
@ -281,7 +281,7 @@ namespace Avalonia.Controls.Presenters
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_dataTemplate = null;
_recyclingDataTemplate = null;
_createdChild = false;
InvalidateMeasure();
}
@ -307,22 +307,21 @@ namespace Avalonia.Controls.Presenters
{
var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default;
// We have content and it isn't a control, so if the new data template is the same
// as the old data template, try to recycle the existing child control to display
// the new data.
if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling)
if (dataTemplate is IRecyclingDataTemplate rdt)
{
newChild = oldChild;
var toRecycle = rdt == _recyclingDataTemplate ? oldChild : null;
newChild = rdt.Build(content, toRecycle);
_recyclingDataTemplate = rdt;
}
else
{
_dataTemplate = dataTemplate;
newChild = _dataTemplate.Build(content);
newChild = dataTemplate.Build(content);
_recyclingDataTemplate = null;
}
}
else
{
_dataTemplate = null;
_recyclingDataTemplate = null;
}
return newChild;
@ -422,7 +421,7 @@ namespace Avalonia.Controls.Presenters
LogicalChildren.Remove(Child);
((ISetInheritanceParent)Child).SetParent(Child.Parent);
Child = null;
_dataTemplate = null;
_recyclingDataTemplate = null;
}
InvalidateMeasure();

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

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

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

@ -14,6 +14,9 @@ namespace Avalonia.Controls.Presenters
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
@ -75,7 +78,7 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender<TextPresenter>(SelectionBrushProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty,
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
@ -84,7 +87,7 @@ namespace Avalonia.Controls.Presenters
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
SelectionStartProperty.Changed, SelectionEndProperty.Changed,
SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed
).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
@ -210,6 +213,12 @@ namespace Avalonia.Controls.Presenters
set => SetValue(PasswordCharProperty, value);
}
public bool RevealPassword
{
get => GetValue(RevealPasswordProperty);
set => SetValue(RevealPasswordProperty, value);
}
public IBrush SelectionBrush
{
get => GetValue(SelectionBrushProperty);
@ -426,7 +435,7 @@ namespace Avalonia.Controls.Presenters
var text = Text;
if (PasswordChar != default(char))
if (PasswordChar != default(char) && !RevealPassword)
{
result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}

7
src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs

@ -8,7 +8,7 @@ namespace Avalonia.Controls.Primitives
{
public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest
{
public static ChromeOverlayLayer? GetOverlayLayer(IVisual visual)
public static Panel? GetOverlayLayer(IVisual visual)
{
foreach (var v in visual.GetVisualAncestors())
if (v is VisualLayerManager vlm)
@ -24,6 +24,11 @@ namespace Avalonia.Controls.Primitives
return null;
}
public void Add(Control c)
{
base.Children.Add(c);
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
}
}

61
src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs

@ -0,0 +1,61 @@
using System;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// A layer that is used to dismiss a <see cref="Popup"/> when the user clicks outside.
/// </summary>
public class LightDismissOverlayLayer : Border, ICustomHitTest
{
public IInputElement? InputPassThroughElement { get; set; }
/// <summary>
/// Returns the light dismiss overlay for a specified visual.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>The light dismiss overlay, or null if none found.</returns>
public static LightDismissOverlayLayer? GetLightDismissOverlayLayer(IVisual visual)
{
visual = visual ?? throw new ArgumentNullException(nameof(visual));
VisualLayerManager? manager;
if (visual is TopLevel topLevel)
{
manager = topLevel.GetTemplateChildren()
.OfType<VisualLayerManager>()
.FirstOrDefault();
}
else
{
manager = visual.FindAncestorOfType<VisualLayerManager>();
}
return manager?.LightDismissOverlayLayer;
}
public bool HitTest(Point point)
{
if (InputPassThroughElement is object)
{
var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value);
var hit = VisualRoot.GetVisualAt(p, x => x != this);
if (hit is object)
{
return !InputPassThroughElement.IsVisualAncestorOf(hit);
}
}
return true;
}
}
}

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

@ -1,12 +1,10 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
@ -86,12 +84,27 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
public static readonly DirectProperty<Popup, IInputElement> OverlayInputPassThroughElementProperty =
AvaloniaProperty.RegisterDirect<Popup, IInputElement>(
nameof(OverlayInputPassThroughElement),
o => o.OverlayInputPassThroughElement,
(o, v) => o.OverlayInputPassThroughElement = v);
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
/// <summary>
/// Defines the <see cref="IsLightDismissEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsLightDismissEnabledProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(IsLightDismissEnabled));
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
@ -101,8 +114,13 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
/// </summary>
public static readonly StyledProperty<bool> StaysOpenProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(StaysOpen), true);
[Obsolete("Use IsLightDismissEnabledProperty")]
public static readonly DirectProperty<Popup, bool> StaysOpenProperty =
AvaloniaProperty.RegisterDirect<Popup, bool>(
nameof(StaysOpen),
o => o.StaysOpen,
(o, v) => o.StaysOpen = v,
true);
/// <summary>
/// Defines the <see cref="Topmost"/> property.
@ -113,6 +131,7 @@ namespace Avalonia.Controls.Primitives
private bool _isOpen;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private IInputElement _overlayInputPassThroughElement;
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
@ -127,7 +146,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler<PopupClosedEventArgs>? Closed;
public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
@ -165,6 +184,18 @@ namespace Avalonia.Controls.Primitives
set;
}
/// <summary>
/// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
/// </summary>
/// <remarks>
/// Light dismiss is when the user taps on any area other than the popup.
/// </remarks>
public bool IsLightDismissEnabled
{
get => GetValue(IsLightDismissEnabledProperty);
set => SetValue(IsLightDismissEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the popup is currently open.
/// </summary>
@ -246,6 +277,32 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ObeyScreenEdgesProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the event that closes the popup is passed
/// through to the parent window.
/// </summary>
/// <remarks>
/// When <see cref="IsLightDismissEnabled"/> is set to true, clicks outside the the popup
/// cause the popup to close. When <see cref="OverlayDismissEventPassThrough"/> is set to
/// false, these clicks will be handled by the popup and not be registered by the parent
/// window. When set to true, the events will be passed through to the parent window.
/// </remarks>
public bool OverlayDismissEventPassThrough
{
get => GetValue(OverlayDismissEventPassThroughProperty);
set => SetValue(OverlayDismissEventPassThroughProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the popup's overlay.
/// </summary>
public IInputElement OverlayInputPassThroughElement
{
get => _overlayInputPassThroughElement;
set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value);
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
@ -268,10 +325,11 @@ namespace Avalonia.Controls.Primitives
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
/// </summary>
[Obsolete("Use IsLightDismissEnabled")]
public bool StaysOpen
{
get { return GetValue(StaysOpenProperty); }
set { SetValue(StaysOpenProperty, value); }
get => !IsLightDismissEnabled;
set => IsLightDismissEnabled = !value;
}
/// <summary>
@ -363,14 +421,12 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
}
DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
@ -384,6 +440,29 @@ namespace Avalonia.Controls.Primitives
state.popupHost.Dispose();
});
if (IsLightDismissEnabled)
{
var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget);
if (dismissLayer != null)
{
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
DeferCleanup(Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}));
DeferCleanup(SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler));
}
}
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
@ -401,7 +480,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Closes the popup.
/// </summary>
public void Close() => CloseCore(null);
public void Close() => CloseCore();
/// <summary>
/// Measures the control.
@ -471,7 +550,7 @@ namespace Avalonia.Controls.Primitives
}
}
private void CloseCore(EventArgs? closeEvent)
private void CloseCore()
{
if (_openState is null)
{
@ -491,24 +570,46 @@ namespace Avalonia.Controls.Primitives
IsOpen = false;
}
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
CloseCore(e);
CloseCore();
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v))
{
CloseCore(e);
CloseCore();
if (OverlayDismissEventPassThrough)
{
PassThroughEvent(e);
}
}
}
private void PassThroughEvent(PointerPressedEventArgs e)
{
if (e.Source is LightDismissOverlayLayer layer &&
layer.GetVisualRoot() is IInputElement root)
{
var p = e.GetCurrentPoint(root);
var hit = root.InputHitTest(p.Position, x => x != layer);
if (hit != null)
{
e.Pointer.Capture(hit);
hit.RaiseEvent(e);
e.Handled = true;
}
}
}
@ -602,7 +703,7 @@ namespace Avalonia.Controls.Primitives
private void WindowDeactivated(object sender, EventArgs e)
{
if (!StaysOpen)
if (IsLightDismissEnabled)
{
Close();
}
@ -610,7 +711,7 @@ namespace Avalonia.Controls.Primitives
private void ParentClosed(object sender, EventArgs e)
{
if (!StaysOpen)
if (IsLightDismissEnabled)
{
Close();
}
@ -618,7 +719,7 @@ namespace Avalonia.Controls.Primitives
private void WindowLostFocus()
{
if(!StaysOpen)
if (IsLightDismissEnabled)
Close();
}

33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@ -1,33 +0,0 @@
using System;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Holds data for the <see cref="Popup.Closed"/> event.
/// </summary>
public class PopupClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
/// </summary>
/// <param name="closeEvent"></param>
public PopupClosedEventArgs(EventArgs? closeEvent)
{
CloseEvent = closeEvent;
}
/// <summary>
/// Gets the event that closed the popup, if any.
/// </summary>
/// <remarks>
/// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
/// interaction that caused the popup to close if the close was caused by e.g. a pointer press
/// outside the popup. It can be used to mark the event as handled if the event should not
/// be propagated.
/// </remarks>
public EventArgs? CloseEvent { get; }
}
}

4
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@ -40,9 +40,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public void MoveAndResize(Point devicePoint, Size virtualSize)
{
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.RenderScaling);
}
public virtual double Scaling => _parent.Scaling;
public virtual double Scaling => _parent.DesktopScaling;
}
}

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

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

53
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using Avalonia.LogicalTree;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
@ -7,14 +8,17 @@ namespace Avalonia.Controls.Primitives
{
private const int AdornerZIndex = int.MaxValue - 100;
private const int ChromeZIndex = int.MaxValue - 99;
private const int OverlayZIndex = int.MaxValue - 98;
private const int LightDismissOverlayZIndex = int.MaxValue - 98;
private const int OverlayZIndex = int.MaxValue - 97;
private ILogicalRoot _logicalRoot;
private readonly List<Control> _layers = new List<Control>();
public static readonly StyledProperty<ChromeOverlayLayer> ChromeOverlayLayerProperty =
AvaloniaProperty.Register<VisualLayerManager, ChromeOverlayLayer>(nameof(ChromeOverlayLayer));
public bool IsPopup { get; set; }
public AdornerLayer AdornerLayer
{
get
@ -30,10 +34,19 @@ namespace Avalonia.Controls.Primitives
{
get
{
var rv = FindLayer<ChromeOverlayLayer>();
if (rv == null)
AddLayer(rv = new ChromeOverlayLayer(), ChromeZIndex);
return rv;
var current = GetValue(ChromeOverlayLayerProperty);
if (current is null)
{
var chromeOverlayLayer = new ChromeOverlayLayer();
AddLayer(chromeOverlayLayer, ChromeZIndex);
SetValue(ChromeOverlayLayerProperty, chromeOverlayLayer);
current = chromeOverlayLayer;
}
return current;
}
}
@ -44,12 +57,33 @@ namespace Avalonia.Controls.Primitives
if (IsPopup)
return null;
var rv = FindLayer<OverlayLayer>();
if(rv == null)
if (rv == null)
AddLayer(rv = new OverlayLayer(), OverlayZIndex);
return rv;
}
}
public LightDismissOverlayLayer LightDismissOverlayLayer
{
get
{
if (IsPopup)
return null;
var rv = FindLayer<LightDismissOverlayLayer>();
if (rv == null)
{
rv = new LightDismissOverlayLayer
{
Background = Brushes.Transparent,
IsVisible = false
};
AddLayer(rv, LightDismissOverlayZIndex);
}
return rv;
}
}
T FindLayer<T>() where T : class
{
foreach (var layer in _layers)
@ -65,7 +99,8 @@ namespace Avalonia.Controls.Primitives
layer.ZIndex = zindex;
VisualChildren.Add(layer);
if (((ILogical)this).IsAttachedToLogicalTree)
((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
((ILogical)layer).NotifyAttachedToLogicalTree(
new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
InvalidateArrange();
}

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

Loading…
Cancel
Save