diff --git a/.editorconfig b/.editorconfig index 5f08d1e940..f6bce9cb76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +space_within_single_line_array_initializer_braces = true # Wrapping preferences csharp_wrap_before_ternary_opsigns = false # Xaml files [*.xaml] -indent_size = 4 +indent_size = 2 # Xml project files [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] diff --git a/.gitignore b/.gitignore index 971c945246..b5a46e16f4 100644 --- a/.gitignore +++ b/.gitignore @@ -198,3 +198,11 @@ info.plist build-intermediate obj-Direct2D1/ obj-Skia/ + +################## +# Vim +################## +.vim +coc-settings.json +.ccls-cache +.ccls diff --git a/.gitmodules b/.gitmodules index 22a241f120..10c780c09f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github"] - path = src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github - url = https://github.com/AvaloniaUI/Portable.Xaml.git [submodule "nukebuild/Numerge"] path = nukebuild/Numerge url = https://github.com/kekekeks/Numerge.git diff --git a/Avalonia.sln b/Avalonia.sln index 484d7a4cde..ac678ba9ba 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -63,8 +63,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE src\Shared\SharedAssemblyInfo.cs = src\Shared\SharedAssemblyInfo.cs EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gtk", "Gtk", "{B9894058-278A-46B5-B6ED-AD613FCC03B3}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}" @@ -123,8 +121,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Gtk3", "src\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj", "{BB1F7BB5-6AD4-4776-94D9-C09D0A972658}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "samples\ControlCatalog.NetCore\ControlCatalog.NetCore.csproj", "{39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" @@ -146,7 +142,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Serilog.props = build\Serilog.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props - build\Splat.props = build\Splat.props build\System.Memory.props = build\System.Memory.props build\XUnit.props = build\XUnit.props EndProjectSection @@ -1319,30 +1314,6 @@ Global {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Release|iPhone.Build.0 = Release|Any CPU {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Any CPU.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhone.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhone.Build.0 = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Any CPU.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhone.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhone.Build.0 = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -1912,7 +1883,6 @@ Global {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} - {BB1F7BB5-6AD4-4776-94D9-C09D0A972658} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f4fbb0d50..7e3532ee23 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: - job: Windows pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' steps: - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build-native.sh b/build-native.sh old mode 100644 new mode 100755 diff --git a/build/Base.props b/build/Base.props index e565ab1664..a60373ebb3 100644 --- a/build/Base.props +++ b/build/Base.props @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 4f0b1f0a5b..76abcf6912 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.1 + 0.8.999 Copyright 2019 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index cf8e0fd13a..c03ad0fefd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - + diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 7486aaad69..971bcf24f8 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -26,7 +26,7 @@ const int kVK_ANSI_3 = 0x14; const int kVK_ANSI_4 = 0x15; const int kVK_ANSI_6 = 0x16; const int kVK_ANSI_5 = 0x17; -//const int kVK_ANSI_Equal = 0x18; +const int kVK_ANSI_Equal = 0x18; const int kVK_ANSI_9 = 0x19; const int kVK_ANSI_7 = 0x1A; const int kVK_ANSI_Minus = 0x1B; @@ -45,11 +45,11 @@ const int kVK_ANSI_K = 0x28; const int kVK_ANSI_Semicolon = 0x29; const int kVK_ANSI_Backslash = 0x2A; const int kVK_ANSI_Comma = 0x2B; -//const int kVK_ANSI_Slash = 0x2C; +const int kVK_ANSI_Slash = 0x2C; const int kVK_ANSI_N = 0x2D; const int kVK_ANSI_M = 0x2E; const int kVK_ANSI_Period = 0x2F; -//const int kVK_ANSI_Grave = 0x32; +const int kVK_ANSI_Grave = 0x32; const int kVK_ANSI_KeypadDecimal = 0x41; const int kVK_ANSI_KeypadMultiply = 0x43; const int kVK_ANSI_KeypadPlus = 0x45; @@ -57,7 +57,7 @@ const int kVK_ANSI_KeypadClear = 0x47; const int kVK_ANSI_KeypadDivide = 0x4B; const int kVK_ANSI_KeypadEnter = 0x4C; const int kVK_ANSI_KeypadMinus = 0x4E; -//const int kVK_ANSI_KeypadEquals = 0x51; +const int kVK_ANSI_KeypadEquals = 0x51; const int kVK_ANSI_Keypad0 = 0x52; const int kVK_ANSI_Keypad1 = 0x53; const int kVK_ANSI_Keypad2 = 0x54; @@ -121,7 +121,7 @@ const int kVK_UpArrow = 0x7E; //const int kVK_JIS_Underscore = 0x5E; //const int kVK_JIS_KeypadComma = 0x5F; //const int kVK_JIS_Eisu = 0x66; -//const int kVK_JIS_Kana = 0x68; +const int kVK_JIS_Kana = 0x68; std::map s_KeyMap = { @@ -148,7 +148,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_4, D4}, {kVK_ANSI_6, D6}, {kVK_ANSI_5, D5}, - //{kVK_ANSI_Equal, ?}, + {kVK_ANSI_Equal, OemPlus}, {kVK_ANSI_9, D9}, {kVK_ANSI_7, D7}, {kVK_ANSI_Minus, OemMinus}, @@ -167,11 +167,11 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_Semicolon, OemSemicolon}, {kVK_ANSI_Backslash, OemBackslash}, {kVK_ANSI_Comma, OemComma}, - //{kVK_ANSI_Slash, ?}, + {kVK_ANSI_Slash, Oem2}, {kVK_ANSI_N, N}, {kVK_ANSI_M, M}, {kVK_ANSI_Period, OemPeriod}, - //{kVK_ANSI_Grave, ?}, + {kVK_ANSI_Grave, OemTilde}, {kVK_ANSI_KeypadDecimal, Decimal}, {kVK_ANSI_KeypadMultiply, Multiply}, {kVK_ANSI_KeypadPlus, OemPlus}, @@ -179,7 +179,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_KeypadDivide, Divide}, {kVK_ANSI_KeypadEnter, AvnKeyEnter}, {kVK_ANSI_KeypadMinus, OemMinus}, - //{kVK_ANSI_KeypadEquals, ?}, + {kVK_ANSI_KeypadEquals, OemPlus}, {kVK_ANSI_Keypad0, NumPad0}, {kVK_ANSI_Keypad1, NumPad1}, {kVK_ANSI_Keypad2, NumPad2}, @@ -237,5 +237,6 @@ const int kVK_UpArrow = 0x7E; {kVK_LeftArrow, Left}, {kVK_RightArrow, Right}, {kVK_DownArrow, Down}, - {kVK_UpArrow, Up} + {kVK_UpArrow, Up}, + {kVK_JIS_Kana, AvnKeyKanaMode}, }; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 84092d52eb..dd2f27116d 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -19,7 +19,7 @@ using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.MSBuild.MSBuildTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.Xunit.XunitTasks; - +using static Nuke.Common.Tools.VSWhere.VSWhereTasks; /* Before editing this file, install support plugin for your IDE, @@ -30,7 +30,26 @@ using static Nuke.Common.Tools.Xunit.XunitTasks; */ partial class Build : NukeBuild -{ +{ + static Lazy MsBuildExe = new Lazy(() => + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return null; + + var msBuildDirectory = VSWhere("-latest -nologo -property installationPath -format value -prerelease").FirstOrDefault().Text; + + if (!string.IsNullOrWhiteSpace(msBuildDirectory)) + { + string msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\Current\Bin\MSBuild.exe"); + if (!System.IO.File.Exists(msBuildExe)) + msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\15.0\Bin\MSBuild.exe"); + + return msBuildExe; + } + + return null; + }, false); + BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { @@ -70,6 +89,29 @@ partial class Build : NukeBuild } + IReadOnlyCollection MsBuildCommon( + string projectFile, + Configure configurator = null) + { + return MSBuild(projectFile, c => + { + // This is required for VS2019 image on Azure Pipelines + if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) + { + var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); + if (javaSdk != null) + c = c.AddProperty("JavaSdkDirectory", javaSdk); + } + + c = c.AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal); + c = configurator?.Invoke(c) ?? c; + return c; + }); + } Target Clean => _ => _.Executes(() => { DeleteDirectories(Parameters.BuildDirs); @@ -85,15 +127,9 @@ partial class Build : NukeBuild .DependsOn(Clean) .Executes(() => { - if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c + MsBuildCommon(Parameters.MSBuildSolution, c => c .SetArgumentConfigurator(a => a.Add("/r")) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolsVersion(MSBuildToolsVersion._15_0) .AddTargets("Build") ); @@ -219,12 +255,7 @@ partial class Build : NukeBuild { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolsVersion(MSBuildToolsVersion._15_0) + MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else DotNetPack(Parameters.MSBuildSolution, c => diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index e02acff007..2a736e4653 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -1,4 +1,4 @@ - + Exe @@ -13,6 +13,7 @@ + diff --git a/readme.md b/readme.md index cf995f10fb..ee44a0cc3f 100644 --- a/readme.md +++ b/readme.md @@ -8,9 +8,9 @@ ## About -Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (libX11), MacOS, Android (experimental) and iOS (exprerimental). +**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS. -**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development, for more details about the status see https://github.com/AvaloniaUI/Avalonia/issues/2239 +**Avalonia** is ready for **General-Purpose Desktop App Development**. However there may be some bugs and breaking changes as we continue along into this project's development. To see the status for some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). | Control catalog | Desktop platforms | Mobile platforms | |---|---|---| @@ -32,19 +32,16 @@ Install-Package Avalonia.Desktop ## Bleeding Edge Builds -Try out the latest build of Avalonia available for download here: -https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts - or use nightly build feeds as described here: https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed ## Documentation -As mentioned above, Avalonia is still in beta and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. -Contributions are always welcome! +Contributions for our docs are always welcome! ## Building and Using @@ -80,6 +77,4 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - - - + diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index d8f0f39977..bc76a39f08 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 4027c5cd63..de9ca02ed1 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Threading; using Avalonia; +using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -11,7 +13,7 @@ namespace ControlCatalog.NetCore static class Program { - static void Main(string[] args) + static int Main(string[] args) { Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) @@ -25,21 +27,21 @@ namespace ControlCatalog.NetCore } } + var builder = BuildAvaloniaApp(); if (args.Contains("--fbdev")) - AppBuilder.Configure().InitializeWithLinuxFramebuffer(tl => - { - tl.Content = new MainView(); - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - }); + { + SilenceConsole(); + return builder.StartLinuxFbDev(args); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args); + } else - BuildAvaloniaApp().Start(AppMain, args); + return builder.StartWithClassicDesktopLifetime(args); } - - static void AppMain(Application app, string[] args) - { - app.Run(new MainWindow()); - } - + /// /// This method is needed for IDE previewer infrastructure /// @@ -55,11 +57,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index d862749132..07c42c60c4 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; namespace ControlCatalog @@ -9,5 +10,15 @@ namespace ControlCatalog { AvaloniaXamlLoader.Load(this); } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + desktopLifetime.MainWindow = new MainWindow(); + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) + singleViewLifetime.MainView = new MainView(); + + base.OnFrameworkInitializationCompleted(); + } } } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..5f24c8062e 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -26,6 +26,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 0ca3567970..f90a0c4658 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -37,10 +37,6 @@ - - + + + ItemsRepeater + A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. + + + + Stack - Vertical + Stack - Horizontal + UniformGrid - Vertical + UniformGrid - Horizontal + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs new file mode 100644 index 0000000000..214de89253 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ItemsRepeaterPage : UserControl + { + private ItemsRepeater _repeater; + private ScrollViewer _scroller; + + public ItemsRepeaterPage() + { + this.InitializeComponent(); + _repeater = this.FindControl("repeater"); + _scroller = this.FindControl("scroller"); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void LayoutChanged(object sender, SelectionChangedEventArgs e) + { + if (_repeater == null) + { + return; + } + + var comboBox = (ComboBox)sender; + + switch (comboBox.SelectedIndex) + { + case 0: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical }; + break; + case 1: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal }; + break; + case 2: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Vertical, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + case 3: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Horizontal, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + } + } + } +} diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs index 0a77607719..46dbe3dcad 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs +++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Reactive; using System.Threading.Tasks; @@ -21,5 +22,18 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); } + + private MenuPageViewModel _model; + protected override void OnDataContextChanged(EventArgs e) + { + if (_model != null) + _model.View = null; + _model = DataContext as MenuPageViewModel; + if (_model != null) + _model.View = this; + + base.OnDataContextChanged(e); + } + } } diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml b/samples/ControlCatalog/Pages/TabStripPage.xaml new file mode 100644 index 0000000000..a824336f75 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml @@ -0,0 +1,33 @@ + + + TabStrip + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 0000000000..f0630cf534 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class TabStripPage : UserControl + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new[] + { + new TabStripItemViewModel + { + Header = "Item 1", + }, + new TabStripItemViewModel + { + Header = "Item 2", + }, + new TabStripItemViewModel + { + Header = "Disabled", + IsEnabled = false, + }, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private class TabStripItemViewModel + { + public string Header { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 0c0a4d705b..64118a00b4 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -26,6 +26,10 @@ + + diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3047b1e519..3513e94107 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -29,8 +29,7 @@ Name="PART_ItemsPresenter" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" - ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> (OpenRecent); MenuItems = new[] @@ -64,8 +67,11 @@ namespace ControlCatalog.ViewModels public async Task Open() { + var window = View?.GetVisualRoot() as Window; + if (window == null) + return; var dialog = new OpenFileDialog(); - var result = await dialog.ShowAsync(App.Current.MainWindow); + var result = await dialog.ShowAsync(window); if (result != null) { diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index e37b9bb5fc..d6bde0b146 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -20,8 +20,7 @@ Name="PART_ItemsPresenter" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" - ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> - \ No newline at end of file + diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index 58970eff01..12137cd03d 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -39,6 +39,8 @@ + + - + diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs index e883cdfeb9..4401a2dfeb 100644 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs @@ -10,6 +10,7 @@ namespace VirtualizationDemo.ViewModels { private string _prefix; private int _index; + private double _height = double.NaN; public ItemViewModel(int index, string prefix = "Item") { @@ -18,5 +19,11 @@ namespace VirtualizationDemo.ViewModels } public string Header => $"{_prefix} {_index}"; + + public double Height + { + get => _height; + set => this.RaiseAndSetIfChanged(ref _height, value); + } } } diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index eba17f92e4..93fe09a156 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI.Legacy; using ReactiveUI; +using Avalonia.Layout; namespace VirtualizationDemo.ViewModels { @@ -98,6 +99,24 @@ namespace VirtualizationDemo.ViewModels public ReactiveCommand SelectFirstCommand { get; private set; } public ReactiveCommand SelectLastCommand { get; private set; } + public void RandomizeSize() + { + var random = new Random(); + + foreach (var i in Items) + { + i.Height = random.Next(240) + 10; + } + } + + public void ResetSize() + { + foreach (var i in Items) + { + i.Height = double.NaN; + } + } + private void ResizeItems(int count) { if (Items == null) diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 9c3d4fb3a1..0089ea3b8d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid44 + monoandroid80 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index c2c0cd4301..1b2b205d45 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml @@ -150,6 +150,7 @@ + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..0e2f0feada 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -163,6 +163,37 @@ namespace Avalonia SetValue(property, AvaloniaProperty.UnsetValue); } + /// + /// Compares two objects using reference equality. + /// + /// The object to compare. + /// + /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: + /// + /// - AvaloniaObjects are by their nature mutable + /// - The presence of attached properties means that the semantics of equality are + /// difficult to define + /// + /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted + /// this. + /// + public sealed override bool Equals(object obj) => base.Equals(obj); + + /// + /// Gets the hash code for the object. + /// + /// + /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: + /// + /// - AvaloniaObjects are by their nature mutable + /// - The presence of attached properties means that the semantics of equality are + /// difficult to define + /// + /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted + /// this. + /// + public sealed override int GetHashCode() => base.GetHashCode(); + /// /// Gets a value. /// @@ -466,7 +497,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected void RaisePropertyChanged( + protected internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, @@ -508,45 +539,6 @@ namespace Avalonia } } - /// - /// A callback type for encapsulating complex logic for setting direct properties. - /// - /// The type of the property. - /// The value to which to set the property. - /// The backing field for the property. - /// A wrapper for the property-changed notification. - protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); - - /// - /// Sets the backing field for a direct avalonia property, raising the - /// event if the value has changed. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// A callback called to actually set the value to the backing field. - /// The value. - /// - /// True if the value changed, otherwise false. - /// - protected bool SetAndRaise( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - Contract.Requires(setterCallback != null); - return Values.Setter.SetAndNotify( - property, - ref field, - (object update, ref T backing, Action notify) => - { - setterCallback((T)update, ref backing, notify); - return true; - }, - value); - } - /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -561,32 +553,15 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - value); - } - /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) - { - var old = field; - field = value; + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetter setter = Values.GetDirectDeferredSetter(property); - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; + return setter.SetAndNotify(this, property, ref field, value); } /// diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 15de6c4d0d..44b47329ac 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -10,6 +10,8 @@ namespace Avalonia.Data { public static class BindingOperations { + public static readonly object DoNothing = new object(); + /// /// Applies an a property on an . /// diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 990a4b04f2..0ffd6a9539 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return AvaloniaProperty.UnsetValue; + return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs new file mode 100644 index 0000000000..b190f06be5 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Avalonia.Data.Converters +{ + /// + /// A multi-value converter which calls + /// + public class StringFormatMultiValueConverter : IMultiValueConverter + { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// + public StringFormatMultiValueConverter(string format, IMultiValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// + public IMultiValueConverter Inner { get; } + + /// + /// Gets the format string. + /// + public string Format { get; } + + /// + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return Inner == null + ? string.Format(culture, Format, values.ToArray()) + : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture)); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index f1717bde3b..7f8396cdfa 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -114,6 +114,11 @@ namespace Avalonia.Data.Core /// public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + using (_inner.Subscribe(_ => { })) { var type = _inner.ResultType; @@ -126,6 +131,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = TypeUtilities.Default(type); @@ -186,6 +196,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == BindingOperations.DoNothing) + { + return value; + } + var notification = value as BindingNotification; if (notification == null) @@ -196,6 +211,11 @@ namespace Avalonia.Data.Core ConverterParameter, CultureInfo.CurrentCulture); + if (converted == BindingOperations.DoNothing) + { + return converted; + } + notification = converted as BindingNotification; if (notification?.ErrorType == BindingErrorType.None) @@ -327,7 +347,18 @@ namespace Avalonia.Data.Core public void OnNext(object value) { + if (value == BindingOperations.DoNothing) + { + return; + } + var converted = _owner.ConvertValue(value); + + if (converted == BindingOperations.DoNothing) + { + return; + } + _owner._value = new WeakReference(converted); _owner.PublishNext(converted); } diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 540b1bf19b..1d6e5e59ad 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -29,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Returns deferred setter for given non-direct property. + /// + /// Property. + /// Deferred setter for given property. + DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); + /// /// Logs a binding error. /// @@ -40,7 +47,5 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); - - DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 89a893577f..4996420fe7 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -30,7 +30,9 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; + private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; + private DeferredSetter _setter; /// /// Initializes a new instance of the class. @@ -50,6 +52,7 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; + _setAndNotifyCallback = SetAndNotify; } /// @@ -242,22 +245,22 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - Owner.Setter.SetAndNotify(Property, - ref _value, - UpdateCore, - (value, priority)); - } + var newValue = (value, priority); + + if (newValue == _value) + { + return; + } - private bool UpdateCore( - object update, - ref (object value, int priority) backing, - Action notify) - => UpdateCore(((object, int))update, ref backing, notify); + if (_setter == null) + { + _setter = Owner.GetNonDirectDeferredSetter(Property); + } + + _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + } - private bool UpdateCore( - (object value, int priority) update, - ref (object value, int priority) backing, - Action notify) + private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; @@ -286,7 +289,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); + Owner?.Changed(Property, ValuePriority, old, Value); } if (notification != null) @@ -305,7 +308,6 @@ namespace Avalonia val, val?.GetType()); } - return true; } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs new file mode 100644 index 0000000000..ac128d83de --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -0,0 +1,150 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore + { + private Entry[] _entries; + + public AvaloniaPropertyValueStore() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValue(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValue(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,168 +1,122 @@ -using System; -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; namespace Avalonia.Utilities { + /// + /// Callback invoked when deferred setter wants to set a value. + /// + /// Value type. + /// Property being set. + /// Backing field reference. + /// New value. + internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); + /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. /// /// The type of value with which to track the delayed assignment. - class DeferredSetter + internal sealed class DeferredSetter { - private struct NotifyDisposable : IDisposable + private readonly SingleOrQueue _pendingValues; + private bool _isNotifying; + + public DeferredSetter() { - private readonly SettingStatus status; + _pendingValues = new SingleOrQueue(); + } - internal NotifyDisposable(SettingStatus status) - { - this.status = status; - status.Notifying = true; - } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; - public void Dispose() - { - status.Notifying = false; - } + backing = value; + + source.RaisePropertyChanged(property, old, value); } - /// - /// Information on current setting/notification status of a property. - /// - private class SettingStatus + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value) { - public bool Notifying { get; set; } - - private SingleOrQueue pendingValues; - - public SingleOrQueue PendingValues + if (!_isNotifying) { - get + using (new NotifyDisposable(this)) { - return pendingValues ?? (pendingValues = new SingleOrQueue()); + SetAndRaisePropertyChanged(source, property, ref backing, value); } - } - } - private Dictionary _setRecords; - private Dictionary SetRecords - => _setRecords ?? (_setRecords = new Dictionary()); + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } - private SettingStatus GetOrCreateStatus(AvaloniaProperty property) - { - if (!SetRecords.TryGetValue(property, out var status)) - { - status = new SettingStatus(); - SetRecords.Add(property, status); + return true; } - return status; + _pendingValues.Enqueue(value); + + return false; } - /// - /// Mark the property as currently notifying. - /// - /// The property to mark as notifying. - /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(AvaloniaProperty property) + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } - /// - /// Check if the property is currently notifying listeners. - /// - /// The property. - /// If the property is currently notifying listeners. - private bool IsNotifying(AvaloniaProperty property) - => SetRecords.TryGetValue(property, out var value) && value.Notifying; + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); + return true; + } - GetOrCreateStatus(property).PendingValues.Enqueue(value); - } + _pendingValues.Enqueue(value); - /// - /// Checks if there are any pending assignments for the property. - /// - /// The property to check. - /// If the property has any pending assignments. - private bool HasPendingSet(AvaloniaProperty property) - { - return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; + return false; } /// - /// Gets the first pending assignment for the property. + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. /// - /// The property to check. - /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(AvaloniaProperty property) + private readonly struct NotifyDisposable : IDisposable { - return GetOrCreateStatus(property).PendingValues.Dequeue(); - } - - public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + private readonly DeferredSetter _setter; - /// - /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 - /// - /// The property to set. - /// The backing field for the property - /// - /// A callback that actually sets the property. - /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. - /// - /// The value to try to set. - public bool SetAndNotify( - AvaloniaProperty property, - ref TValue backing, - SetterDelegate setterCallback, - TSetRecord value) - { - Contract.Requires(setterCallback != null); - if (!IsNotifying(property)) + internal NotifyDisposable(DeferredSetter setter) { - bool updated = false; - if (!object.Equals(value, backing)) - { - updated = setterCallback(value, ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - while (HasPendingSet(property)) - { - updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - - return updated; + _setter = setter; + _setter._isNotifying = true; } - else if(!object.Equals(value, backing)) + + public void Dispose() { - AddPendingSet(property, value); + _setter._isNotifying = false; } - return false; } } } diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs new file mode 100644 index 0000000000..f321b2b2f1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// A task-like operation that is guaranteed to finish continuations synchronously, + /// can be used for parametrized one-shot events + /// + public struct SynchronousCompletionAsyncResult : INotifyCompletion + { + private readonly SynchronousCompletionAsyncResultSource _source; + private readonly T _result; + private readonly bool _isValid; + internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource source) + { + _source = source; + _result = default; + _isValid = true; + } + + public SynchronousCompletionAsyncResult(T result) + { + _result = result; + _source = null; + _isValid = true; + } + + static void ThrowNotInitialized() => + throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized"); + + public bool IsCompleted + { + get + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null || _source.IsCompleted; + } + } + + public T GetResult() + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null ? _result : _source.Result; + } + + + public void OnCompleted(Action continuation) + { + if (!_isValid) + ThrowNotInitialized(); + if (_source == null) + continuation(); + else + _source.OnCompleted(continuation); + } + + public SynchronousCompletionAsyncResult GetAwaiter() => this; + } + + /// + /// Source for incomplete SynchronousCompletionAsyncResult + /// + /// + public class SynchronousCompletionAsyncResultSource + { + private T _result; + internal bool IsCompleted { get; private set; } + public SynchronousCompletionAsyncResult AsyncResult => new SynchronousCompletionAsyncResult(this); + + internal T Result => IsCompleted ? + _result : + throw new InvalidOperationException("Asynchronous operation is not yet completed"); + + private List _continuations; + + internal void OnCompleted(Action continuation) + { + if(_continuations==null) + _continuations = new List(); + _continuations.Add(continuation); + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("Asynchronous operation is already completed"); + _result = result; + IsCompleted = true; + if(_continuations!=null) + foreach (var c in _continuations) + c(); + _continuations = null; + } + + public void TrySetResult(T result) + { + if(IsCompleted) + return; + SetResult(result); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..1bdbd4ca7c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValue(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,28 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { - get + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); + return (DeferredSetter)deferredSetter; } - } - private bool TryGetValue(AvaloniaProperty property, out object value) - { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) - { - value = null; - return false; - } - - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetter(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValue(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) { - _entries[TryFindEntry(property.Id).Item1].Value = value; + return GetDeferredSetter(property); } - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else - { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); - } - - return (0, false); + return GetDeferredSetter(property); } } } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index c2d0950264..d356b15408 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.IO; +using System.Linq; using Microsoft.Build.Framework; namespace Avalonia.Build.Tasks @@ -11,8 +12,14 @@ namespace Avalonia.Build.Tasks { if (args.Length != 3) { - Console.Error.WriteLine("input references output"); - return 1; + if (args.Length == 1) + args = new[] {"original.dll", "references", "out.dll"} + .Select(x => Path.Combine(args[0], x)).ToArray(); + else + { + Console.Error.WriteLine("input references output"); + return 1; + } } return new CompileAvaloniaXamlTask() diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index c54d8e19a1..0dd52c9b48 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -75,9 +75,9 @@ namespace Avalonia.Build.Tasks .First(c => c.Parameters.Count == 1)); var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); - var rootServiceProviderField = asm.MainModule.ImportReference( - typeSystem.GetTypeReference(runtimeHelpers).Resolve().Fields - .First(x => x.Name == "RootServiceProviderV1")); + var createRootServiceProviderMethod = asm.MainModule.ImportReference( + typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods + .First(x => x.Name == "CreateRootServiceProviderV2")); var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); @@ -211,7 +211,7 @@ namespace Avalonia.Build.Tasks trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); classTypeDefinition.Methods.Add(trampoline); - var regularStart = Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField); + var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart)); @@ -307,7 +307,7 @@ namespace Avalonia.Build.Tasks i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor)); else { - i.Add(Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField)); + i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod)); } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index bcd12fbfbb..490a724eda 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,6 +24,7 @@ using System.Linq; using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 2889a5c77c..eaa267ba66 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -195,7 +195,6 @@ - @@ -230,4 +229,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 419064b051..59ff35be76 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -4,6 +4,7 @@ using System; using System.Reflection; using System.Linq; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; namespace Avalonia.Controls @@ -57,10 +58,6 @@ namespace Avalonia.Controls /// public Action AfterSetupCallback { get; private set; } = builder => { }; - /// - /// Gets or sets a method to call before Start is called on the . - /// - public Action BeforeStartCallback { get; private set; } = builder => { }; protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { @@ -95,17 +92,6 @@ namespace Avalonia.Controls protected TAppBuilder Self => (TAppBuilder)this; - /// - /// Registers a callback to call before Start is called on the . - /// - /// The callback. - /// An instance. - public TAppBuilder BeforeStarting(Action callback) - { - BeforeStartCallback = (Action)Delegate.Combine(BeforeStartCallback, callback); - return Self; - } - public TAppBuilder AfterSetup(Action callback) { AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); @@ -117,48 +103,37 @@ namespace Avalonia.Controls /// /// The window type. /// A delegate that will be called to create a data context for the window (optional). + [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")] public void Start(Func dataContextProvider = null) where TMainWindow : Window, new() { - Setup(); - BeforeStartCallback(Self); - - var window = new TMainWindow(); - if (dataContextProvider != null) - window.DataContext = dataContextProvider(); - Instance.Run(window); - } - - /// - /// Starts the application with the provided instance of . - /// - /// The window type. - /// Instance of type TMainWindow to use when starting the app - /// A delegate that will be called to create a data context for the window (optional). - public void Start(TMainWindow mainWindow, Func dataContextProvider = null) - where TMainWindow : Window - { - Setup(); - BeforeStartCallback(Self); - - if (dataContextProvider != null) - mainWindow.DataContext = dataContextProvider(); - Instance.Run(mainWindow); + AfterSetup(builder => + { + var window = new TMainWindow(); + if (dataContextProvider != null) + window.DataContext = dataContextProvider(); + ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime) + .MainWindow = window; + }); + + // Copy-pasted because we can't call extension methods due to generic constraints + var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose}; + Instance.ApplicationLifetime = lifetime; + SetupWithoutStarting(); + lifetime.Start(Array.Empty()); } public delegate void AppMainDelegate(Application app, string[] args); + [Obsolete("Use either lifetimes or AppMain overload. See see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)] public void Start() { - Setup(); - BeforeStartCallback(Self); - Instance.Run(); + throw new NotSupportedException(); } public void Start(AppMainDelegate main, string[] args) { Setup(); - BeforeStartCallback(Self); main(Instance, args); } @@ -224,17 +199,6 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); - /// - /// Sets the shutdown mode of the application. - /// - /// The shutdown mode. - /// - public TAppBuilder SetShutdownMode(ShutdownMode shutdownMode) - { - Instance.ShutdownMode = shutdownMode; - return Self; - } - protected virtual bool CheckSetup => true; private void SetupAvaloniaModules() @@ -313,6 +277,7 @@ namespace Avalonia.Controls Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); + Instance.OnFrameworkInitializationCompleted(); } } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 9f9fe5eae1..382106de65 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -6,6 +6,7 @@ using System.Reactive.Concurrency; using System.Threading; using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; @@ -31,7 +32,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode + public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode { /// /// The application-global data templates. @@ -43,22 +44,6 @@ namespace Avalonia private readonly Styler _styler = new Styler(); private Styles _styles; private IResourceDictionary _resources; - private CancellationTokenSource _mainLoopCancellationTokenSource; - private int _exitCode; - - /// - /// Initializes a new instance of the class. - /// - public Application() - { - Windows = new WindowCollection(this); - } - - /// - public event EventHandler Startup; - - /// - public event EventHandler Exit; /// public event EventHandler ResourcesChanged; @@ -166,198 +151,21 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; - - /// - /// Gets or sets the . This property indicates whether the application is shutdown explicitly or implicitly. - /// If is set to OnExplicitShutdown the application is only closes if Shutdown is called. - /// The default is OnLastWindowClose - /// - /// - /// The shutdown mode. - /// - public ShutdownMode ShutdownMode { get; set; } - - /// - /// Gets or sets the main window of the application. - /// - /// - /// The main window. - /// - public Window MainWindow { get; set; } - - /// - /// Gets the open windows of the application. - /// - /// - /// The windows. - /// - public WindowCollection Windows { get; } - + /// - /// Gets or sets a value indicating whether this instance is shutting down. + /// Application lifetime, use it for things like setting the main window and exiting the app from code + /// Currently supported lifetimes are: + /// - + /// - + /// - /// - /// - /// true if this instance is shutting down; otherwise, false. - /// - internal bool IsShuttingDown { get; private set; } + public IApplicationLifetime ApplicationLifetime { get; set; } /// /// Initializes the application by loading XAML etc. /// public virtual void Initialize() { } - /// - /// Runs the application's main loop. - /// - /// - /// This will return when the condition is met - /// or was called. - /// - /// The application's exit code that is returned to the operating system on termination. - public int Run() - { - return Run(new CancellationTokenSource()); - } - - /// - /// Runs the application's main loop. - /// - /// - /// This will return when the condition is met - /// or was called. - /// This also returns when is closed. - /// - /// The closable to track. - /// The application's exit code that is returned to the operating system on termination. - public int Run(ICloseable closable) - { - closable.Closed += (s, e) => _mainLoopCancellationTokenSource?.Cancel(); - - return Run(new CancellationTokenSource()); - } - - /// - /// Runs the application's main loop. - /// - /// - /// This will return when the condition is met - /// or was called. - /// - /// The window that is used as - /// when the isn't already set. - /// The application's exit code that is returned to the operating system on termination. - public int Run(Window mainWindow) - { - if (mainWindow == null) - { - throw new ArgumentNullException(nameof(mainWindow)); - } - - if (MainWindow == null) - { - if (!mainWindow.IsVisible) - { - mainWindow.Show(); - } - - MainWindow = mainWindow; - } - - return Run(new CancellationTokenSource()); - } - /// - /// Runs the application's main loop. - /// - /// - /// This will return when the condition is met - /// or was called. - /// This also returns when the is canceled. - /// - /// The application's exit code that is returned to the operating system on termination. - /// The token to track. - public int Run(CancellationToken token) - { - return Run(CancellationTokenSource.CreateLinkedTokenSource(token)); - } - - private int Run(CancellationTokenSource tokenSource) - { - if (IsShuttingDown) - { - throw new InvalidOperationException("Application is shutting down."); - } - - if (_mainLoopCancellationTokenSource != null) - { - throw new InvalidOperationException("Application is already running."); - } - - _mainLoopCancellationTokenSource = tokenSource; - - Dispatcher.UIThread.Post(() => OnStartup(new StartupEventArgs()), DispatcherPriority.Send); - - Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); - - if (!IsShuttingDown) - { - Shutdown(_exitCode); - } - - return _exitCode; - } - - /// - /// Raises the event. - /// - /// A that contains the event data. - protected virtual void OnStartup(StartupEventArgs e) - { - Startup?.Invoke(this, e); - } - - /// - /// Raises the event. - /// - /// A that contains the event data. - protected virtual void OnExit(ExitEventArgs e) - { - Exit?.Invoke(this, e); - } - - /// - public void Shutdown(int exitCode = 0) - { - if (IsShuttingDown) - { - throw new InvalidOperationException("Application is already shutting down."); - } - - _exitCode = exitCode; - - IsShuttingDown = true; - - Windows.Clear(); - - try - { - var e = new ExitEventArgs { ApplicationExitCode = _exitCode }; - - OnExit(e); - - _exitCode = e.ApplicationExitCode; - } - finally - { - _mainLoopCancellationTokenSource?.Cancel(); - - _mainLoopCancellationTokenSource = null; - - IsShuttingDown = false; - - Environment.ExitCode = _exitCode; - } - } - /// bool IResourceProvider.TryGetResource(object key, out object value) { @@ -383,7 +191,6 @@ namespace Avalonia .Bind().ToConstant(InputManager) .Bind().ToTransient() .Bind().ToConstant(_styler) - .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance) .Bind().ToConstant(DragDropDevice.Instance) .Bind().ToTransient(); @@ -394,6 +201,11 @@ namespace Avalonia .GetService()?.Add(clock); } + public virtual void OnFrameworkInitializationCompleted() + { + + } + private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs new file mode 100644 index 0000000000..abca7a64ee --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.ApplicationLifetimes +{ + public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable + { + private readonly Application _app; + private int _exitCode; + private CancellationTokenSource _cts; + private bool _isShuttingDown; + private HashSet _windows = new HashSet(); + + private static ClassicDesktopStyleApplicationLifetime _activeLifetime; + static ClassicDesktopStyleApplicationLifetime() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); + Window.WindowClosedEvent.AddClassHandler(typeof(Window), WindowClosedEvent); + } + + private static void WindowClosedEvent(object sender, RoutedEventArgs e) + { + _activeLifetime?._windows.Remove((Window)sender); + _activeLifetime?.HandleWindowClosed((Window)sender); + } + + private static void OnWindowOpened(object sender, RoutedEventArgs e) + { + _activeLifetime?._windows.Add((Window)sender); + } + + public ClassicDesktopStyleApplicationLifetime(Application app) + { + if (_activeLifetime != null) + throw new InvalidOperationException( + "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); + _app = app; + _activeLifetime = this; + } + + /// + public event EventHandler Startup; + /// + public event EventHandler Exit; + + /// + public ShutdownMode ShutdownMode { get; set; } + + /// + public Window MainWindow { get; set; } + + public IReadOnlyList Windows => _windows.ToList(); + + private void HandleWindowClosed(Window window) + { + if (window == null) + return; + + if (_isShuttingDown) + return; + + if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) + Shutdown(); + else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) + Shutdown(); + } + + + + + public void Shutdown(int exitCode = 0) + { + if (_isShuttingDown) + throw new InvalidOperationException("Application is already shutting down."); + + _exitCode = exitCode; + _isShuttingDown = true; + + try + { + foreach (var w in Windows) + w.Close(); + var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); + Exit?.Invoke(this, e); + _exitCode = e.ApplicationExitCode; + } + finally + { + _cts?.Cancel(); + _cts = null; + _isShuttingDown = false; + } + } + + + public int Start(string[] args) + { + Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); + _cts = new CancellationTokenSource(); + MainWindow?.Show(); + _app.Run(_cts.Token); + Environment.ExitCode = _exitCode; + return _exitCode; + } + + public void Dispose() + { + if (_activeLifetime == this) + _activeLifetime = null; + } + } +} + +namespace Avalonia +{ + public static class ClassicDesktopStyleApplicationLifetimeExtensions + { + public static int StartWithClassicDesktopLifetime( + this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + where T : AppBuilderBase, new() + { + var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; + builder.Instance.ApplicationLifetime = lifetime; + builder.SetupWithoutStarting(); + return lifetime.Start(args); + } + } +} diff --git a/src/Avalonia.Controls/ExitEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs similarity index 54% rename from src/Avalonia.Controls/ExitEventArgs.cs rename to src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs index c99f7fe047..2963c019cc 100644 --- a/src/Avalonia.Controls/ExitEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ControlledApplicationLifetimeExitEventArgs.cs @@ -3,13 +3,18 @@ using System; -namespace Avalonia.Controls +namespace Avalonia.Controls.ApplicationLifetimes { /// - /// Contains the arguments for the event. + /// Contains the arguments for the event. /// - public class ExitEventArgs : EventArgs + public class ControlledApplicationLifetimeExitEventArgs : EventArgs { + public ControlledApplicationLifetimeExitEventArgs(int applicationExitCode) + { + ApplicationExitCode = applicationExitCode; + } + /// /// Gets or sets the exit code that an application returns to the operating system when the application exits. /// diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs new file mode 100644 index 0000000000..9860d0cb38 --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/IApplicationLifetime.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls.ApplicationLifetimes +{ + public interface IApplicationLifetime + { + + } +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs new file mode 100644 index 0000000000..a1006d907b --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Controls.ApplicationLifetimes +{ + /// + /// Controls application lifetime in classic desktop style + /// + public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime + { + /// + /// Gets or sets the . This property indicates whether the application is shutdown explicitly or implicitly. + /// If is set to OnExplicitShutdown the application is only closes if Shutdown is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + ShutdownMode ShutdownMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + Window MainWindow { get; set; } + + IReadOnlyList Windows { get; } + } +} diff --git a/src/Avalonia.Controls/IApplicationLifecycle.cs b/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs similarity index 65% rename from src/Avalonia.Controls/IApplicationLifecycle.cs rename to src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs index a3c6599b20..3f61aeb536 100644 --- a/src/Avalonia.Controls/IApplicationLifecycle.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IControlledApplicationLifetime.cs @@ -1,22 +1,19 @@ using System; -namespace Avalonia.Controls +namespace Avalonia.Controls.ApplicationLifetimes { - /// - /// Sends events about the application lifecycle. - /// - public interface IApplicationLifecycle + public interface IControlledApplicationLifetime : IApplicationLifetime { /// /// Sent when the application is starting up. /// - event EventHandler Startup; + event EventHandler Startup; /// /// Sent when the application is exiting. /// - event EventHandler Exit; - + event EventHandler Exit; + /// /// Shuts down the application and sets the exit code that is returned to the operating system when the application exits. /// diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs new file mode 100644 index 0000000000..eb451f51af --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/ISingleViewApplicationLifetime.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls.ApplicationLifetimes +{ + public interface ISingleViewApplicationLifetime : IApplicationLifetime + { + Control MainView { get; set; } + } +} diff --git a/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs new file mode 100644 index 0000000000..423832793e --- /dev/null +++ b/src/Avalonia.Controls/ApplicationLifetimes/StartupEventArgs.cs @@ -0,0 +1,22 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Controls.ApplicationLifetimes +{ + /// + /// Contains the arguments for the event. + /// + public class ControlledApplicationLifetimeStartupEventArgs : EventArgs + { + public ControlledApplicationLifetimeStartupEventArgs(IEnumerable args) + { + Args = args?.ToArray() ?? Array.Empty(); + } + + public string[] Args { get; } + } +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b87e10d284..1e2fc9f9d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -345,7 +345,6 @@ namespace Avalonia.Controls /// private IDisposable _collectionChangeSubscription; - private IMemberSelector _valueMemberSelector; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; @@ -541,12 +540,6 @@ namespace Avalonia.Controls o => o.Items, (o, v) => o.Items = v); - public static readonly DirectProperty ValueMemberSelectorProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueMemberSelector), - o => o.ValueMemberSelector, - (o, v) => o.ValueMemberSelector = v); - public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), @@ -795,7 +788,7 @@ namespace Avalonia.Controls var template = new FuncDataTemplate( typeof(object), - o => + (o, _) => { var control = new ContentControl(); control.Bind(ContentControl.ContentProperty, value); @@ -958,20 +951,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the MemberSelector that is used to get values for - /// display in the text portion of the - /// control. - /// - /// The MemberSelector that is used to get values for display in - /// the text portion of the - /// control. - public IMemberSelector ValueMemberSelector - { - get { return _valueMemberSelector; } - set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } - } - /// /// Gets or sets the selected item in the drop-down. /// @@ -1841,11 +1820,6 @@ namespace Avalonia.Controls return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; } - if (_valueMemberSelector != null) - { - value = _valueMemberSelector.Select(value); - } - return value == null ? String.Empty : value.ToString(); } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 70f26288af..b09d3bddff 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -33,8 +33,6 @@ namespace Avalonia.Controls /// public class Button : ContentControl { - private ICommand _command; - /// /// Defines the property. /// @@ -75,6 +73,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); + private ICommand _command; + private bool _commandCanExecute = true; + /// /// Initializes static members of the class. /// @@ -147,6 +148,8 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -292,7 +295,11 @@ namespace Avalonia.Controls { if (status?.ErrorType == BindingErrorType.Error) { - IsEnabled = false; + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } } } } @@ -351,9 +358,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 8232697c18..395196d926 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -4,6 +4,7 @@ // All other rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Avalonia.Data; @@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives { if (MonthView != null) { + var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth; + var children = new List(childCount); + for (int i = 0; i < Calendar.RowsPerMonth; i++) { if (_dayTitleTemplate != null) @@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); - MonthView.Children.Add(cell); + children.Add(cell); } } @@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives cell.PointerEnter += Cell_MouseEnter; cell.PointerLeave += Cell_MouseLeave; cell.Click += Cell_Click; - MonthView.Children.Add(cell); + children.Add(cell); } } + + MonthView.Children.AddRange(children); } if (YearView != null) { + var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear; + var children = new List(childCount); + CalendarButton month; for (int i = 0; i < Calendar.RowsPerYear; i++) { @@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; month.PointerEnter += Month_MouseEnter; month.PointerLeave += Month_MouseLeave; - YearView.Children.Add(month); + children.Add(month); } } + + YearView.Children.AddRange(children); } } diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index e3d2489241..9c520c434e 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -62,8 +62,15 @@ namespace Avalonia.Controls /// public double MaxWidth { - get { return GetValue(MaxWidthProperty); } - set { SetValue(MaxWidthProperty, value); } + get + { + return GetValue(MaxWidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MaxWidthProperty, value); + } } /// @@ -71,8 +78,15 @@ namespace Avalonia.Controls /// public double MinWidth { - get { return GetValue(MinWidthProperty); } - set { SetValue(MinWidthProperty, value); } + get + { + return GetValue(MinWidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MinWidthProperty, value); + } } /// @@ -80,8 +94,15 @@ namespace Avalonia.Controls /// public GridLength Width { - get { return GetValue(WidthProperty); } - set { SetValue(WidthProperty, value); } + get + { + return GetValue(WidthProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(WidthProperty, value); + } } internal override GridLength UserSizeValueCache => this.Width; diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index 4f5bbf3bc3..d4fc2ee0a1 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -16,10 +16,8 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinitions() + public ColumnDefinitions() : base () { - ResetBehavior = ResetBehavior.Remove; - CollectionChanged += OnCollectionChanged; } /// diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index bf79e192c5..f32b8fabc6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls } } - private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible; private void UpdateSelectionBoxItem(object item) { @@ -333,8 +333,7 @@ namespace Avalonia.Controls } else { - var selector = MemberSelector; - SelectionBoxItem = selector != null ? selector.Select(item) : item; + SelectionBoxItem = item; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..d0804107b3 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,12 +1,13 @@ using System; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +91,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +113,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +128,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +144,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +172,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 8899c38bf9..a68fe1265f 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -20,49 +20,15 @@ namespace Avalonia.Controls /// public abstract class DefinitionBase : AvaloniaObject { - //------------------------------------------------------ - // - // Constructors - // - //------------------------------------------------------ - - #region Constructors - - /* internal DefinitionBase(bool isColumnDefinition) - { - _isColumnDefinition = isColumnDefinition; - _parentIndex = -1; - }*/ - - #endregion Constructors - - //------------------------------------------------------ - // - // Public Properties - // - //------------------------------------------------------ - - #region Public Properties - /// /// SharedSizeGroup property. /// public string SharedSizeGroup { - get { return (string) GetValue(SharedSizeGroupProperty); } + get { return (string)GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } - #endregion Public Properties - - //------------------------------------------------------ - // - // Internal Methods - // - //------------------------------------------------------ - - #region Internal Methods - /// /// Callback to notify about entering model tree. /// @@ -86,15 +52,6 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.PropertyType == typeof(GridLength) - || e.Property.PropertyType == typeof(double)) - OnUserSizePropertyChanged(e); - - base.OnPropertyChanged(e); - } - /// /// Callback to notify about exitting model tree. /// @@ -139,119 +96,14 @@ namespace Avalonia.Controls _minSize = minSize; } - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal void OnUserSizePropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (InParentLogicalTree) - { - if (_sharedState != null) - { - _sharedState.Invalidate(); - } - else - { - if (((GridLength)e.OldValue).GridUnitType != ((GridLength)e.NewValue).GridUnitType) - { - Parent.Invalidate(); - } - else - { - Parent.InvalidateMeasure(); - } - } - } - } - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal static bool IsUserSizePropertyValueValid(object value) - { - return (((GridLength)value).Value >= 0); - } - - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal static void OnUserMinSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - DefinitionBase definition = (DefinitionBase) d; - - if (definition.InParentLogicalTree) - { - Grid parentGrid = (Grid) definition.Parent; - parentGrid.InvalidateMeasure(); - } - } - - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal static bool IsUserMinSizePropertyValueValid(object value) - { - double v = (double)value; - return (!double.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); - } - - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal static void OnUserMaxSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - DefinitionBase definition = (DefinitionBase) d; - - if (definition.InParentLogicalTree) - { - Grid parentGrid = (Grid) definition.Parent; - parentGrid.InvalidateMeasure(); - } - } - - /// - /// - /// - /// - /// This method needs to be internal to be accessable from derived classes. - /// - internal static bool IsUserMaxSizePropertyValueValid(object value) - { - double v = (double)value; - return (!double.IsNaN(v) && v >= 0.0d); - } - - /// - /// - /// /// /// This method reflects Grid.SharedScopeProperty state by setting / clearing /// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty /// is a collection of SharedSizeState objects for the scope. - /// Also PrivateSharedSizeScopeProperty is FrameworkPropertyMetadataOptions.Inherits property. So that all children - /// elements belonging to a certain scope can easily access SharedSizeState collection. As well - /// as been norified about enter / exit a scope. /// internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - // is it possible to optimize here something like this: - // if ((bool)d.GetValue(Grid.IsSharedSizeScopeProperty) == (d.GetLocalValue(PrivateSharedSizeScopeProperty) != null) - // { /* do nothing */ } - if ((bool) e.NewValue) + if ((bool)e.NewValue) { SharedSizeScope sharedStatesCollection = new SharedSizeScope(); d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); @@ -262,16 +114,6 @@ namespace Avalonia.Controls } } - #endregion Internal Methods - - //------------------------------------------------------ - // - // Internal Properties - // - //------------------------------------------------------ - - #region Internal Properties - /// /// Returns true if this definition is a part of shared group. /// @@ -349,8 +191,8 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if ( _sizeType != Grid.LayoutTimeSizeType.Auto - && preferredSize < _measureSize ) + if (_sizeType != Grid.LayoutTimeSizeType.Auto + && preferredSize < _measureSize) { preferredSize = _measureSize; } @@ -375,9 +217,9 @@ namespace Avalonia.Controls get { double minSize = _minSize; - if ( UseSharedMinimum - && _sharedState != null - && minSize < _sharedState.MinSize ) + if (UseSharedMinimum + && _sharedState != null + && minSize < _sharedState.MinSize) { minSize = _sharedState.MinSize; } @@ -393,9 +235,9 @@ namespace Avalonia.Controls get { double minSize = _minSize; - if ( _sharedState != null - && (UseSharedMinimum || !LayoutWasUpdated) - && minSize < _sharedState.MinSize ) + if (_sharedState != null + && (UseSharedMinimum || !LayoutWasUpdated) + && minSize < _sharedState.MinSize) { minSize = _sharedState.MinSize; } @@ -426,27 +268,9 @@ namespace Avalonia.Controls /// Internal helper to access up-to-date UserMaxSize property value. /// internal abstract double UserMaxSizeValueCache { get; } - - /// - /// Protected. Returns true if this DefinitionBase instance is in parent's logical tree. - /// - internal bool InParentLogicalTree - { - get { return (_parentIndex != -1); } - } internal Grid Parent { get; set; } - #endregion Internal Properties - - //------------------------------------------------------ - // - // Private Methods - // - //------------------------------------------------------ - - #region Private Methods - /// /// SetFlags is used to set or unset one or multiple /// flags on the object. @@ -465,16 +289,13 @@ namespace Avalonia.Controls return ((_flags & flags) == flags); } - /// - /// - /// private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase) d; - - if (definition.InParentLogicalTree) + DefinitionBase definition = (DefinitionBase)d; + + if (definition.Parent != null) { - string sharedSizeGroupId = (string) e.NewValue; + string sharedSizeGroupId = (string)e.NewValue; if (definition._sharedState != null) { @@ -483,7 +304,7 @@ namespace Avalonia.Controls definition._sharedState.RemoveMember(definition); definition._sharedState = null; } - + if ((definition._sharedState == null) && (sharedSizeGroupId != null)) { SharedSizeScope privateSharedSizeScope = definition.PrivateSharedSizeScope; @@ -498,9 +319,6 @@ namespace Avalonia.Controls } } - /// - /// - /// /// /// Verifies that Shared Size Group Property string /// a) not empty. @@ -520,10 +338,10 @@ namespace Avalonia.Controls { bool isDigit = Char.IsDigit(id[i]); - if ( (i == 0 && isDigit) - || !( isDigit - || Char.IsLetter(id[i]) - || '_' == id[i] ) ) + if ((i == 0 && isDigit) + || !(isDigit + || Char.IsLetter(id[i]) + || '_' == id[i])) { break; } @@ -535,12 +353,9 @@ namespace Avalonia.Controls } } - throw new ArgumentException("Invalid SharedSizeGroup string."); + throw new ArgumentException("Invalid SharedSizeGroup string."); } - /// - /// - /// /// /// OnPrivateSharedSizeScopePropertyChanged is called when new scope enters or /// existing scope just left. In both cases if the DefinitionBase object is already registered @@ -550,9 +365,9 @@ namespace Avalonia.Controls { DefinitionBase definition = (DefinitionBase)d; - if (definition.InParentLogicalTree) + if (definition.Parent != null) { - SharedSizeScope privateSharedSizeScope = (SharedSizeScope) e.NewValue; + SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue; if (definition._sharedState != null) { @@ -576,22 +391,12 @@ namespace Avalonia.Controls } } - #endregion Private Methods - - //------------------------------------------------------ - // - // Private Properties - // - //------------------------------------------------------ - - #region Private Properties - /// /// Private getter of shared state collection dynamic property. /// private SharedSizeScope PrivateSharedSizeScope { - get { return (SharedSizeScope) GetValue(PrivateSharedSizeScopeProperty); } + get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); } } /// @@ -612,16 +417,6 @@ namespace Avalonia.Controls set { SetFlags(value, Flags.LayoutWasUpdated); } } - #endregion Private Properties - - //------------------------------------------------------ - // - // Private Fields - // - //------------------------------------------------------ - - #region Private Fields - private readonly bool _isColumnDefinition; // when "true", this is a ColumnDefinition; when "false" this is a RowDefinition (faster than a type check) private Flags _flags; // flags reflecting various aspects of internal state internal int _parentIndex = -1; // this instance's index in parent's children collection @@ -633,19 +428,6 @@ namespace Avalonia.Controls private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) private SharedSizeState _sharedState; // reference to shared state object this instance is registered with - - internal const bool ThisIsColumnDefinition = true; - internal const bool ThisIsRowDefinition = false; - - #endregion Private Fields - - //------------------------------------------------------ - // - // Private Structures / Classes - // - //------------------------------------------------------ - - #region Private Structures Classes [System.Flags] private enum Flags : byte @@ -653,8 +435,8 @@ namespace Avalonia.Controls // // bool flags // - UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum - LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured + UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum + LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured } /// @@ -799,8 +581,8 @@ namespace Avalonia.Controls for (int i = 0, count = _registry.Count; i < count; ++i) { - Debug.Assert( _userSize.GridUnitType == GridUnitType.Auto - || _userSize.GridUnitType == GridUnitType.Pixel ); + Debug.Assert(_userSize.GridUnitType == GridUnitType.Auto + || _userSize.GridUnitType == GridUnitType.Pixel); GridLength currentGridLength = _registry[i].UserSizeValueCache; if (currentGridLength.GridUnitType == GridUnitType.Pixel) @@ -845,7 +627,7 @@ namespace Avalonia.Controls { DefinitionBase definitionBase = _registry[i]; - if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) + if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) { // if definition's min size is different, then need to re-measure if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) @@ -857,7 +639,7 @@ namespace Avalonia.Controls else { definitionBase.UseSharedMinimum = false; - + // if measure is valid then also need to check arrange. // Note: definitionBase.SizeCache is volatile but at this point // it contains up-to-date final size @@ -879,27 +661,34 @@ namespace Avalonia.Controls _broadcastInvalidation = true; } - - private readonly SharedSizeScope _sharedSizeScope; // the scope this state belongs to - private readonly string _sharedSizeGroupId; // Id of the shared size group this object is servicing - private readonly List _registry; // registry of participating definitions - private readonly EventHandler _layoutUpdated; // instance event handler for layout updated event - private Control _layoutUpdatedHost; // Control for which layout updated event handler is registered - private bool _broadcastInvalidation; // "true" when broadcasting of invalidation is needed - private bool _userSizeValid; // "true" when _userSize is up to date - private GridLength _userSize; // shared state - private double _minSize; // shared state - } - #endregion Private Structures Classes + // the scope this state belongs to + private readonly SharedSizeScope _sharedSizeScope; + + // Id of the shared size group this object is servicing + private readonly string _sharedSizeGroupId; + + // Registry of participating definitions + private readonly List _registry; - //------------------------------------------------------ - // - // Properties - // - //------------------------------------------------------ + // Instance event handler for layout updated event + private readonly EventHandler _layoutUpdated; - #region Properties + // Control for which layout updated event handler is registered + private Control _layoutUpdatedHost; + + // "true" when broadcasting of invalidation is needed + private bool _broadcastInvalidation; + + // "true" when _userSize is up to date + private bool _userSizeValid; + + // shared state + private GridLength _userSize; + + // shared state + private double _minSize; + } /// /// Private shared size scope property holds a collection of shared state objects for the a given shared size scope. @@ -931,7 +720,7 @@ namespace Avalonia.Controls public static readonly AttachedProperty SharedSizeGroupProperty = AvaloniaProperty.RegisterAttached( "SharedSizeGroup", - validate:SharedSizeGroupPropertyValueValid); + validate: SharedSizeGroupPropertyValueValid); /// /// Static ctor. Used for static registration of properties. @@ -941,7 +730,5 @@ namespace Avalonia.Controls SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } - - #endregion Properties } } diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs index b36ca9ce8a..97d92bdcb7 100644 --- a/src/Avalonia.Controls/DefinitionList.cs +++ b/src/Avalonia.Controls/DefinitionList.cs @@ -10,6 +10,12 @@ namespace Avalonia.Controls { public abstract class DefinitionList : AvaloniaList where T : DefinitionBase { + public DefinitionList() + { + ResetBehavior = ResetBehavior.Remove; + CollectionChanged += OnCollectionChanged; + } + internal bool IsDirty = true; private Grid _parent; @@ -19,7 +25,6 @@ namespace Avalonia.Controls set => SetParent(value); } - private void SetParent(Grid value) { _parent = value; diff --git a/src/Avalonia.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs new file mode 100644 index 0000000000..ff6705cdc0 --- /dev/null +++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; + +namespace Avalonia.Controls +{ + public static class DesktopApplicationExtensions + { + [Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details", true)] + public static void Run(this Application app) => throw new NotSupportedException(); + + /// + /// On desktop-style platforms runs the application's main loop until closable is closed + /// + /// + /// Consider using StartWithDesktopStyleLifetime instead, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details + /// + public static void Run(this Application app, ICloseable closable) + { + var cts = new CancellationTokenSource(); + closable.Closed += (s, e) => cts.Cancel(); + + app.Run(cts.Token); + } + + /// + /// On desktop-style platforms runs the application's main loop until main window is closed + /// + /// + /// Consider using StartWithDesktopStyleLifetime instead, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details + /// + public static void Run(this Application app, Window mainWindow) + { + if (mainWindow == null) + { + throw new ArgumentNullException(nameof(mainWindow)); + } + var cts = new CancellationTokenSource(); + mainWindow.Closed += (_, __) => cts.Cancel(); + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + app.Run(cts.Token); + } + + /// + /// On desktop-style platforms runs the application's main loop with custom CancellationToken + /// without setting a lifetime. + /// + /// The token to track. + public static void Run(this Application app, CancellationToken token) + { + Dispatcher.UIThread.MainLoop(token); + } + + public static void RunWithMainWindow(this Application app) + where TWindow : Avalonia.Controls.Window, new() + { + var window = new TWindow(); + window.Show(); + app.Run(window); + } + } +} diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index e147fe1a52..8e23555c2d 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -1,7 +1,12 @@ +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + namespace Avalonia.Controls { - using System; - /// /// Defines the available docking modes for a control in a . /// @@ -70,107 +75,137 @@ namespace Avalonia.Controls set { SetValue(LastChildFillProperty, value); } } - /// + /// + /// Updates DesiredSize of the DockPanel. Called by parent Control. This is the first pass of layout. + /// + /// + /// Children are measured based on their sizing properties and . + /// Each child is allowed to consume all of the space on the side on which it is docked; Left/Right docked + /// children are granted all vertical space for their entire width, and Top/Bottom docked children are + /// granted all horizontal space for their entire height. + /// + /// Constraint size is an "upper limit" that the return value should not exceed. + /// The Panel's desired size. protected override Size MeasureOverride(Size constraint) { - double usedWidth = 0.0; - double usedHeight = 0.0; - double maximumWidth = 0.0; - double maximumHeight = 0.0; + var children = Children; + + double parentWidth = 0; // Our current required width due to children thus far. + double parentHeight = 0; // Our current required height due to children thus far. + double accumulatedWidth = 0; // Total width consumed by children. + double accumulatedHeight = 0; // Total height consumed by children. - // Measure each of the Children - foreach (Control element in Children) + for (int i = 0, count = children.Count; i < count; ++i) { - // Get the child's desired size - Size remainingSize = new Size( - Math.Max(0.0, constraint.Width - usedWidth), - Math.Max(0.0, constraint.Height - usedHeight)); - element.Measure(remainingSize); - Size desiredSize = element.DesiredSize; - - // Decrease the remaining space for the rest of the children - switch (GetDock(element)) + var child = children[i]; + Size childConstraint; // Contains the suggested input constraint for this child. + Size childDesiredSize; // Contains the return size from child measure. + + if (child == null) + { continue; } + + // Child constraint is the remaining size; this is total size minus size consumed by previous children. + childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), + Math.Max(0.0, constraint.Height - accumulatedHeight)); + + // Measure child. + child.Measure(childConstraint); + childDesiredSize = child.DesiredSize; + + // Now, we adjust: + // 1. Size consumed by children (accumulatedSize). This will be used when computing subsequent + // children to determine how much space is remaining for them. + // 2. Parent size implied by this child (parentSize) when added to the current children (accumulatedSize). + // This is different from the size above in one respect: A Dock.Left child implies a height, but does + // not actually consume any height for subsequent children. + // If we accumulate size in a given dimension, the next child (or the end conditions after the child loop) + // will deal with computing our minimum size (parentSize) due to that accumulation. + // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does + // not accumulate: Width for Top/Bottom, Height for Left/Right. + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: case Dock.Right: - maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height); - usedWidth += desiredSize.Width; + parentHeight = Math.Max(parentHeight, accumulatedHeight + childDesiredSize.Height); + accumulatedWidth += childDesiredSize.Width; break; + case Dock.Top: case Dock.Bottom: - maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width); - usedHeight += desiredSize.Height; + parentWidth = Math.Max(parentWidth, accumulatedWidth + childDesiredSize.Width); + accumulatedHeight += childDesiredSize.Height; break; } } - maximumWidth = Math.Max(maximumWidth, usedWidth); - maximumHeight = Math.Max(maximumHeight, usedHeight); - return new Size(maximumWidth, maximumHeight); + // Make sure the final accumulated size is reflected in parentSize. + parentWidth = Math.Max(parentWidth, accumulatedWidth); + parentHeight = Math.Max(parentHeight, accumulatedHeight); + + return (new Size(parentWidth, parentHeight)); } - /// + /// + /// DockPanel computes a position and final size for each of its children based upon their + /// enum and sizing properties. + /// + /// Size that DockPanel will assume to position children. protected override Size ArrangeOverride(Size arrangeSize) { - double left = 0.0; - double top = 0.0; - double right = 0.0; - double bottom = 0.0; - - // Arrange each of the Children var children = Children; - int dockedCount = children.Count - (LastChildFill ? 1 : 0); - int index = 0; + int totalChildrenCount = children.Count; + int nonFillChildrenCount = totalChildrenCount - (LastChildFill ? 1 : 0); + + double accumulatedLeft = 0; + double accumulatedTop = 0; + double accumulatedRight = 0; + double accumulatedBottom = 0; - foreach (Control element in children) + for (int i = 0; i < totalChildrenCount; ++i) { - // Determine the remaining space left to arrange the element - Rect remainingRect = new Rect( - left, - top, - Math.Max(0.0, arrangeSize.Width - left - right), - Math.Max(0.0, arrangeSize.Height - top - bottom)); - - // Trim the remaining Rect to the docked size of the element - // (unless the element should fill the remaining space because - // of LastChildFill) - if (index < dockedCount) + var child = children[i]; + if (child == null) + { continue; } + + Size childDesiredSize = child.DesiredSize; + Rect rcChild = new Rect( + accumulatedLeft, + accumulatedTop, + Math.Max(0.0, arrangeSize.Width - (accumulatedLeft + accumulatedRight)), + Math.Max(0.0, arrangeSize.Height - (accumulatedTop + accumulatedBottom))); + + if (i < nonFillChildrenCount) { - Size desiredSize = element.DesiredSize; - switch (GetDock(element)) + switch (DockPanel.GetDock((Control)child)) { case Dock.Left: - left += desiredSize.Width; - remainingRect = remainingRect.WithWidth(desiredSize.Width); - break; - case Dock.Top: - top += desiredSize.Height; - remainingRect = remainingRect.WithHeight(desiredSize.Height); + accumulatedLeft += childDesiredSize.Width; + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + case Dock.Right: - right += desiredSize.Width; - remainingRect = new Rect( - Math.Max(0.0, arrangeSize.Width - right), - remainingRect.Y, - desiredSize.Width, - remainingRect.Height); + accumulatedRight += childDesiredSize.Width; + rcChild = rcChild.WithX(Math.Max(0.0, arrangeSize.Width - accumulatedRight)); + rcChild = rcChild.WithWidth(childDesiredSize.Width); break; + + case Dock.Top: + accumulatedTop += childDesiredSize.Height; + rcChild = rcChild.WithHeight(childDesiredSize.Height); + break; + case Dock.Bottom: - bottom += desiredSize.Height; - remainingRect = new Rect( - remainingRect.X, - Math.Max(0.0, arrangeSize.Height - bottom), - remainingRect.Width, - desiredSize.Height); + accumulatedBottom += childDesiredSize.Height; + rcChild = rcChild.WithY(Math.Max(0.0, arrangeSize.Height - accumulatedBottom)); + rcChild = rcChild.WithHeight(childDesiredSize.Height); break; } } - element.Arrange(remainingRect); - index++; + child.Arrange(rcChild); } - return arrangeSize; + return (arrangeSize); } } } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 43beb923e5..2d48a7d33b 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -8,7 +8,7 @@ using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { - public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable + public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable { public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) { @@ -45,28 +45,12 @@ namespace Avalonia.Controls.Embedding { if (EnforceClientSize) availableSize = PlatformImpl?.ClientSize ?? default(Size); - return base.MeasureOverride(availableSize); - } - - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } + var rv = base.MeasureOverride(availableSize); + if (EnforceClientSize) + return availableSize; + return rv; } - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); - Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() => PlatformImpl?.Dispose(); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index c4f83ffd54..d326ab5734 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -30,25 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen init.EndInit(); } } - - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 653a4f5dcb..2d6757219d 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators /// The index of the item of data in the control's items. /// /// The item. - /// An optional member selector. /// The created controls. - ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector); + ItemContainerInfo Materialize(int index, object item); /// /// Removes a set of created containers. @@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); - bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector); + bool TryRecycle(int oldIndex, int newIndex, object item); /// /// Clears all created containers and returns the removed controls. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f1a1f94a01..4fd6f4135c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators public virtual Type ContainerType => null; /// - public ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector) + public ItemContainerInfo Materialize(int index, object item) { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainerInfo(CreateContainer(i), item, index); + var container = new ItemContainerInfo(CreateContainer(item), item, index); _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); @@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators } /// - public virtual bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) - { - return false; - } + public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; /// public virtual IEnumerable Clear() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 320d6c8faf..d1d1c2b172 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators } /// - public override bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) { var container = ContainerFromIndex(oldIndex); @@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators throw new IndexOutOfRangeException("Could not recycle container: not materialized."); } - var i = selector != null ? selector.Select(item) : item; - - container.SetValue(ContentProperty, i); + container.SetValue(ContentProperty, item); if (!(item is IControl)) { - container.DataContext = i; + container.DataContext = item; } - var info = MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, item); RaiseRecycled(new ItemContainerEventArgs(info)); return true; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 304c86dbf7..c06a64443c 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -92,7 +92,6 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - NameScope.SetNameScope((Control)(object)result, new NameScope()); Index.Add(item, result); return result; @@ -118,16 +117,22 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } - public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + + class WrapperTreeDataTemplate : ITreeDataTemplate { - return false; + 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; } private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; - var treeTemplate = template as ITreeDataTemplate ?? - new FuncTreeDataTemplate(typeof(object), template.Build, x => null); + var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); return treeTemplate; } } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 6d3b9f37cf..4f41b0bf1e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -22,12 +22,6 @@ namespace Avalonia.Controls /// public class Grid : Panel { - //------------------------------------------------------ - // - // Constructors - // - //------------------------------------------------------ - static Grid() { IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); @@ -47,42 +41,6 @@ namespace Avalonia.Controls { } - //------------------------------------------------------ - // - // Public Methods - // - //------------------------------------------------------ - - - /// - /// - /// - /* protected internal override IEnumerator LogicalChildren - { - get - { - // empty panel or a panel being used as the items - // host has *no* logical children; give empty enumerator - bool noChildren = (base.VisualChildrenCount == 0) || IsItemsHost; - - if (noChildren) - { - ExtendedData extData = ExtData; - - if ( extData == null - || ( (extData.ColumnDefinitions == null || extData.ColumnDefinitions.Count == 0) - && (extData.RowDefinitions == null || extData.RowDefinitions.Count == 0) ) - ) - { - // grid is empty - return EmptyEnumerator.Instance; - } - } - - return (new GridChildrenCollectionEnumeratorSimple(this, !noChildren)); - } - } */ - /// /// Helper for setting Column property on a Control. /// @@ -193,12 +151,6 @@ namespace Avalonia.Controls return element.GetValue(IsSharedSizeScopeProperty); } - //------------------------------------------------------ - // - // Public Properties - // - //------------------------------------------------------ - /// /// ShowGridLines property. /// @@ -248,52 +200,6 @@ namespace Avalonia.Controls } } - //------------------------------------------------------ - // - // Protected Methods - // - //------------------------------------------------------ - - /* /// - /// Derived class must implement to support Visual children. The method must return - /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. - /// - /// By default a Visual does not have any children. - /// - /// Remark: - /// During this virtual call it is not valid to modify the Visual tree. - /// - protected override Visual GetVisualChild(int index) - { - // because "base.Count + 1" for GridLinesRenderer - // argument checking done at the base class - if(index == base.VisualChildrenCount) - { - if (_gridLinesRenderer == null) - { - throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); - } - return _gridLinesRenderer; - } - else return base.GetVisualChild(index); - } - - /// - /// Derived classes override this property to enable the Visual code to enumerate - /// the Visual children. Derived classes need to return the number of children - /// from this method. - /// - /// By default a Visual does not have any children. - /// - /// Remark: During this virtual method the Visual tree must not be modified. - /// - protected override int VisualChildrenCount - { - //since GridLinesRenderer has not been added as a child, so we do not subtract - get { return base.VisualChildrenCount + (_gridLinesRenderer != null ? 1 : 0); } - }*/ - - /// /// Content measurement. /// @@ -306,8 +212,6 @@ namespace Avalonia.Controls try { - - ListenToNotifications = true; MeasureOverrideInProgress = true; @@ -550,7 +454,7 @@ namespace Avalonia.Controls // also use a count heuristic to break a loop in case of one. bool hasDesiredSizeUChanged = false; - int cnt=0; + int cnt = 0; // Cache Group2MinWidths & Group3MinHeights double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); @@ -582,17 +486,14 @@ namespace Avalonia.Controls MeasureCellsGroup(extData.CellGroup4, constraint, false, false); - gridDesiredSize = new Size( CalculateDesiredSize(DefinitionsU), CalculateDesiredSize(DefinitionsV)); - } } finally { MeasureOverrideInProgress = false; - } return (gridDesiredSize); @@ -606,8 +507,6 @@ namespace Avalonia.Controls { try { - - ArrangeOverrideInProgress = true; if (_data == null) @@ -627,13 +526,9 @@ namespace Avalonia.Controls { Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); - - SetFinalSize(DefinitionsU, arrangeSize.Width, true); SetFinalSize(DefinitionsV, arrangeSize.Height, false); - - var children = this.Children; for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) @@ -653,11 +548,11 @@ namespace Avalonia.Controls columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), - GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan) ); + GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); + - cell.Arrange(cellRect); - + } // update render bound on grid lines renderer visual @@ -669,31 +564,12 @@ namespace Avalonia.Controls { SetValid(); ArrangeOverrideInProgress = false; - } return (arrangeSize); } /// - /// - /// - /*protected internal override void OnVisualChildrenChanged( - AvaloniaObject visualAdded, - AvaloniaObject visualRemoved) - { - CellsStructureDirty = true; - - base.OnVisualChildrenChanged(visualAdded, visualRemoved); - }*/ - - //------------------------------------------------------ - // - // Internal Methods - // - //------------------------------------------------------ - - /// - /// Invalidates grid caches and makes the grid dirty for measure. + /// Invalidates grid caches and makes the grid dirty for measure. /// internal void Invalidate() { @@ -702,10 +578,10 @@ namespace Avalonia.Controls } /// - /// Returns final width for a column. + /// Returns final width for a column. /// /// - /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. /// internal double GetFinalColumnDefinitionWidth(int columnIndex) { @@ -724,10 +600,10 @@ namespace Avalonia.Controls } /// - /// Returns final height for a row. + /// Returns final height for a row. /// /// - /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. /// internal double GetFinalRowDefinitionHeight(int rowIndex) { @@ -745,12 +621,6 @@ namespace Avalonia.Controls return (value); } - //------------------------------------------------------ - // - // Internal Properties - // - //------------------------------------------------------ - /// /// Convenience accessor to MeasureOverrideInProgress bit flag. /// @@ -781,32 +651,22 @@ namespace Avalonia.Controls /// /// Convenience accessor to ValidDefinitionsVStructure bit flag. /// - internal bool RowDefinitionsDirty + internal bool RowDefinitionsDirty { get => RowDefinitions?.IsDirty ?? false; set => RowDefinitions.IsDirty = value; } - //------------------------------------------------------ - // - // Private Methods - // - //------------------------------------------------------ - /// /// Lays out cells according to rows and columns, and creates lookup grids. /// private void ValidateCells() { - - if (CellsStructureDirty) { ValidateCellsCore(); CellsStructureDirty = false; } - - } /// @@ -837,11 +697,8 @@ namespace Avalonia.Controls CellCache cell = new CellCache(); - // - // read and cache child positioning properties - // - // read indices from the corresponding properties + // Read indices from the corresponding properties: // clamp to value < number_of_columns // column >= 0 is guaranteed by property value validation callback cell.ColumnIndex = Math.Min(GetColumn((Control)child), DefinitionsU.Count - 1); @@ -849,7 +706,7 @@ namespace Avalonia.Controls // row >= 0 is guaranteed by property value validation callback cell.RowIndex = Math.Min(GetRow((Control)child), DefinitionsV.Count - 1); - // read span properties + // Read span properties: // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback cell.ColumnSpan = Math.Min(GetColumnSpan((Control)child), DefinitionsU.Count - cell.ColumnIndex); @@ -861,9 +718,7 @@ namespace Avalonia.Controls Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Count); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Count); - // - // calculate and cache length types for the child - // + // Calculate and cache length types for the child. cell.SizeTypeU = GetLengthTypeForRange(DefinitionsU, cell.ColumnIndex, cell.ColumnSpan); cell.SizeTypeV = GetLengthTypeForRange(DefinitionsV, cell.RowIndex, cell.RowSpan); @@ -871,9 +726,7 @@ namespace Avalonia.Controls hasStarCellsU |= cell.IsStarU; hasStarCellsV |= cell.IsStarV; - // - // distribute cells into four groups. - // + // Distribute cells into four groups. if (!cell.IsStarV) { @@ -887,15 +740,15 @@ namespace Avalonia.Controls cell.Next = extData.CellGroup3; extData.CellGroup3 = i; - // remember if this cell belongs to auto row + // Remember if this cell belongs to auto row hasGroup3CellsInAutoRows |= cell.IsAutoV; } } else { - if ( cell.IsAutoU - // note below: if spans through Star column it is NOT Auto - && !cell.IsStarU ) + if (cell.IsAutoU + // Note below: if spans through Star column it is NOT Auto + && !cell.IsStarU) { cell.Next = extData.CellGroup2; extData.CellGroup2 = i; @@ -986,7 +839,7 @@ namespace Avalonia.Controls extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() { Parent = this } }; } else - { + { extData.DefinitionsV = extData.RowDefinitions; } } @@ -1053,7 +906,7 @@ namespace Avalonia.Controls { double[] minSizes = isRows ? new double[DefinitionsV.Count] : new double[DefinitionsU.Count]; - for (int j=0; j (double)o ) + if (o == null + || value > (double)o) { store[key] = value; } @@ -1235,13 +1088,13 @@ namespace Avalonia.Controls int cell, bool forceInfinityV) { - + double cellMeasureWidth; double cellMeasureHeight; - if ( PrivateCells[cell].IsAutoU - && !PrivateCells[cell].IsStarU ) + if (PrivateCells[cell].IsAutoU + && !PrivateCells[cell].IsStarU) { // if cell belongs to at least one Auto column and not a single Star column // then it should be calculated "to content", thus it is possible to "shortcut" @@ -1261,8 +1114,8 @@ namespace Avalonia.Controls { cellMeasureHeight = double.PositiveInfinity; } - else if ( PrivateCells[cell].IsAutoV - && !PrivateCells[cell].IsStarV ) + else if (PrivateCells[cell].IsAutoV + && !PrivateCells[cell].IsStarV) { // if cell belongs to at least one Auto row and not a single Star row // then it should be calculated "to content", thus it is possible to "shortcut" @@ -1277,19 +1130,18 @@ namespace Avalonia.Controls PrivateCells[cell].RowSpan); } - + var child = this.Children[cell]; if (child != null) { Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); child.Measure(childConstraint); } - - - } + } + /// /// Calculates one dimensional measure size for given definitions' range. /// @@ -1393,12 +1245,12 @@ namespace Avalonia.Controls // sanity check: no matter what, but min size must always be the smaller; // max size must be the biggest; and preferred should be in between - Debug.Assert( minSize <= preferredSize - && preferredSize <= maxSize - && rangeMinSize <= rangePreferredSize - && rangePreferredSize <= rangeMaxSize ); + Debug.Assert(minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize); - if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (maxMaxSize < maxSize) maxMaxSize = maxSize; if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; tempDefinitions[i - start] = definitions[i]; } @@ -1492,8 +1344,8 @@ namespace Avalonia.Controls // double equalSize = requestedSize / count; - if ( equalSize < maxMaxSize - && !_AreClose(equalSize, maxMaxSize) ) + if (equalSize < maxMaxSize + && !_AreClose(equalSize, maxMaxSize)) { // equi-size is less than maximum of maxSizes. // in this case distribute so that smaller definitions grow faster than @@ -1502,12 +1354,12 @@ namespace Avalonia.Controls double sizeToDistribute = requestedSize - rangeMaxSize; // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers - Debug.Assert( !double.IsInfinity(totalRemainingSize) - && !double.IsNaN(totalRemainingSize) - && totalRemainingSize > 0 - && !double.IsInfinity(sizeToDistribute) - && !double.IsNaN(sizeToDistribute) - && sizeToDistribute > 0 ); + Debug.Assert(!double.IsInfinity(totalRemainingSize) + && !double.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) + && sizeToDistribute > 0); for (int i = 0; i < count; ++i) { @@ -1543,105 +1395,10 @@ namespace Avalonia.Controls IReadOnlyList definitions, double availableSize) { - // if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) - // { - // ResolveStarLegacy(definitions, availableSize); - // } - // else - // { - ResolveStarMaxDiscrepancy(definitions, availableSize); - // } - } - - // original implementation, used from 3.0 through 4.6.2 - private void ResolveStarLegacy( - IReadOnlyList definitions, - double availableSize) - { - DefinitionBase[] tempDefinitions = TempDefinitions; - int starDefinitionsCount = 0; - double takenSize = 0; - - for (int i = 0; i < definitions.Count; ++i) - { - switch (definitions[i].SizeType) - { - case (LayoutTimeSizeType.Auto): - takenSize += definitions[i].MinSize; - break; - case (LayoutTimeSizeType.Pixel): - takenSize += definitions[i].MeasureSize; - break; - case (LayoutTimeSizeType.Star): - { - tempDefinitions[starDefinitionsCount++] = definitions[i]; - - double starValue = definitions[i].UserSize.Value; - - if (_IsZero(starValue)) - { - definitions[i].MeasureSize = 0; - definitions[i].SizeCache = 0; - } - else - { - // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values - // can be summed up without overflow - starValue = Math.Min(starValue, c_starClip); - - // Note: normalized star value is temporary cached into MeasureSize - definitions[i].MeasureSize = starValue; - double maxSize = Math.Max(definitions[i].MinSize, definitions[i].UserMaxSize); - maxSize = Math.Min(maxSize, c_starClip); - definitions[i].SizeCache = maxSize / starValue; - } - } - break; - } - } - - if (starDefinitionsCount > 0) - { - Array.Sort(tempDefinitions, 0, starDefinitionsCount, s_starDistributionOrderComparer); - - // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... - // partial sum value is stored in each definition's SizeCache member. - // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus - // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. - // this is an important change from previous implementation where the following was possible: - // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... - double allStarWeights = 0; - int i = starDefinitionsCount - 1; - do - { - allStarWeights += tempDefinitions[i].MeasureSize; - tempDefinitions[i].SizeCache = allStarWeights; - } while (--i >= 0); - - i = 0; - do - { - double resolvedSize; - double starValue = tempDefinitions[i].MeasureSize; - - if (_IsZero(starValue)) - { - resolvedSize = tempDefinitions[i].MinSize; - } - else - { - double userSize = Math.Max(availableSize - takenSize, 0.0) * (starValue / tempDefinitions[i].SizeCache); - resolvedSize = Math.Min(userSize, tempDefinitions[i].UserMaxSize); - resolvedSize = Math.Max(tempDefinitions[i].MinSize, resolvedSize); - } - - tempDefinitions[i].MeasureSize = resolvedSize; - takenSize += resolvedSize; - } while (++i < starDefinitionsCount); - } + ResolveStarMaxDiscrepancy(definitions, availableSize); } - // new implementation as of 4.7. Several improvements: + // New implementation as of 4.7. Several improvements: // 1. Allocate to *-defs hitting their min or max constraints, before allocating // to other *-defs. A def that hits its min uses more space than its // proportional share, reducing the space available to everyone else. @@ -1668,7 +1425,7 @@ namespace Avalonia.Controls // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights double maxStar = 0.0; - for (int i=0; i definitions, - double finalSize, - bool columns) - { - int starDefinitionsCount = 0; // traverses form the first entry up - int nonStarIndex = definitions.Count; // traverses from the last entry down - double allPreferredArrangeSize = 0; - bool useLayoutRounding = this.UseLayoutRounding; - int[] definitionIndices = DefinitionIndices; - double[] roundingErrors = null; - - // If using layout rounding, check whether rounding needs to compensate for high DPI - double dpi = 1.0; - - if (useLayoutRounding) - { - // DpiScale dpiScale = GetDpi(); - // dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; - dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; - roundingErrors = RoundingErrors; - } - - for (int i = 0; i < definitions.Count; ++i) - { - // if definition is shared then is cannot be star - Debug.Assert(!definitions[i].IsShared || !definitions[i].UserSize.IsStar); - - if (definitions[i].UserSize.IsStar) - { - double starValue = definitions[i].UserSize.Value; - - if (_IsZero(starValue)) - { - // cach normilized star value temporary into MeasureSize - definitions[i].MeasureSize = 0; - definitions[i].SizeCache = 0; - } - else - { - // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values - // can be summed up without overflow - starValue = Math.Min(starValue, c_starClip); - - // Note: normalized star value is temporary cached into MeasureSize - definitions[i].MeasureSize = starValue; - double maxSize = Math.Max(definitions[i].MinSizeForArrange, definitions[i].UserMaxSize); - maxSize = Math.Min(maxSize, c_starClip); - definitions[i].SizeCache = maxSize / starValue; - if (useLayoutRounding) - { - roundingErrors[i] = definitions[i].SizeCache; - definitions[i].SizeCache = MathUtilities.RoundLayoutValue(definitions[i].SizeCache, dpi); - } - } - definitionIndices[starDefinitionsCount++] = i; - } - else - { - double userSize = 0; - - switch (definitions[i].UserSize.GridUnitType) - { - case (GridUnitType.Pixel): - userSize = definitions[i].UserSize.Value; - break; - - case (GridUnitType.Auto): - userSize = definitions[i].MinSizeForArrange; - break; - } - - double userMaxSize; - - if (definitions[i].IsShared) - { - // overriding userMaxSize effectively prevents squishy-ness. - // this is a "solution" to avoid shared definitions from been sized to - // different final size at arrange time, if / when different grids receive - // different final sizes. - userMaxSize = userSize; - } - else - { - userMaxSize = definitions[i].UserMaxSize; - } - - definitions[i].SizeCache = Math.Max(definitions[i].MinSizeForArrange, Math.Min(userSize, userMaxSize)); - if (useLayoutRounding) - { - roundingErrors[i] = definitions[i].SizeCache; - definitions[i].SizeCache = MathUtilities.RoundLayoutValue(definitions[i].SizeCache, dpi); - } - - allPreferredArrangeSize += definitions[i].SizeCache; - definitionIndices[--nonStarIndex] = i; - } - } - - // indices should meet - Debug.Assert(nonStarIndex == starDefinitionsCount); - - if (starDefinitionsCount > 0) - { - StarDistributionOrderIndexComparer starDistributionOrderIndexComparer = new StarDistributionOrderIndexComparer(definitions); - Array.Sort(definitionIndices, 0, starDefinitionsCount, starDistributionOrderIndexComparer); - - // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... - // partial sum value is stored in each definition's SizeCache member. - // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus - // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. - // this is an important change from previous implementation where the following was possible: - // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... - double allStarWeights = 0; - int i = starDefinitionsCount - 1; - do - { - allStarWeights += definitions[definitionIndices[i]].MeasureSize; - definitions[definitionIndices[i]].SizeCache = allStarWeights; - } while (--i >= 0); - - i = 0; - do - { - double resolvedSize; - double starValue = definitions[definitionIndices[i]].MeasureSize; - - if (_IsZero(starValue)) - { - resolvedSize = definitions[definitionIndices[i]].MinSizeForArrange; - } - else - { - double userSize = Math.Max(finalSize - allPreferredArrangeSize, 0.0) * (starValue / definitions[definitionIndices[i]].SizeCache); - resolvedSize = Math.Min(userSize, definitions[definitionIndices[i]].UserMaxSize); - resolvedSize = Math.Max(definitions[definitionIndices[i]].MinSizeForArrange, resolvedSize); - } - - definitions[definitionIndices[i]].SizeCache = resolvedSize; - if (useLayoutRounding) - { - roundingErrors[definitionIndices[i]] = definitions[definitionIndices[i]].SizeCache; - definitions[definitionIndices[i]].SizeCache = MathUtilities.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); - } - - allPreferredArrangeSize += definitions[definitionIndices[i]].SizeCache; - } while (++i < starDefinitionsCount); - } - - if ( allPreferredArrangeSize > finalSize - && !_AreClose(allPreferredArrangeSize, finalSize) ) - { - DistributionOrderIndexComparer distributionOrderIndexComparer = new DistributionOrderIndexComparer(definitions); - Array.Sort(definitionIndices, 0, definitions.Count, distributionOrderIndexComparer); - double sizeToDistribute = finalSize - allPreferredArrangeSize; - - for (int i = 0; i < definitions.Count; ++i) - { - int definitionIndex = definitionIndices[i]; - double final = definitions[definitionIndex].SizeCache + (sizeToDistribute / (definitions.Count - i)); - double finalOld = final; - final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); - final = Math.Min(final, definitions[definitionIndex].SizeCache); - - if (useLayoutRounding) - { - roundingErrors[definitionIndex] = final; - final = MathUtilities.RoundLayoutValue(finalOld, dpi); - final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); - final = Math.Min(final, definitions[definitionIndex].SizeCache); - } - - sizeToDistribute -= (final - definitions[definitionIndex].SizeCache); - definitions[definitionIndex].SizeCache = final; - } - - allPreferredArrangeSize = finalSize - sizeToDistribute; - } - - if (useLayoutRounding) - { - if (!_AreClose(allPreferredArrangeSize, finalSize)) - { - // Compute deltas - for (int i = 0; i < definitions.Count; ++i) - { - roundingErrors[i] = roundingErrors[i] - definitions[i].SizeCache; - definitionIndices[i] = i; - } - - // Sort rounding errors - RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); - Array.Sort(definitionIndices, 0, definitions.Count, roundingErrorIndexComparer); - double adjustedSize = allPreferredArrangeSize; - double dpiIncrement = MathUtilities.RoundLayoutValue(1.0, dpi); - - if (allPreferredArrangeSize > finalSize) - { - int i = definitions.Count - 1; - while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); - if (final < definition.SizeCache) - { - adjustedSize -= dpiIncrement; - } - definition.SizeCache = final; - i--; - } - } - else if (allPreferredArrangeSize < finalSize) - { - int i = 0; - while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); - if (final > definition.SizeCache) - { - adjustedSize += dpiIncrement; - } - definition.SizeCache = final; - i++; - } - } - } - } - - definitions[0].FinalOffset = 0.0; - for (int i = 0; i < definitions.Count; ++i) - { - definitions[(i + 1) % definitions.Count].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; - } + SetFinalSizeMaxDiscrepancy(definitions, finalSize, columns); } // new implementation, as of 4.7. This incorporates the same algorithm @@ -2251,7 +1762,7 @@ namespace Avalonia.Controls // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights double maxStar = 0.0; - for (int i=0; i finalSize) { @@ -2694,7 +2204,7 @@ namespace Avalonia.Controls /// if the max constraint has higher discrepancy /// null if proportion doesn't fail a min or max constraint /// The discrepancy is the ratio of the proportion to the max- or min-ratio. - /// When both ratios hit the constraint, minRatio < proportion > maxRatio, + /// When both ratios hit the constraint, minRatio < proportion < maxRatio, /// and the minRatio has higher discrepancy if /// (proportion / minRatio) > (maxRatio / proportion) /// @@ -2782,8 +2292,8 @@ namespace Avalonia.Controls ExtendedData extData = ExtData; if (extData != null) { -// for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); -// for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); + // for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); + // for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); if (extData.TempDefinitions != null) { @@ -2793,29 +2303,7 @@ namespace Avalonia.Controls } } } - - /// - /// Returns true if ColumnDefinitions collection is not empty - /// - public bool ShouldSerializeColumnDefinitions() - { - ExtendedData extData = ExtData; - return ( extData != null - && extData.ColumnDefinitions != null - && extData.ColumnDefinitions.Count > 0 ); - } - - /// - /// Returns true if RowDefinitions collection is not empty - /// - public bool ShouldSerializeRowDefinitions() - { - ExtendedData extData = ExtData; - return ( extData != null - && extData.RowDefinitions != null - && extData.RowDefinitions.Count > 0 ); - } - + /// /// Synchronized ShowGridLines property with the state of the grid's visual collection /// by adding / removing GridLinesRenderer visual. @@ -2872,25 +2360,19 @@ namespace Avalonia.Controls return (flags == 0 || (_flags & flags) != 0); } - /// - /// - /// private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { Grid grid = (Grid)d; - if ( grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway - && grid.ListenToNotifications) + if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + && grid.ListenToNotifications) { grid.InvalidateVisual(); } - grid.SetFlags((bool) e.NewValue, Flags.ShowGridLinesPropertyValue); + grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); } - /// - /// - /// private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { Visual child = d as Visual; @@ -2898,36 +2380,20 @@ namespace Avalonia.Controls if (child != null) { Grid grid = child.GetVisualParent(); - if ( grid != null - && grid.ExtData != null - && grid.ListenToNotifications ) + if (grid != null + && grid.ExtData != null + && grid.ListenToNotifications) { grid.CellsStructureDirty = true; } } } - /* /// - /// - /// - private static bool IsIntValueNotNegative(object value) - { - return ((int)value >= 0); - } - - /// - /// - /// - private static bool IsIntValueGreaterThanZero(object value) - { - return ((int)value > 0); - }*/ - /// /// Helper for Comparer methods. /// /// - /// true iff one or both of x and y are null, in which case result holds + /// true if one or both of x and y are null, in which case result holds /// the relative sort order. /// private static bool CompareNullRefs(object x, object y, out int result) @@ -2956,12 +2422,6 @@ namespace Avalonia.Controls return (result != 2); } - //------------------------------------------------------ - // - // Private Properties - // - //------------------------------------------------------ - /// /// Private version returning array of column definitions. /// @@ -2988,8 +2448,8 @@ namespace Avalonia.Controls ExtendedData extData = ExtData; int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count) * 2; - if ( extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength ) + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) { WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); if (tempDefinitionsWeakRef == null) @@ -3000,8 +2460,8 @@ namespace Avalonia.Controls else { extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if ( extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength ) + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) { extData.TempDefinitions = new DefinitionBase[requiredLength]; tempDefinitionsWeakRef.Target = extData.TempDefinitions; @@ -3129,7 +2589,7 @@ namespace Avalonia.Controls /// true if d == 0. private static bool _IsZero(double d) { - return (Math.Abs(d) < c_epsilon); + return (Math.Abs(d) < double.Epsilon); } /// @@ -3140,7 +2600,7 @@ namespace Avalonia.Controls /// true if d1 == d2 private static bool _AreClose(double d1, double d2) { - return (Math.Abs(d1 - d2) < c_epsilon); + return (Math.Abs(d1 - d2) < double.Epsilon); } /// @@ -3169,13 +2629,11 @@ namespace Avalonia.Controls } } - //------------------------------------------------------ - // - // Private Fields - // - //------------------------------------------------------ - private ExtendedData _data; // extended data instantiated on demand, for non-trivial case handling only - private Flags _flags; // grid validity / property caches dirtiness flags + // Extended data instantiated on demand, for non-trivial case handling only + private ExtendedData _data; + + // Grid validity / property caches dirtiness flags + private Flags _flags; private GridLinesRenderer _gridLinesRenderer; // Keeps track of definition indices. @@ -3184,29 +2642,16 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. double[] _roundingErrors; - //------------------------------------------------------ - // - // Static Fields - // - //------------------------------------------------------ - private const double c_epsilon = 1e-5; // used in fp calculations - private const double c_starClip = 1e298; // used as maximum for clipping star values during normalization - private const int c_layoutLoopMaxCount = 5; // 5 is an arbitrary constant chosen to end the measure loop + // 5 is an arbitrary constant chosen to end the measure loop + private const int c_layoutLoopMaxCount = 5; + private static readonly LocalDataStoreSlot s_tempDefinitionsDataSlot = Thread.AllocateDataSlot(); private static readonly IComparer s_spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); private static readonly IComparer s_spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); - private static readonly IComparer s_starDistributionOrderComparer = new StarDistributionOrderComparer(); - private static readonly IComparer s_distributionOrderComparer = new DistributionOrderComparer(); private static readonly IComparer s_minRatioComparer = new MinRatioComparer(); private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); - //------------------------------------------------------ - // - // Private Structures / Classes - // - //------------------------------------------------------ - /// /// Extended data instantiated on demand, when grid handles non-trivial case. /// @@ -3237,34 +2682,28 @@ namespace Avalonia.Controls // * Valid???Layout flags indicate that layout time portion of the information // stored on the objects should be updated. // - ValidDefinitionsUStructure = 0x00000001, - ValidDefinitionsVStructure = 0x00000002, - ValidCellsStructure = 0x00000004, + ValidDefinitionsUStructure = 0x00000001, + ValidDefinitionsVStructure = 0x00000002, + ValidCellsStructure = 0x00000004, // // boolean properties state // - ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? + ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? // // boolean flags // - ListenToNotifications = 0x00001000, // "0" when all notifications are ignored - SizeToContentU = 0x00002000, // "1" if calculating to content in U direction - SizeToContentV = 0x00004000, // "1" if calculating to content in V direction - HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column - HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row - HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row - MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride - ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride + ListenToNotifications = 0x00001000, // "0" when all notifications are ignored + SizeToContentU = 0x00002000, // "1" if calculating to content in U direction + SizeToContentV = 0x00004000, // "1" if calculating to content in V direction + HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column + HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row + HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row + MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride + ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride } - //------------------------------------------------------ - // - // Properties - // - //------------------------------------------------------ - /// /// ShowGridLines property. This property is used mostly /// for simplification of visual debuggig. When it is set @@ -3289,8 +2728,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "Column", defaultValue: 0, - validate: (_, v) => { if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Column value."); }); + validate: (_, v) => + { + if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Column value."); + }); /// /// Row property. This is an attached property. @@ -3307,8 +2749,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "Row", defaultValue: 0, - validate: (_, v) => { if (v >= 0) return v; - else throw new ArgumentException("Invalid Grid.Row value."); }); + validate: (_, v) => + { + if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Row value."); + }); /// /// ColumnSpan property. This is an attached property. @@ -3324,8 +2769,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "ColumnSpan", defaultValue: 1, - validate: (_, v) => { if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.ColumnSpan value."); }); + validate: (_, v) => + { + if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.ColumnSpan value."); + }); /// /// RowSpan property. This is an attached property. @@ -3341,8 +2789,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "RowSpan", defaultValue: 1, - validate: (_, v) => { if (v >= 1) return v; - else throw new ArgumentException("Invalid Grid.RowSpan value."); }); + validate: (_, v) => + { + if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.RowSpan value."); + }); /// /// IsSharedSizeScope property marks scoping element for shared size. @@ -3351,30 +2802,18 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached( "IsSharedSizeScope"); - //------------------------------------------------------ - // - // Internal Structures / Classes - // - //------------------------------------------------------ - /// /// LayoutTimeSizeType is used internally and reflects layout-time size type. /// [System.Flags] internal enum LayoutTimeSizeType : byte { - None = 0x00, - Pixel = 0x01, - Auto = 0x02, - Star = 0x04, + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, } - //------------------------------------------------------ - // - // Private Structures / Classes - // - //------------------------------------------------------ - /// /// CellCache stored calculated values of /// 1. attached cell positioning properties; @@ -3422,7 +2861,7 @@ namespace Avalonia.Controls int hash = (_start ^ (_count << 2)); if (_u) hash &= 0x7ffffff; - else hash |= 0x8000000; + else hash |= 0x8000000; return (hash); } @@ -3433,10 +2872,10 @@ namespace Avalonia.Controls public override bool Equals(object obj) { SpanKey sk = obj as SpanKey; - return ( sk != null - && sk._start == _start - && sk._count == _count - && sk._u == _u ); + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); } /// @@ -3544,51 +2983,6 @@ namespace Avalonia.Controls } } - /// - /// StarDistributionOrderComparer. - /// - private class StarDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// DistributionOrderComparer. - /// - private class DistributionOrderComparer: IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; - double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; - result = xprime.CompareTo(yprime); - } - - return result; - } - } - - /// /// StarDistributionOrderIndexComparer. /// @@ -3895,94 +3289,8 @@ namespace Avalonia.Controls } } - /* /// - /// Implementation of a simple enumerator of grid's logical children - /// - private class GridChildrenCollectionEnumeratorSimple : IEnumerator - { - internal GridChildrenCollectionEnumeratorSimple(Grid grid, bool includeChildren) - { - Debug.Assert(grid != null); - _currentEnumerator = -1; - _enumerator0 = new ColumnDefinitions.Enumerator(grid.ExtData != null ? grid.ExtData.ColumnDefinitions : null); - _enumerator1 = new RowDefinitions.Enumerator(grid.ExtData != null ? grid.ExtData.RowDefinitions : null); - // GridLineRenderer is NOT included into this enumerator. - _enumerator2Index = 0; - if (includeChildren) - { - _enumerator2Collection = grid.Children; - _enumerator2Count = _enumerator2Collection.Count; - } - else - { - _enumerator2Collection = null; - _enumerator2Count = 0; - } - } - - public bool MoveNext() - { - while (_currentEnumerator < 3) - { - if (_currentEnumerator >= 0) - { - switch (_currentEnumerator) - { - case (0): if (_enumerator0.MoveNext()) { _currentChild = _enumerator0.Current; return (true); } break; - case (1): if (_enumerator1.MoveNext()) { _currentChild = _enumerator1.Current; return (true); } break; - case (2): if (_enumerator2Index < _enumerator2Count) - { - _currentChild = _enumerator2Collection[_enumerator2Index]; - _enumerator2Index++; - return (true); - } - break; - } - } - _currentEnumerator++; - } - return (false); - } - - public Object Current - { - get - { - if (_currentEnumerator == -1) - { - throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); - } - if (_currentEnumerator >= 3) - { - throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); - } - - // assert below is not true anymore since Controls allowes for null children - //Debug.Assert(_currentChild != null); - return (_currentChild); - } - } - - public void Reset() - { - _currentEnumerator = -1; - _currentChild = null; - _enumerator0.Reset(); - _enumerator1.Reset(); - _enumerator2Index = 0; - } - - private int _currentEnumerator; - private Object _currentChild; - private ColumnDefinitions.Enumerator _enumerator0; - private RowDefinitions.Enumerator _enumerator1; - private Controls _enumerator2Collection; - private int _enumerator2Index; - private int _enumerator2Count; - }*/ - /// - /// Helper to render grid lines. + /// Helper for rendering grid lines. /// internal class GridLinesRenderer : Control { @@ -3995,15 +3303,15 @@ namespace Avalonia.Controls var ds1 = new DashStyle(dashArray, 0); _oddDashPen = new Pen(Brushes.Blue, - _penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds1); + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); var ds2 = new DashStyle(dashArray, _dashLength); _evenDashPen = new Pen(Brushes.Yellow, - _penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds2); + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); } /// @@ -4052,7 +3360,6 @@ namespace Avalonia.Controls internal void UpdateRenderBounds(Size arrangeSize) { _lastArrangeSize = arrangeSize; - this.InvalidateMeasure(); this.InvalidateVisual(); } @@ -4061,6 +3368,6 @@ namespace Avalonia.Controls private const double _penWidth = 1.0; // private static readonly Pen _oddDashPen; // first pen to draw dash private static readonly Pen _evenDashPen; // second pen to draw dash - } + } } } diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 304a760216..28b9b3a38f 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs new file mode 100644 index 0000000000..6b5cb2ee25 --- /dev/null +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls +{ + public interface IScrollAnchorProvider + { + IControl CurrentAnchor { get; } + void RegisterAnchorCandidate(IControl element); + void UnregisterAnchorCandidate(IControl element); + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3dfeae52a4..902e55bde6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -36,6 +36,12 @@ namespace Avalonia.Controls public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemCountProperty = + AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount); + /// /// Defines the property. /// @@ -48,13 +54,8 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - AvaloniaProperty.Register(nameof(MemberSelector)); - private IEnumerable _items = new AvaloniaList(); + private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; private IDisposable _itemsCollectionChangedSubscription; @@ -110,10 +111,13 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + /// + /// Gets the number of items in . + /// public int ItemCount { - get; - private set; + get => _itemCount; + private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); } /// @@ -134,15 +138,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the items presenter control. /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 07372eb714..1430c39c76 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls } /// - /// Utilize the for layout transforms. + /// Utilize the for layout transforms. /// public bool UseRenderTransform { diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 041b81155a..f26cd47bcb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -68,7 +68,13 @@ namespace Avalonia.Controls /// public new IList SelectedItems => base.SelectedItems; - /// + /// + /// Gets or sets the selection mode. + /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// public new SelectionMode SelectionMode { get { return base.SelectionMode; } @@ -84,6 +90,16 @@ namespace Avalonia.Controls set { SetValue(VirtualizationModeProperty, value); } } + /// + /// Selects all items in the . + /// + public new void SelectAll() => base.SelectAll(); + + /// + /// Deselects all items in the . + /// + public new void UnselectAll() => base.UnselectAll(); + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -118,7 +134,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..6ec97aa04e 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -40,37 +41,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..8eed58bb4d 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -7,7 +7,6 @@ using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -31,13 +30,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 5f01c233b8..bd558af5ef 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -20,8 +21,6 @@ namespace Avalonia.Controls /// public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { - private ICommand _command; - /// /// Defines the property. /// @@ -91,9 +90,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - /// - /// The submenu popup. - /// + private ICommand _command; + private bool _commandCanExecute = true; private Popup _popup; /// @@ -231,6 +229,8 @@ namespace Avalonia.Controls /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); @@ -394,6 +394,22 @@ namespace Avalonia.Controls } } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + { + base.UpdateDataValidation(property, status); + if (property == CommandProperty) + { + if (status?.ErrorType == BindingErrorType.Error) + { + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } + } + } + } + /// /// Closes all submenus of the menu item. /// @@ -437,9 +453,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 0f365fcb08..a4c674a03b 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); LogicalChildren.InsertRange(e.NewStartingIndex, controls); - VisualChildren.AddRange(e.NewItems.OfType()); + VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Move: diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index bb357453ff..cb1291410a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,94 +9,69 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer( - DispatcherPriority.Render, - new TimeSpan(0, 0, 0, 0, 66), - () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - private readonly AutoResetEvent _queued = new AutoResetEvent(false); - private readonly Queue _actions = new Queue(); public void RunLoop(CancellationToken cancellationToken) { - var handles = new[] {_signaled, _queued}; while (true) { - if (0 == WaitHandle.WaitAny(handles)) - Signaled?.Invoke(null); - else - { - while (true) - { - Action item; - lock (_actions) - if (_actions.Count == 0) - break; - else - item = _actions.Dequeue(); - item(); - } - } + Signaled?.Invoke(null); + _signaled.WaitOne(); } } - public void Send(Action cb) - { - lock (_actions) - { - _actions.Enqueue(cb); - _queued.Set(); - } - } - class WatTimer : IDisposable + class TimerImpl : IDisposable { - private readonly IDisposable _timer; + private readonly DispatcherPriority _priority; + private readonly TimeSpan _interval; + private readonly Action _tick; + private Timer _timer; private GCHandle _handle; - public WatTimer(IDisposable timer) + public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) { - _timer = timer; + _priority = priority; + _interval = interval; + _tick = tick; + _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1)); _handle = GCHandle.Alloc(_timer); } + private void OnTimer(object state) + { + if (_timer == null) + return; + Dispatcher.UIThread.Post(() => + { + + if (_timer == null) + return; + _tick(); + _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1)); + }); + } + + public void Dispose() { _handle.Free(); _timer.Dispose(); + _timer = null; } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - return new WatTimer(new System.Threading.Timer(delegate - { - var tcs = new TaskCompletionSource(); - Send(() => - { - try - { - tick(); - } - finally - { - tcs.SetResult(0); - } - }); - - - tcs.Task.Wait(); - }, null, TimeSpan.Zero, interval)); - - + return new TimerImpl(priority, interval, tick); } public void Signal(DispatcherPriority prio) diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a3123cf8c6..dedab3e43e 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters if (container == null && IsVirtualized) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + var materialized = ItemContainerGenerator.Materialize(index, item); Panel.Children.Add(materialized.ContainerControl); container = materialized.ContainerControl; } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 49f268c128..c2690d503d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -325,12 +325,6 @@ namespace Avalonia.Controls.Presenters { _dataTemplate = dataTemplate; newChild = _dataTemplate.Build(content); - - // Give the new control its own name scope. - if (newChild is Control controlResult) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } } } else diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs index 035d404dec..6e72908e6b 100644 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 46da8fe3f8..ae52e733b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 413855bcc6..56f64779f6 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, Owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index f72a65ead2..b8b8094582 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -20,6 +20,8 @@ namespace Avalonia.Controls.Presenters /// internal class ItemVirtualizerSimple : ItemVirtualizer { + private int _anchor; + /// /// Initializes a new instance of the class. /// @@ -312,7 +314,6 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) { - var memberSelector = Owner.MemberSelector; var index = NextIndex; var step = 1; @@ -335,7 +336,7 @@ namespace Avalonia.Controls.Presenters } } - var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + var materialized = generator.Materialize(index, Items.ElementAt(index)); if (step == 1) { @@ -362,7 +363,10 @@ namespace Avalonia.Controls.Presenters if (panel.OverflowCount > 0) { - RemoveContainers(panel.OverflowCount); + if (_anchor <= FirstIndex) + { + RemoveContainers(panel.OverflowCount); + } } } @@ -378,7 +382,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; var containers = generator.Containers.ToList(); var itemIndex = FirstIndex; @@ -388,7 +391,7 @@ namespace Avalonia.Controls.Presenters if (!object.Equals(container.Item, item)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + if (!generator.TryRecycle(itemIndex, itemIndex, item)) { throw new NotImplementedException(); } @@ -415,7 +418,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; //validate delta it should never overflow last index or generate index < 0 delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); @@ -432,7 +434,7 @@ namespace Avalonia.Controls.Presenters var item = Items.ElementAt(newItemIndex); - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) { throw new NotImplementedException(); } @@ -540,7 +542,9 @@ namespace Avalonia.Controls.Presenters // it means we're running a unit test. if (container != null && layoutManager != null) { + _anchor = index; layoutManager.ExecuteLayoutPass(); + _anchor = -1; if (newOffset != -1 && newOffset != OffsetValue) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b4b792139d..ea56a0c6fc 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - ItemsControl.MemberSelectorProperty.AddOwner(); - private IEnumerable _items; private IDisposable _itemsSubscription; private bool _createdPanel; @@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the panel used to display the items. /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index fbdf885709..debbb81264 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -19,6 +19,15 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( o => o.SelectionStart, @@ -34,11 +43,20 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private IBrush _highlightBrush; - + static TextPresenter() { - AffectsRender(PasswordCharProperty); + AffectsRender(PasswordCharProperty, + SelectionBrushProperty, SelectionForegroundBrushProperty, + SelectionStartProperty, SelectionEndProperty); + + Observable.Merge( + SelectionStartProperty.Changed, + SelectionEndProperty.Changed, + PasswordCharProperty.Changed + ).AddClassHandler((x,_) => x.InvalidateFormattedText()); + + CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } public TextPresenter() @@ -46,17 +64,6 @@ namespace Avalonia.Controls.Presenters _caretTimer = new DispatcherTimer(); _caretTimer.Interval = TimeSpan.FromMilliseconds(500); _caretTimer.Tick += CaretTimerTick; - - Observable.Merge( - this.GetObservable(SelectionStartProperty), - this.GetObservable(SelectionEndProperty)) - .Subscribe(_ => InvalidateFormattedText()); - - this.GetObservable(CaretIndexProperty) - .Subscribe(CaretIndexChanged); - - this.GetObservable(PasswordCharProperty) - .Subscribe(_ => InvalidateFormattedText()); } public int CaretIndex @@ -79,6 +86,24 @@ namespace Avalonia.Controls.Presenters set => SetValue(PasswordCharProperty, value); } + public IBrush SelectionBrush + { + get => GetValue(SelectionBrushProperty); + set => SetValue(SelectionBrushProperty, value); + } + + public IBrush SelectionForegroundBrush + { + get => GetValue(SelectionForegroundBrushProperty); + set => SetValue(SelectionForegroundBrushProperty, value); + } + + public IBrush CaretBrush + { + get => GetValue(CaretBrushProperty); + set => SetValue(CaretBrushProperty, value); + } + public int SelectionStart { get @@ -129,14 +154,9 @@ namespace Avalonia.Controls.Presenters var rects = FormattedText.HitTestTextRange(start, length); - if (_highlightBrush == null) - { - _highlightBrush = (IBrush)this.FindResource("HighlightBrush"); - } - foreach (var rect in rects) { - context.FillRectangle(_highlightBrush, rect); + context.FillRectangle(SelectionBrush, rect); } } @@ -144,16 +164,21 @@ namespace Avalonia.Controls.Presenters if (selectionStart == selectionEnd) { - var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; - var caretBrush = Brushes.Black; + var caretBrush = CaretBrush; - if (backgroundColor.HasValue) + if (caretBrush is null) { - byte red = (byte)~(backgroundColor.Value.R); - byte green = (byte)~(backgroundColor.Value.G); - byte blue = (byte)~(backgroundColor.Value.B); - - caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color; + if (backgroundColor.HasValue) + { + byte red = (byte)~(backgroundColor.Value.R); + byte green = (byte)~(backgroundColor.Value.G); + byte blue = (byte)~(backgroundColor.Value.B); + + caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue)); + } + else + caretBrush = Brushes.Black; } if (_caretBlink) @@ -252,7 +277,7 @@ namespace Avalonia.Controls.Presenters { result.Spans = new[] { - new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White), + new FormattedTextStyleSpan(start, length, SelectionForegroundBrush), }; } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..058658357f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; @@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 90020839d6..d2e8f1ab92 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.Primitives /// public void SnapInsideScreenEdges() { - var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position); + var screen = (VisualRoot as WindowBase)?.Screens?.ScreenFromPoint(Position); if (screen != null) { diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 76df94cdb8..f1ee7c0e1a 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -44,13 +44,13 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty SmallChangeProperty = - AvaloniaProperty.Register(nameof(SmallChange), 0.1); + AvaloniaProperty.Register(nameof(SmallChange), 1); /// /// Defines the property. /// public static readonly StyledProperty LargeChangeProperty = - AvaloniaProperty.Register(nameof(LargeChange), 1); + AvaloniaProperty.Register(nameof(LargeChange), 10); private double _minimum; private double _maximum = 100.0; @@ -75,10 +75,18 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateMinimum(value); - SetAndRaise(MinimumProperty, ref _minimum, value); - Maximum = ValidateMaximum(Maximum); - Value = ValidateValue(Value); + ValidateDouble(value, "Minimum"); + + if (IsInitialized) + { + SetAndRaise(MinimumProperty, ref _minimum, value); + Maximum = ValidateMaximum(Maximum); + Value = ValidateValue(Value); + } + else + { + SetAndRaise(MinimumProperty, ref _minimum, value); + } } } @@ -94,9 +102,18 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateMaximum(value); - SetAndRaise(MaximumProperty, ref _maximum, value); - Value = ValidateValue(Value); + ValidateDouble(value, "Maximum"); + + if (IsInitialized) + { + value = ValidateMaximum(value); + SetAndRaise(MaximumProperty, ref _maximum, value); + Value = ValidateValue(Value); + } + else + { + SetAndRaise(MaximumProperty, ref _maximum, value); + } } } @@ -112,8 +129,17 @@ namespace Avalonia.Controls.Primitives set { - value = ValidateValue(value); - SetAndRaise(ValueProperty, ref _value, value); + ValidateDouble(value, "Value"); + + if (IsInitialized) + { + value = ValidateValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + else + { + SetAndRaise(ValueProperty, ref _value, value); + } } } @@ -129,6 +155,14 @@ namespace Avalonia.Controls.Primitives set => SetValue(LargeChangeProperty, value); } + protected override void OnInitialized() + { + base.OnInitialized(); + + Maximum = ValidateMaximum(Maximum); + Value = ValidateValue(Value); + } + /// /// Throws an exception if the double value is NaN or Inf. /// @@ -142,17 +176,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Validates the property. - /// - /// The value. - /// The coerced value. - private double ValidateMinimum(double value) - { - ValidateDouble(value, "Minimum"); - return value; - } - /// /// Validates/coerces the property. /// @@ -160,7 +183,6 @@ namespace Avalonia.Controls.Primitives /// The coerced value. private double ValidateMaximum(double value) { - ValidateDouble(value, "Maximum"); return Math.Max(value, Minimum); } @@ -171,7 +193,6 @@ namespace Avalonia.Controls.Primitives /// The coerced value. private double ValidateValue(double value) { - ValidateDouble(value, "Value"); return MathUtilities.Clamp(value, Minimum, Maximum); } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index f0d8c81808..c6119e89dc 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -216,25 +217,25 @@ namespace Avalonia.Controls.Primitives private void SmallDecrement() { - Value = Math.Max(Value - SmallChange * ViewportSize, Minimum); + Value = Math.Max(Value - SmallChange, Minimum); OnScroll(ScrollEventType.SmallDecrement); } private void SmallIncrement() { - Value = Math.Min(Value + SmallChange * ViewportSize, Maximum); + Value = Math.Min(Value + SmallChange, Maximum); OnScroll(ScrollEventType.SmallIncrement); } private void LargeDecrement() { - Value = Math.Max(Value - LargeChange * ViewportSize, Minimum); + Value = Math.Max(Value - LargeChange, Minimum); OnScroll(ScrollEventType.LargeDecrement); } private void LargeIncrement() { - Value = Math.Min(Value + LargeChange * ViewportSize, Maximum); + Value = Math.Min(Value + LargeChange, Maximum); OnScroll(ScrollEventType.LargeIncrement); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7b91d6235d..c8c15bc079 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -12,6 +12,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Logging; using Avalonia.Styling; using Avalonia.VisualTree; @@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); + private readonly Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; private IList _selectedItems; @@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action notifyWrapper) => - { - var old = backing; - var effective = (val >= 0 && val < Items?.Cast().Count()) ? val : -1; - - if (old != effective) - { - backing = effective; - notifyWrapper(() => - RaisePropertyChanged( - SelectedIndexProperty, - old, - effective, - BindingPriority.LocalValue)); - SelectedItem = ElementAt(Items, effective); - } - }, value); + var effective = (value >= 0 && value < ItemCount) ? value : -1; + UpdateSelectedItem(effective); } else { @@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - var index = IndexOf(Items, val); - var effective = index != -1 ? val : null; - - if (!object.Equals(effective, old)) - { - backing = effective; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - effective, - BindingPriority.LocalValue)); - - SelectedIndex = index; - - if (effective != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != effective) - { - _syncingSelectedItems = true; - SelectedItems.Clear(); - SelectedItems.Add(effective); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - } - }, value); + UpdateSelectedItem(IndexOf(Items, value)); } else { @@ -269,6 +222,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the selection mode. /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } @@ -354,36 +311,33 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } + else + { + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + UpdateSelectedItem(_selection.First(), false); + } break; case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - var selectedIndex = SelectedIndex; - - if (selectedIndex >= e.OldStartingIndex && - selectedIndex < e.OldStartingIndex + e.OldItems.Count) - { - if (!AlwaysSelected) - { - selectedIndex = SelectedIndex = -1; - } - else - { - LostSelection(); - } - } + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + UpdateSelectedItem(_selection.First(), false); + ResetSelectedItems(); + break; - var items = Items?.Cast(); - if (selectedIndex >= items.Count()) - { - selectedIndex = SelectedIndex = items.Count() - 1; - } + case NotifyCollectionChangedAction.Replace: + UpdateSelectedItem(SelectedIndex, false); + ResetSelectedItems(); break; case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(Items, SelectedItem); + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; + } break; } } @@ -393,24 +347,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var selectedIndex = SelectedIndex; - var selectedContainer = e.Containers - .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + var resetSelectedItems = false; - if (selectedContainer != null) - { - SelectedIndex = selectedContainer.Index; - } - else if (selectedIndex >= e.StartingIndex && - selectedIndex < e.StartingIndex + e.Containers.Count) + foreach (var container in e.Containers) { - var container = e.Containers[selectedIndex - e.StartingIndex]; + if ((container.ContainerControl as ISelectable)?.IsSelected == true) + { + if (SelectedIndex == -1) + { + SelectedIndex = container.Index; + } + else + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } - if (container.ContainerControl != null) + MarkContainerSelected(container.ContainerControl, true); + } + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// @@ -439,11 +405,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - var ms = MemberSelector; - bool selected = ms == null ? - SelectedItems.Contains(i.Item) : - SelectedItems.OfType().Any(v => Equals(ms.Select(v), i.Item)); - + bool selected = _selection.Contains(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -476,9 +438,12 @@ namespace Avalonia.Controls.Primitives var keymap = AvaloniaLocator.Current.GetService(); bool Match(List gestures) => gestures.Any(g => g.Matches(e)); - if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) + if (ItemCount > 0 && + Match(keymap.SelectAll) && + (((SelectionMode & SelectionMode.Multiple) != 0) || + (SelectionMode & SelectionMode.Toggle) != 0)) { - SynchronizeItems(SelectedItems, Items?.Cast()); + SelectAll(); e.Handled = true; } } @@ -520,6 +485,36 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Selects all items in the control. + /// + protected void SelectAll() + { + UpdateSelectedItems(() => + { + _selection.Clear(); + + for (var i = 0; i < ItemCount; ++i) + { + _selection.Add(i); + } + + UpdateSelectedItem(0, false); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, true); + } + + ResetSelectedItems(); + }); + } + + /// + /// Deselects all items in the control. + /// + protected void UnselectAll() => UpdateSelectedItem(-1); + /// /// Updates the selection for an item based on user interaction. /// @@ -527,51 +522,90 @@ namespace Avalonia.Controls.Primitives /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( int index, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { if (index != -1) { if (select) { var mode = SelectionMode; - var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0; var multi = (mode & SelectionMode.Multiple) != 0; - var range = multi && SelectedIndex != -1 && rangeModifier; + var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); + var range = multi && rangeModifier; - if (!toggle && !range) + if (rightButton) { - SelectedIndex = index; + if (!_selection.Contains(index)) + { + UpdateSelectedItem(index); + } } - else if (multi && range) + else if (range) { - SynchronizeItems( - SelectedItems, - GetRange(Items, SelectedIndex, index)); + UpdateSelectedItems(() => + { + var start = SelectedIndex != -1 ? SelectedIndex : 0; + var step = start < index ? 1 : -1; + + _selection.Clear(); + + for (var i = start; i != index; i += step) + { + _selection.Add(i); + } + + _selection.Add(index); + + var first = Math.Min(start, index); + var last = Math.Max(start, index); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected( + container.Index, + container.Index >= first && container.Index <= last); + } + + ResetSelectedItems(); + }); } - else + else if (multi && toggle) { - var item = ElementAt(Items, index); - var i = SelectedItems.IndexOf(item); - - if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) + UpdateSelectedItems(() => { - SelectedItems.Remove(item); - } - else - { - if (multi) + if (!_selection.Contains(index)) { - SelectedItems.Add(item); + _selection.Add(index); + MarkItemSelected(index, true); + SelectedItems.Add(ElementAt(Items, index)); } else { - SelectedIndex = index; + _selection.Remove(index); + MarkItemSelected(index, false); + + if (index == _selectedIndex) + { + UpdateSelectedItem(_selection.First(), false); + } + + SelectedItems.Remove(ElementAt(Items, index)); } - } + }); + } + else if (toggle) + { + SelectedIndex = (SelectedIndex == index) ? -1 : index; + } + else + { + UpdateSelectedItem(index); } if (Presenter?.Panel != null) @@ -596,17 +630,19 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelection( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; if (index != -1) { - UpdateSelection(index, select, rangeModifier, toggleModifier); + UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton); } } @@ -618,6 +654,7 @@ namespace Avalonia.Controls.Primitives /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -626,51 +663,20 @@ namespace Avalonia.Controls.Primitives IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelection(container, select, rangeModifier, toggleModifier); + UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton); return true; } return false; } - /// - /// Makes a list of objects equal another. - /// - /// The items collection. - /// The desired items. - internal static void SynchronizeItems(IList items, IEnumerable desired) - { - var index = 0; - - foreach (object item in desired) - { - int itemIndex = items.IndexOf(item); - - if (itemIndex == -1) - { - items.Insert(index, item); - } - else if(itemIndex != index) - { - items.RemoveAt(itemIndex); - items.Insert(index, item); - } - - ++index; - } - - while (index < items.Count) - { - items.RemoveAt(items.Count - 1); - } - } - /// /// Gets a range of items from an IEnumerable. /// @@ -678,17 +684,19 @@ namespace Avalonia.Controls.Primitives /// The index of the first item. /// The index of the last item. /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) + private static List GetRange(IEnumerable items, int first, int last) { var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; + var step = first > last ? -1 : 1; + var result = new List(); for (int i = first; i != last; i += step) { - yield return list[i]; + result.Add(list[i]); } - yield return list[last]; + result.Add(list[last]); + return result; } /// @@ -724,19 +732,14 @@ namespace Avalonia.Controls.Primitives private void LostSelection() { var items = Items?.Cast(); + var index = -1; if (items != null && AlwaysSelected) { - var index = Math.Min(SelectedIndex, items.Count() - 1); - - if (index > -1) - { - SelectedItem = items.ElementAt(index); - return; - } + index = Math.Min(SelectedIndex, items.Count() - 1); } - SelectedIndex = -1; + SelectedIndex = index; } /// @@ -793,7 +796,7 @@ namespace Avalonia.Controls.Primitives /// /// The item. /// Whether the item should be selected or deselected. - private void MarkItemSelected(object item, bool selected) + private int MarkItemSelected(object item, bool selected) { var index = IndexOf(Items, item); @@ -801,6 +804,21 @@ namespace Avalonia.Controls.Primitives { MarkItemSelected(index, selected); } + + return index; + } + + private void ResetSelectedItems() + { + UpdateSelectedItems(() => + { + SelectedItems.Clear(); + + foreach (var i in _selection) + { + SelectedItems.Add(ElementAt(Items, i)); + } + }); } /// @@ -810,95 +828,97 @@ namespace Avalonia.Controls.Primitives /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - var generator = ItemContainerGenerator; + if (_syncingSelectedItems) + { + return; + } + + void Add(IList newItems, IList addedItems = null) + { + foreach (var item in newItems) + { + var index = MarkItemSelected(item, true); + + if (index != -1 && _selection.Add(index) && addedItems != null) + { + addedItems.Add(item); + } + } + } + + void UpdateSelection() + { + if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || + (SelectedIndex == -1 && _selection.HasItems)) + { + _selectedIndex = _selection.First(); + _selectedItem = ElementAt(Items, _selectedIndex); + RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); + RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(_selectedIndex); + } + } + } + IList added = null; IList removed = null; switch (e.Action) { case NotifyCollectionChangedAction.Add: - SelectedItemsAdded(e.NewItems.Cast().ToList()); - - if (AutoScrollToSelectedItem) { - ScrollIntoView(e.NewItems[0]); + Add(e.NewItems); + UpdateSelection(); + added = e.NewItems; } - added = e.NewItems; break; case NotifyCollectionChangedAction.Remove: if (SelectedItems.Count == 0) { - if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } + SelectedIndex = -1; } foreach (var item in e.OldItems) { - MarkItemSelected(item, false); + var index = MarkItemSelected(item, false); + _selection.Remove(index); } removed = e.OldItems; break; + case NotifyCollectionChangedAction.Replace: + throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); + + case NotifyCollectionChangedAction.Move: + throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); + case NotifyCollectionChangedAction.Reset: - if (generator != null) { removed = new List(); + added = new List(); - foreach (var item in generator.Containers) + foreach (var index in _selection.ToList()) { - if (item?.ContainerControl != null) + var item = ElementAt(Items, index); + + if (!SelectedItems.Contains(item)) { - if (MarkContainerSelected(item.ContainerControl, false)) - { - removed.Add(item.Item); - } + MarkItemSelected(index, false); + removed.Add(item); + _selection.Remove(index); } } - } - - if (SelectedItems.Count > 0) - { - _selectedItem = null; - SelectedItemsAdded(SelectedItems); - added = SelectedItems; - } - else if (!_syncingSelectedItems) - { - SelectedIndex = -1; - } - - break; - - case NotifyCollectionChangedAction.Replace: - foreach (var item in e.OldItems) - { - MarkItemSelected(item, false); - } - - foreach (var item in e.NewItems) - { - MarkItemSelected(item, true); - } - if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems) - { - var oldItem = SelectedItem; - var oldIndex = SelectedIndex; - var item = SelectedItems[0]; - var index = IndexOf(Items, item); - _selectedIndex = index; - _selectedItem = item; - RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue); + Add(SelectedItems, added); + UpdateSelection(); } - added = e.NewItems; - removed = e.OldItems; break; } @@ -912,34 +932,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Called when items are added to the collection. - /// - /// The added items. - private void SelectedItemsAdded(IList items) - { - if (items.Count > 0) - { - foreach (var item in items) - { - MarkItemSelected(item, true); - } - - if (SelectedItem == null && !_syncingSelectedItems) - { - var index = IndexOf(Items, items[0]); - - if (index != -1) - { - _selectedItem = items[0]; - _selectedIndex = index; - RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue); - } - } - } - } - /// /// Subscribes to the CollectionChanged event, if any. /// @@ -970,6 +962,112 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Updates the selection due to a change to or + /// . + /// + /// The new selected index. + /// Whether to clear existing selection. + private void UpdateSelectedItem(int index, bool clear = true) + { + var oldIndex = _selectedIndex; + var oldItem = _selectedItem; + + if (index == -1 && AlwaysSelected) + { + index = Math.Min(SelectedIndex, ItemCount - 1); + } + + var item = ElementAt(Items, index); + var added = -1; + HashSet removed = null; + + _selectedIndex = index; + _selectedItem = item; + + if (oldIndex != index || _selection.HasMultiple) + { + if (clear) + { + removed = _selection.Clear(); + } + + if (index != -1) + { + if (_selection.Add(index)) + { + added = index; + } + + if (removed?.Contains(index) == true) + { + removed.Remove(index); + added = -1; + } + } + + if (removed != null) + { + foreach (var i in removed) + { + MarkItemSelected(i, false); + } + } + + MarkItemSelected(index, true); + + RaisePropertyChanged( + SelectedIndexProperty, + oldIndex, + index); + } + + if (!Equals(item, oldItem)) + { + RaisePropertyChanged( + SelectedItemProperty, + oldItem, + item); + } + + if (removed != null && index != -1) + { + removed.Remove(index); + } + + if (added != -1 || removed?.Count > 0) + { + ResetSelectedItems(); + + var e = new SelectionChangedEventArgs( + SelectionChangedEvent, + added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), + removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); + RaiseEvent(e); + } + } + + private void UpdateSelectedItems(Action action) + { + try + { + _syncingSelectedItems = true; + action(); + } + catch (Exception ex) + { + Logger.Error( + LogArea.Property, + this, + "Error thrown updating SelectedItems: {Error}", + ex); + } + finally + { + _syncingSelectedItems = false; + } + } + private void UpdateFinished() { if (_updateSelectedIndex != int.MinValue) @@ -981,5 +1079,104 @@ namespace Avalonia.Controls.Primitives SelectedItems = _updateSelectedItems; } } + + private class Selection : IEnumerable + { + private readonly List _list = new List(); + private HashSet _set = new HashSet(); + + public bool HasItems => _set.Count > 0; + public bool HasMultiple => _set.Count > 1; + + public bool Add(int index) + { + if (index == -1) + { + throw new ArgumentException("Invalid index", "index"); + } + + if (_set.Add(index)) + { + _list.Add(index); + return true; + } + + return false; + } + + public bool Remove(int index) + { + if (_set.Remove(index)) + { + _list.RemoveAll(x => x == index); + return true; + } + + return false; + } + + public HashSet Clear() + { + var result = _set; + _list.Clear(); + _set = new HashSet(); + return result; + } + + public void ItemsInserted(int index, int count) + { + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index) + { + var newIndex = ix + count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public void ItemsRemoved(int index, int count) + { + var last = (index + count) - 1; + + _set = new HashSet(); + + for (var i = 0; i < _list.Count; ++i) + { + var ix = _list[i]; + + if (ix >= index && ix <= last) + { + _list.RemoveAt(i--); + } + else if (ix > last) + { + var newIndex = ix - count; + _list[i] = newIndex; + _set.Add(newIndex); + } + else + { + _set.Add(ix); + } + } + } + + public bool Contains(int index) => _set.Contains(index); + + public int First() => HasItems ? _list[0] : -1; + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..ec0dbd124c 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -12,11 +13,8 @@ namespace Avalonia.Controls.Primitives private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); - static TabStrip() { - MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -51,11 +49,5 @@ namespace Avalonia.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } - - private static object SelectHeader(object o) - { - var headered = o as IHeadered; - return (headered != null) ? (headered.Header ?? string.Empty) : o; - } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 32e220b789..47c3240374 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -257,13 +257,14 @@ namespace Avalonia.Controls.Primitives { Logger.Verbose(LogArea.Control, this, "Creating control template"); - var child = template.Build(this); - var nameScope = new NameScope(); - NameScope.SetNameScope((Control)child, nameScope); + var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); - RegisterNames(child, nameScope); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); + + // Existing code kinda expect to see a NameScope even if it's empty + if (nameScope == null) + nameScope = new NameScope(); OnTemplateApplied(new TemplateAppliedEventArgs(nameScope)); } @@ -342,26 +343,5 @@ namespace Avalonia.Controls.Primitives } } } - - /// - /// Registers each control with its name scope. - /// - /// The control. - /// The name scope. - private void RegisterNames(IControl control, INameScope nameScope) - { - if (control.Name != null) - { - nameScope.Register(control.Name, control); - } - - if (control.TemplatedParent == this) - { - foreach (IControl child in control.GetLogicalChildren()) - { - RegisterNames(child, nameScope); - } - } - } } } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index c96fea6c25..21a7dd68f8 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Metadata; namespace Avalonia.Controls.Primitives diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a0f51099cd..29e3a17f74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -33,8 +34,8 @@ namespace Avalonia.Controls static ProgressBar() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); @@ -120,4 +121,4 @@ namespace Avalonia.Controls UpdateIndicator(Bounds.Size); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs new file mode 100644 index 0000000000..04d859c742 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -0,0 +1,54 @@ +// 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 Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class ItemTemplateWrapper + { + private readonly IDataTemplate _dataTemplate; + + public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; + + public IControl GetElement(IControl parent, object data) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + IControl element = null; + + if (recyclePool != null) + { + // try to get an element from the recycle pool. + element = recyclePool.TryGetElement(string.Empty, parent); + } + + if (element == null) + { + // no element was found in recycle pool, create a new element + element = selectedTemplate.Build(data); + + // Associate template with element + element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); + } + + return element; + } + + public void RecycleElement(IControl parent, IControl element) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + if (recyclePool == null) + { + // No Recycle pool in the template, create one. + recyclePool = new RecyclePool(); + RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); + } + + recyclePool.PutElement(element, "" /* key */, parent); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs new file mode 100644 index 0000000000..44783e2c97 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -0,0 +1,724 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Represents a data-driven collection control that incorporates a flexible layout system, + /// custom views, and virtualization. + /// + public class ItemsRepeater : Panel + { + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemTemplateProperty = + ItemsControl.ItemTemplateProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty VerticalCacheLengthProperty = + AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + + private static readonly AttachedProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + + internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); + internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); + + private readonly ViewManager _viewManager; + private readonly ViewportManager _viewportManager; + private IEnumerable _items; + private VirtualizingLayoutContext _layoutContext; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private bool _isLayoutInProgress; + private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; + private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; + private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + + /// + /// Initializes a new instance of the class. + /// + public ItemsRepeater() + { + _viewManager = new ViewManager(this); + _viewportManager = new ViewportManager(this); + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + OnLayoutChanged(null, Layout); + } + + static ItemsRepeater() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + } + + /// + /// Gets or sets the layout used to size and position elements in the ItemsRepeater. + /// + /// + /// The layout used to size and position elements. The default is a StackLayout with + /// vertical orientation. + /// + public AttachedLayout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + /// + /// Gets or sets an object source used to generate the content of the ItemsRepeater. + /// + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + /// + /// Gets or sets the template used to display each item. + /// + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling horizontally. + /// + public double HorizontalCacheLength + { + get => GetValue(HorizontalCacheLengthProperty); + set => SetValue(HorizontalCacheLengthProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling vertically. + /// + public double VerticalCacheLength + { + get => GetValue(VerticalCacheLengthProperty); + set => SetValue(VerticalCacheLengthProperty, value); + } + + /// + /// Gets a standardized view of the supported interactions between a given Items object and + /// the ItemsRepeater control and its associated components. + /// + public ItemsSourceView ItemsSourceView { get; private set; } + + internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal Point LayoutOrigin { get; set; } + internal object LayoutState { get; set; } + internal IControl MadeAnchor => _viewportManager.MadeAnchor; + internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); + internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; + + private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; + + private LayoutContext LayoutContext + { + get + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } + + /// + /// Occurs each time an element is cleared and made available to be re-used. + /// + /// + /// This event is raised immediately each time an element is cleared, such as when it falls + /// outside the range of realized items. Elements are cleared when they become available + /// for re-use. + /// + public event EventHandler ElementClearing; + + /// + /// Occurs for each realized when the index for the item it + /// represents has changed. + /// + /// + /// When you use ItemsRepeater to build a more complex control that supports specific + /// interactions on the child elements (such as selection or click), it is useful to be + /// able to keep an up-to-date identifier for the backing data item. + /// + /// This event is raised for each realized IControl where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// + public event EventHandler ElementIndexChanged; + + /// + /// Occurs each time an element is prepared for use. + /// + /// + /// The prepared element might be newly created or an existing element that is being re- + /// used. + /// + public event EventHandler ElementPrepared; + + /// + /// Retrieves the index of the item from the data source that corresponds to the specified + /// . + /// + /// + /// The element that corresponds to the item to get the index of. + /// + /// + /// The index of the item from the data source that corresponds to the specified UIElement, + /// or -1 if the element is not supported. + /// + public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + + /// + /// Retrieves the realized UIElement that corresponds to the item at the specified index in + /// the data source. + /// + /// The index of the item. + /// + /// he UIElement that corresponds to the item at the specified index if the item is + /// realized, or null if the item is not realized. + /// + public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + + internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + + internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + + internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + { + var value = element.GetValue(VirtualizationInfoProperty); + return value; + } + + internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) + { + if (TryGetVirtualizationInfo(element) != null) + { + throw new InvalidOperationException("VirtualizationInfo already created."); + } + + var result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + return result; + } + + internal static VirtualizationInfo GetVirtualizationInfo(IControl element) + { + var result = element.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + } + + return result; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _viewportManager.OnOwnerMeasuring(); + + _isLayoutInProgress = true; + + try + { + _viewManager.PrunePinnedElements(); + var extent = new Rect(); + var desiredSize = new Size(); + var layout = Layout; + + if (layout != null) + { + var layoutContext = GetLayoutContext(); + + desiredSize = layout.Measure(layoutContext, availableSize); + extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); + + // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index). + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + + if (virtInfo.Owner == ElementOwner.Layout && + virtInfo.AutoRecycleCandidate && + !virtInfo.KeepAlive) + { + ClearElementImpl(element); + } + } + } + + _viewportManager.SetLayoutExtent(extent); + return desiredSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _isLayoutInProgress = true; + + try + { + var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + + // The view manager might clear elements during this call. + // That's why we call it before arranging cleared elements + // off screen. + _viewManager.OnOwnerArranged(); + + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + virtInfo.KeepAlive = false; + + if (virtInfo.Owner == ElementOwner.ElementFactory || + virtInfo.Owner == ElementOwner.PinnedPool) + { + // Toss it away. And arrange it with size 0 so that XYFocus won't use it. + element.Arrange(new Rect( + ClearedElementsArrangePosition.X - element.DesiredSize.Width, + ClearedElementsArrangePosition.Y - element.DesiredSize.Height, + 0, + 0)); + } + else + { + var newBounds = element.Bounds; + virtInfo.ArrangeBounds = newBounds; + } + } + + _viewportManager.OnOwnerArranged(); + + return arrangeSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + InvalidateMeasure(); + _viewportManager.ResetScrollers(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewportManager.ResetScrollers(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var property = args.Property; + + if (property == ItemsProperty) + { + var newValue = (IEnumerable)args.NewValue; + var newDataSource = newValue as ItemsSourceView; + if (newValue != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newValue); + } + + OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + } + else if (property == ItemTemplateProperty) + { + OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + } + else if (property == LayoutProperty) + { + OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + } + else if (property == HorizontalCacheLengthProperty) + { + _viewportManager.HorizontalCacheLength = (double)args.NewValue; + } + else if (property == VerticalCacheLengthProperty) + { + _viewportManager.VerticalCacheLength = (double)args.NewValue; + } + else + { + base.OnPropertyChanged(args); + } + } + + internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + { + var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + return element; + } + + internal void ClearElementImpl(IControl element) + { + // Clearing an element due to a collection change + // is more strict in that pinned elements will be forcibly + // unpinned and sent back to the view generator. + var isClearedDueToCollectionChange = + _processingItemsSourceChange != null && + (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); + + _viewManager.ClearElement(element, isClearedDueToCollectionChange); + _viewportManager.OnElementCleared(element); + } + + private int GetElementIndexImpl(IControl element) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + private IControl GetElementFromIndexImpl(int index) + { + IControl result = null; + + var children = Children; + for (var i = 0; i < children.Count && result == null; ++i) + { + var element = children[i]; + var virtInfo = TryGetVirtualizationInfo(element); + if (virtInfo?.IsRealized == true && virtInfo.Index == index) + { + result = element; + } + } + + return result; + } + + private IControl GetOrCreateElementImpl(int index) + { + if (index >= 0 && index >= ItemsSourceView.Count) + { + throw new ArgumentException("Argument index is invalid.", "index"); + } + + if (_isLayoutInProgress) + { + throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); + } + + var element = GetElementFromIndexImpl(index); + bool isAnchorOutsideRealizedRange = element == null; + + if (isAnchorOutsideRealizedRange) + { + if (Layout == null) + { + throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); + } + + element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); + element.Measure(Size.Infinity); + } + + _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); + InvalidateMeasure(); + + return element; + } + + internal void OnElementPrepared(IControl element, int index) + { + _viewportManager.OnElementPrepared(element); + if (ElementPrepared != null) + { + if (_elementPreparedArgs == null) + { + _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + } + else + { + _elementPreparedArgs.Update(element, index); + } + + ElementPrepared(this, _elementPreparedArgs); + } + } + + internal void OnElementClearing(IControl element) + { + if (ElementClearing != null) + { + if (_elementClearingArgs == null) + { + _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + } + else + { + _elementClearingArgs.Update(element); + } + + ElementClearing(this, _elementClearingArgs); + } + } + + internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) + { + if (ElementIndexChanged != null) + { + if (_elementIndexChangedArgs == null) + { + _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + } + else + { + _elementIndexChangedArgs.Update(element, oldIndex, newIndex); + } + + ElementIndexChanged(this, _elementIndexChangedArgs); + } + } + + private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); + } + + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + + if (oldValue != null) + { + oldValue.CollectionChanged -= OnItemsSourceViewChanged; + } + + if (newValue != null) + { + newValue.CollectionChanged += OnItemsSourceViewChanged; + } + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + + InvalidateMeasure(); + } + } + + private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) + { + if (_isLayoutInProgress && oldValue != null) + { + throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); + } + + // Since the ItemTemplate has changed, we need to re-evaluate all the items that + // have already been created and are now in the tree. The easiest way to do that + // would be to do a reset.. Note that this has to be done before we change the template + // so that the cleared elements go back into the old template. + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } + } + else if (Layout is NonVirtualizingLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + } + + ItemTemplateShim = new ItemTemplateWrapper(newValue); + + InvalidateMeasure(); + } + + private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) + { + if (_isLayoutInProgress) + { + throw new InvalidOperationException("Layout cannot be changed during layout."); + } + + _viewManager.OnLayoutChanging(); + + if (oldValue != null) + { + oldValue.UninitializeForContext(LayoutContext); + oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; + oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + // Walk through all the elements and make sure they are cleared + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + + LayoutState = null; + } + + if (newValue != null) + { + newValue.InitializeForContext(LayoutContext); + newValue.MeasureInvalidated += InvalidateMeasureForLayout; + newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + } + + bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + _viewportManager.OnLayoutChanged(isVirtualizingLayout); + InvalidateMeasure(); + } + + private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_isLayoutInProgress) + { + // Bad things will follow if the data changes while we are in the middle of a layout pass. + throw new InvalidOperationException("Changes in data source are not allowed during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); + } + + _processingItemsSourceChange = args; + + try + { + _viewManager.OnItemsSourceChanged(sender, args); + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); + } + else + { + // NonVirtualizingLayout + InvalidateMeasure(); + } + } + } + finally + { + _processingItemsSourceChange = null; + } + } + + private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure(); + + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange(); + + private VirtualizingLayoutContext GetLayoutContext() + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs new file mode 100644 index 0000000000..75d50e52a6 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs @@ -0,0 +1,24 @@ +// 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; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementClearingEventArgs : EventArgs + { + internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + + /// + /// Gets the element that is being cleared for re-use. + /// + public IControl Element { get; private set; } + + internal void Update(IControl element) => Element = element; + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs new file mode 100644 index 0000000000..7ca68140b2 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -0,0 +1,44 @@ +// 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; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + { + internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + + /// + /// Get the element for which the index changed. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the element after the change. + /// + public int NewIndex { get; private set; } + + /// + /// Gets the index of the element before the change. + /// + public int OldIndex { get; private set; } + + internal void Update(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs new file mode 100644 index 0000000000..5a30dbcf2a --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs @@ -0,0 +1,35 @@ +// 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. + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementPreparedEventArgs + { + internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + { + Element = element; + Index = index; + } + + /// + /// Gets the prepared element. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the item the element was prepared for. + /// + public int Index { get; private set; } + + internal void Update(IControl element, int index) + { + Element = element; + Index = index; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs new file mode 100644 index 0000000000..732ba8501c --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -0,0 +1,143 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls +{ + /// + /// Represents a standardized view of the supported interactions between a given ItemsSource + /// object and an control. + /// + /// + /// Components written to work with ItemsRepeater should consume the + /// via ItemsSourceView since this provides a normalized + /// view of the Items. That way, each component does not need to know if the source is an + /// IEnumerable, an IList, or something else. + /// + public class ItemsSourceView : INotifyCollectionChanged, IDisposable + { + private readonly IList _inner; + private INotifyCollectionChanged _notifyCollectionChanged; + private int _cachedSize = -1; + + /// + /// Initializes a new instance of the ItemsSourceView class for the specified data source. + /// + /// The data source. + public ItemsSourceView(IEnumerable source) + { + Contract.Requires(source != null); + + _inner = source as IList; + + if (_inner == null && source is IEnumerable objectEnumerable) + { + _inner = new List(objectEnumerable); + } + else + { + _inner = new List(source.Cast()); + } + + ListenToCollectionChanges(); + } + + /// + /// Gets the number of items in the collection. + /// + public int Count + { + get + { + if (_cachedSize == -1) + { + _cachedSize = _inner.Count; + } + + return _cachedSize; + } + } + + /// + /// Gets a value that indicates whether the items source can provide a unique key for each item. + /// + /// + /// TODO: Not yet implemented in Avalonia. + /// + public bool HasKeyIndexMapping => false; + + /// + /// Occurs when the collection has changed to indicate the reason for the change and which items changed. + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + public void Dispose() + { + if (_notifyCollectionChanged != null) + { + _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + } + } + + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// the item. + public object GetAt(int index) => _inner[index]; + + /// + /// Retrieves the index of the item that has the specified unique identifier (key). + /// + /// The index. + /// The key + /// + /// TODO: Not yet implemented in Avalonia. + /// + public string KeyFromIndex(int index) + { + throw new NotImplementedException(); + } + + /// + /// Retrieves the unique identifier (key) for the item at the specified index. + /// + /// The key. + /// The index. + /// + /// TODO: Not yet implemented in Avalonia. + /// + public int IndexFromKey(string key) + { + throw new NotImplementedException(); + } + + protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) + { + _cachedSize = _inner.Count; + CollectionChanged?.Invoke(this, args); + } + + private void ListenToCollectionChanges() + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + _notifyCollectionChanged = incc; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnItemsSourceChanged(e); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs new file mode 100644 index 0000000000..4e5950bdc5 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -0,0 +1,106 @@ +// 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; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class RecyclePool + { + public static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + + private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); + private readonly Dictionary> _elements = new Dictionary>(); + + public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate) + { + s_pools.TryGetValue(dataTemplate, out var result); + return result; + } + + public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value); + + public void PutElement(IControl element, string key, IControl owner) + { + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + var elementInfo = new ElementInfo(element, ownerAsPanel); + + if (!_elements.TryGetValue(key, out var pool)) + { + pool = new List(); + _elements.Add(key, pool); + } + + pool.Add(elementInfo); + } + + public IControl TryGetElement(string key, IControl owner) + { + if (_elements.TryGetValue(key, out var elements)) + { + if (elements.Count > 0) + { + // Prefer an element from the same owner or with no owner so that we don't incur + // the enter/leave cost during recycling. + // TODO: prioritize elements with the same owner to those without an owner. + var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault(); + elements.Remove(elementInfo); + + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel) + { + // Element is still under its parent. remove it from its parent. + var panel = elementInfo.Owner; + if (panel != null) + { + int childIndex = panel.Children.IndexOf(elementInfo.Element); + if (childIndex == -1) + { + throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection."); + } + + panel.Children.RemoveAt(childIndex); + } + } + + return elementInfo.Element; + } + } + + return null; + } + + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) + { + if (owner is IPanel panel) + { + return panel; + } + else if (owner != null) + { + throw new InvalidOperationException("Owner must be IPanel or null."); + } + + return null; + } + + private class ElementInfo + { + public ElementInfo(IControl element, IPanel owner) + { + Element = element; + Owner = owner; + } + + public IControl Element { get; } + public IPanel Owner { get;} + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs new file mode 100644 index 0000000000..977d9d794c --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -0,0 +1,65 @@ +// 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.Text; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + internal class RepeaterLayoutContext : VirtualizingLayoutContext + { + private readonly ItemsRepeater _owner; + + public RepeaterLayoutContext(ItemsRepeater owner) + { + _owner = owner; + } + + protected override Point LayoutOriginCore + { + get => _owner.LayoutOrigin; + set => _owner.LayoutOrigin = value; + } + + protected override object LayoutStateCore + { + get => _owner.LayoutState; + set => _owner.LayoutState = value; + } + + protected override int RecommendedAnchorIndexCore + { + get + { + int anchorIndex = -1; + var anchor = _owner.SuggestedAnchor; + if (anchor != null) + { + anchorIndex = _owner.GetElementIndex(anchor); + } + + return anchorIndex; + } + } + + protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; + + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + { + return _owner.GetElementImpl( + index, + (options & ElementRealizationOptions.ForceCreate) != 0, + (options & ElementRealizationOptions.SuppressAutoRecycle) != 0); + } + + protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); + + protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); + + protected override Rect RealizationRectCore() => _owner.RealizationWindow; + } +} diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs new file mode 100644 index 0000000000..775aa3f113 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs @@ -0,0 +1,54 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + internal class UniqueIdElementPool : IEnumerable> + { + private readonly Dictionary _elementMap = new Dictionary(); + private readonly ItemsRepeater _owner; + + public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner; + + public void Add(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var key = virtInfo.UniqueId; + + if (_elementMap.ContainsKey(key)) + { + throw new InvalidOperationException($"The unique id provided ({key}) is not unique."); + } + + _elementMap.Add(key, element); + } + + public IControl Remove(int index) + { + // Check if there is already a element in the mapping and if so, use it. + string key = _owner.ItemsSourceView.KeyFromIndex(index); + + if (_elementMap.TryGetValue(key, out var element)) + { + _elementMap.Remove(key); + } + + return element; + } + + public void Clear() + { + _elementMap.Clear(); + } + + public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs new file mode 100644 index 0000000000..833e708e9e --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -0,0 +1,682 @@ +// 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.Collections.Specialized; +using System.Linq; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal sealed class ViewManager + { + private const int FirstRealizedElementIndexDefault = int.MaxValue; + private const int LastRealizedElementIndexDefault = int.MinValue; + + private readonly ItemsRepeater _owner; + private readonly List _pinnedPool = new List(); + private readonly UniqueIdElementPool _resetPool; + private IControl _lastFocusedElement; + private bool _isDataSourceStableResetPending; + private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + private bool _eventsSubscribed; + + public ViewManager(ItemsRepeater owner) + { + _owner = owner; + _resetPool = new UniqueIdElementPool(owner); + } + + public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle) + { + var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index); + if (element == null) + { + // check if this is the anchor made through repeater in preparation + // for a bring into view. + var madeAnchor = _owner.MadeAnchor; + if (madeAnchor != null) + { + var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); + if (anchorVirtInfo.Index == index) + { + element = madeAnchor; + } + } + } + if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; + if (element == null) { element = GetElementFromPinnedElements(index); } + if (element == null) { element = GetElementFromElementFactory(index); } + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (suppressAutoRecycle) + { + virtInfo.AutoRecycleCandidate = false; + } + else + { + virtInfo.AutoRecycleCandidate = true; + virtInfo.KeepAlive = true; + } + + return element; + } + + public void ClearElement(IControl element, bool isClearedDueToCollectionChange) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var index = virtInfo.Index; + bool cleared = + ClearElementToUniqueIdResetPool(element, virtInfo) || + ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); + + if (!cleared) + { + ClearElementToElementFactory(element); + } + + // Both First and Last indices need to be valid or default. + if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout) + { + // First and last were pointing to the same element and that is going away. + InvalidateRealizedIndicesHeldByLayout(); + } + else if (index == _firstRealizedElementIndexHeldByLayout) + { + // The FirstElement is going away, shrink the range by one. + ++_firstRealizedElementIndexHeldByLayout; + } + else if (index == _lastRealizedElementIndexHeldByLayout) + { + // Last element is going away, shrink the range by one at the end. + --_lastRealizedElementIndexHeldByLayout; + } + else + { + // Index is either outside the range we are keeping track of or inside the range. + // In both these cases, we just keep the range we have. If this clear was due to + // a collection change, then in the CollectionChanged event, we will invalidate these guys. + } + } + + public void ClearElementToElementFactory(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var clearedIndex = virtInfo.Index; + _owner.OnElementClearing(element); + _owner.ItemTemplateShim.RecycleElement(_owner, element); + + virtInfo.MoveOwnershipToElementFactory(); + + if (_lastFocusedElement == element) + { + // Focused element is going away. Remove the tracked last focused element + // and pick a reasonable next focus if we can find one within the layout + // realized elements. + MoveFocusFromClearedIndex(clearedIndex); + } + + } + + private void MoveFocusFromClearedIndex(int clearedIndex) + { + IControl focusedChild = null; + var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); + if (focusCandidate != null) + { + focusCandidate.Focus(); + _lastFocusedElement = focusedChild; + + // Add pin to hold the focused element. + UpdatePin(focusedChild, true /* addPin */); + } + else + { + // We could not find a candiate. + _lastFocusedElement = null; + } + } + + IControl FindFocusCandidate(int clearedIndex, IControl focusedChild) + { + // Walk through all the children and find elements with index before and after the cleared index. + // Note that during a delete the next element would now have the same index. + int previousIndex = int.MinValue; + int nextIndex = int.MaxValue; + IControl nextElement = null; + IControl previousElement = null; + + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + int currentIndex = virtInfo.Index; + if (currentIndex < clearedIndex) + { + if (currentIndex > previousIndex) + { + previousIndex = currentIndex; + previousElement = child; + } + } + else if (currentIndex >= clearedIndex) + { + // Note that we use >= above because if we deleted the focused element, + // the next element would have the same index now. + if (currentIndex < nextIndex) + { + nextIndex = currentIndex; + nextElement = child; + } + } + } + } + + // TODO: Find the next element if one exists, if not use the previous element. + // If the container itself is not focusable, find a descendent that is. + + return nextElement; + } + + public int GetElementIndex(VirtualizationInfo virtInfo) + { + if (virtInfo == null) + { + throw new ArgumentException("Element is not a child of this ItemsRepeater."); + } + + return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; + } + + public void PrunePinnedElements() + { + EnsureEventSubscriptions(); + + // Go through pinned elements and make sure they still have + // a reason to be pinned. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (!virtInfo.IsPinned) + { + _pinnedPool.RemoveAt(i); + --i; + + // Pinning was the only thing keeping this element alive. + ClearElementToElementFactory(elementInfo.PinnedElement); + } + } + } + + public void UpdatePin(IControl element, bool addPin) + { + var parent = element.VisualParent; + var child = (IVisual)element; + + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child); + if (virtInfo.IsRealized) + { + if (addPin) + { + virtInfo.AddPin(); + } + else if (virtInfo.IsPinned) + { + if (virtInfo.RemovePin() == 0) + { + // ElementFactory is invoked during the measure pass. + // We will clear the element then. + repeater.InvalidateMeasure(); + } + } + } + } + + child = parent; + parent = child.VisualParent; + } + } + + public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args) + { + // Note: For items that have been removed, the index will not be touched. It will hold + // the old index before it was removed. It is not valid anymore. + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + { + var newIndex = args.NewStartingIndex; + var newCount = args.NewItems.Count; + EnsureFirstLastRealizedIndices(); + if (newIndex <= _lastRealizedElementIndexHeldByLayout) + { + _lastRealizedElementIndexHeldByLayout += newCount; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + else + { + // Indices held by layout are not affected + // We could still have items in the pinned elements that need updates. This is usually a very small vector. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + var element = elementInfo.PinnedElement; + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true. + // Two cases here + // case 1: oldCount == newCount + // indices are not affected. nothing to do here. + // case 2: oldCount != newCount + // Replaced with less or more items. This is like an insert or remove + // depending on the counts. + var oldStartIndex = args.OldStartingIndex; + var newStartingIndex = args.NewStartingIndex; + var oldCount = args.OldItems.Count; + var newCount = args.NewItems.Count; + if (oldStartIndex != newStartingIndex) + { + throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); + } + + if (oldCount == 0) + { + throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); + } + + if (newCount == 0) + { + throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); + } + + int countChange = newCount - oldCount; + if (countChange != 0) + { + // countChange > 0 : countChange items were added + // countChange < 0 : -countChange items were removed + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (dataIndex >= oldStartIndex + oldCount) + { + UpdateElementIndex(element, virtInfo, dataIndex + countChange); + } + } + } + + EnsureFirstLastRealizedIndices(); + _lastRealizedElementIndexHeldByLayout += countChange; + } + break; + } + + case NotifyCollectionChangedAction.Remove: + { + var oldStartIndex = args.OldStartingIndex; + var oldCount = args.OldItems.Count; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) + { + // If we are doing the mapping, remove the element who's data was removed. + _owner.ClearElementImpl(element); + } + else if (dataIndex >= (oldStartIndex + oldCount)) + { + UpdateElementIndex(element, virtInfo, dataIndex - oldCount); + } + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + + case NotifyCollectionChangedAction.Reset: + if (_owner.ItemsSourceView.HasKeyIndexMapping) + { + _isDataSourceStableResetPending = true; + } + + // Walk through all the elements and make sure they are cleared, they will go into + // the stable id reset pool. + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + { + _owner.ClearElementImpl(element); + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + } + + private void EnsureFirstLastRealizedIndices() + { + if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault) + { + // This will ensure that the indexes are updated. + GetElementIfAlreadyHeldByLayout(0); + } + } + + public void OnLayoutChanging() + { + if (_owner.ItemsSourceView?.HasKeyIndexMapping == true) + { + _isDataSourceStableResetPending = true; + } + } + + public void OnOwnerArranged() + { + if (_isDataSourceStableResetPending) + { + _isDataSourceStableResetPending = false; + + foreach (var entry in _resetPool) + { + // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource. + // Focused element is getting cleared. Need to figure out semantics on where + // focus should go when the focused element is removed from the data collection. + ClearElement(entry.Value, true /* isClearedDueToCollectionChange */); + } + + _resetPool.Clear(); + } + } + + // We optimize for the case where index is not realized to return null as quickly as we can. + // Flow layouts manage containers on their own and will never ask for an index that is already realized. + // If an index that is realized is requested by the layout, we unfortunately have to walk the + // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing + // and non-virtualizing hosts. + private IControl GetElementIfAlreadyHeldByLayout(int index) + { + IControl element = null; + + bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; + bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); + + if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) + { + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + // Only give back elements held by layout. If someone else is holding it, they will be served by other methods. + int childIndex = virtInfo.Index; + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex); + if (virtInfo.Index == index) + { + element = child; + // If we have valid first/last indices, we don't have to walk the rest, but if we + // do not, then we keep walking through the entire children collection to get accurate + // indices once. + if (!cachedFirstLastIndicesInvalid) + { + break; + } + } + } + } + } + + return element; + } + + private IControl GetElementFromUniqueIdResetPool(int index) + { + IControl element = null; + // See if you can get it from the reset pool. + if (_isDataSourceStableResetPending) + { + element = _resetPool.Remove(index); + if (element != null) + { + // Make sure that the index is updated to the current one + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); + UpdateElementIndex(element, virtInfo, index); + } + } + + return element; + } + + private IControl GetElementFromPinnedElements(int index) + { + IControl element = null; + + // See if you can find something among the pinned elements. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (virtInfo.Index == index) + { + _pinnedPool.RemoveAt(i); + element = elementInfo.PinnedElement; + elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); + break; + } + } + + return element; + } + + private IControl GetElementFromElementFactory(int index) + { + // The view generator is the provider of last resort. + + var itemTemplateFactory = _owner.ItemTemplateShim; + if (itemTemplateFactory == null) + { + // If no ItemTemplate was provided, use a default + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + itemTemplateFactory = _owner.ItemTemplateShim; + } + + var data = _owner.ItemsSourceView.GetAt(index); + var element = itemTemplateFactory.GetElement(_owner, data); + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (virtInfo == null) + { + virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); + } + + // Prepare the element + element.DataContext = data; + + virtInfo.MoveOwnershipToLayoutFromElementFactory( + index, + /* uniqueId: */ + _owner.ItemsSourceView.HasKeyIndexMapping ? + _owner.ItemsSourceView.KeyFromIndex(index) : + string.Empty); + + // The view generator is the only provider that prepares the element. + var repeater = _owner; + + // Add the element to the children collection here before raising OnElementPrepared so + // that handlers can walk up the tree in case they want to find their IndexPath in the + // nested case. + var children = repeater.Children; + if (element.VisualParent != repeater) + { + children.Add(element); + } + + repeater.OnElementPrepared(element, index); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); + + return element; + } + + private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private void UpdateFocusedElement() + { + IControl focusedElement = null; + + var child = FocusManager.Instance.Current; + + if (child != null) + { + var parent = child.VisualParent; + var owner = _owner; + + // Find out if the focused element belongs to one of our direct + // children. + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var element = child as IControl; + if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) + { + focusedElement = element; + } + + break; + } + + child = parent as IInputElement; + parent = child.VisualParent; + } + } + + // If the focused element has changed, + // we need to unpin the old one and pin the new one. + if (_lastFocusedElement != focusedElement) + { + if (_lastFocusedElement != null) + { + UpdatePin(_lastFocusedElement, false /* addPin */); + } + + if (focusedElement != null) + { + UpdatePin(focusedElement, true /* addPin */); + } + + _lastFocusedElement = focusedElement; + } + } + + private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement(); + + private void EnsureEventSubscriptions() + { + if (!_eventsSubscribed) + { + _owner.GotFocus += OnFocusChanged; + _owner.LostFocus += OnFocusChanged; + } + } + + private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index) + { + var oldIndex = virtInfo.Index; + if (oldIndex != index) + { + virtInfo.UpdateIndex(index); + _owner.OnElementIndexChanged(element, oldIndex, index); + } + } + + private void InvalidateRealizedIndicesHeldByLayout() + { + _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + } + + private struct PinnedElementInfo + { + public PinnedElementInfo(IControl element) + { + PinnedElement = element; + VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); + } + + public IControl PinnedElement { get; } + public VirtualizationInfo VirtualizationInfo { get; } + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs new file mode 100644 index 0000000000..10c11889d0 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -0,0 +1,501 @@ +// 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.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal class ViewportManager + { + private const double CacheBufferPerSideInflationPixelDelta = 40.0; + private readonly ItemsRepeater _owner; + private bool _ensuredScroller; + private IScrollAnchorProvider _scroller; + private IControl _makeAnchorElement; + private bool _isAnchorOutsideRealizedRange; + private Task _cacheBuildAction; + private Rect _visibleWindow; + private Rect _layoutExtent; + // This is the expected shift by the layout. + private Point _expectedViewportShift; + // This is what is pending and not been accounted for. + // Sometimes the scrolling surface cannot service a shift (for example + // it is already at the top and cannot shift anymore.) + private Point _pendingViewportShift; + // Unshiftable shift amount that this view manager can + // handle on its own to fake it to the layout as if the shift + // actually happened. This can happen in cases where no scrollviewer + // in the parent chain can scroll in the shift direction. + private Point _unshiftableShift; + private double _maximumHorizontalCacheLength = 0.0; + private double _maximumVerticalCacheLength = 0.0; + private double _horizontalCacheBufferPerSide; + private double _verticalCacheBufferPerSide; + private bool _isBringIntoViewInProgress; + // For non-virtualizing layouts, we do not need to keep + // updating viewports and invalidating measure often. So when + // a non virtualizing layout is used, we stop doing all that work. + bool _managingViewportDisabled; + private IDisposable _effectiveViewportChangedRevoker; + private bool _layoutUpdatedSubscribed; + + public ViewportManager(ItemsRepeater owner) + { + _owner = owner; + } + + public IControl SuggestedAnchor + { + get + { + // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick. + var suggestedAnchor = _makeAnchorElement; + var owner = _owner; + + if (suggestedAnchor == null) + { + var anchorElement = _scroller?.CurrentAnchor; + + if (anchorElement != null) + { + // We can't simply return anchorElement because, in case of nested Repeaters, it may not + // be a direct child of ours, or even an indirect child. We need to walk up the tree starting + // from anchorElement to figure out what child of ours (if any) to use as the suggested element. + var child = anchorElement; + var parent = child.VisualParent as IControl; + + while (parent != null) + { + if (parent == owner) + { + suggestedAnchor = child; + break; + } + + child = parent; + parent = parent.VisualParent as IControl; + } + } + } + + return suggestedAnchor; + } + } + + public bool HasScroller => _scroller != null; + + public IControl MadeAnchor => _makeAnchorElement; + + public double HorizontalCacheLength + { + get => _maximumHorizontalCacheLength; + set + { + if (_maximumHorizontalCacheLength != value) + { + ValidateCacheLength(value); + _maximumHorizontalCacheLength = value; + } + } + } + + public double VerticalCacheLength + { + get => _maximumVerticalCacheLength; + set + { + if (_maximumVerticalCacheLength != value) + { + ValidateCacheLength(value); + _maximumVerticalCacheLength = value; + } + } + } + + private Rect GetLayoutVisibleWindowDiscardAnchor() + { + var visibleWindow = _visibleWindow; + + if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutVisibleWindow() + { + var visibleWindow = _visibleWindow; + + if (_makeAnchorElement != null) + { + // The anchor is not necessarily laid out yet. Its position should default + // to zero and the layout origin is expected to change once layout is done. + // Until then, we need a window that's going to protect the anchor from + // getting recycled. + visibleWindow = visibleWindow.WithX(0).WithY(0); + } + else if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutRealizationWindow() + { + var realizationWindow = GetLayoutVisibleWindow(); + if (HasScroller) + { + realizationWindow = new Rect( + realizationWindow.X - _horizontalCacheBufferPerSide, + realizationWindow.Y - _verticalCacheBufferPerSide, + realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0, + realizationWindow.Height + _verticalCacheBufferPerSide * 2.0); + } + + return realizationWindow; + } + + public void SetLayoutExtent(Rect extent) + { + _expectedViewportShift = new Point( + _expectedViewportShift.X + _layoutExtent.X - extent.X, + _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); + + // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. + if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) + { + // There are cases where we might be expecting a shift but not get it. We will + // be waiting for the effective viewport event but if the scroll viewer is not able + // to perform the shift (perhaps because it cannot scroll in negative offset), + // then we will end up not realizing elements in the visible + // window. To avoid this, we register to layout updated for this layout pass. If we + // get an effective viewport, we know we have a new viewport and we unregister from + // layout updated. If we get the layout updated handler, then we know that the + // scroller was unable to perform the shift and we invalidate measure and unregister + // from the layout updated event. + if (!_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated += OnLayoutUpdated; + _layoutUpdatedSubscribed = true; + } + } + + _layoutExtent = extent; + _pendingViewportShift = _expectedViewportShift; + + // We just finished a measure pass and have a new extent. + // Let's make sure the scrollers will run its arrange so that they track the anchor. + ((IControl)_scroller)?.InvalidateArrange(); + } + + public Point GetOrigin() => _layoutExtent.TopLeft; + + public void OnLayoutChanged(bool isVirtualizing) + { + _managingViewportDisabled = !isVirtualizing; + + _layoutExtent = default; + _expectedViewportShift = default; + _pendingViewportShift = default; + _unshiftableShift = default; + + _effectiveViewportChangedRevoker?.Dispose(); + + if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + } + + public void OnElementPrepared(IControl element) + { + // If we have an anchor element, we do not want the + // scroll anchor provider to start anchoring some other element. + ////element.CanBeScrollAnchor(true); + } + + public void OnElementCleared(ILayoutable element) + { + ////element.CanBeScrollAnchor(false); + } + + public void OnOwnerMeasuring() + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + EnsureScroller(); + } + + public void OnOwnerArranged() + { + _expectedViewportShift = default; + + if (!_managingViewportDisabled) + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + // EnsureScroller(); + + if (HasScroller) + { + double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0; + double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0; + + bool continueBuildingCache = + _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || + _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; + + if (continueBuildingCache) + { + _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + + _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); + _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); + } + } + } + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + if (_managingViewportDisabled) + { + return; + } + + // We were expecting a viewport shift but we never got one and we are not going to in this + // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and + // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer + // that can scroll in the direction where the shift is expected. + if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) + { + // Assume this is never going to come. + _unshiftableShift = new Point( + _unshiftableShift.X + _pendingViewportShift.X, + _unshiftableShift.Y + _pendingViewportShift.Y); + _pendingViewportShift = default; + _expectedViewportShift = default; + + TryInvalidateMeasure(); + } + } + + public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) + { + _makeAnchorElement = anchor; + _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + } + + public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) + { + if (!_managingViewportDisabled) + { + // During the time between a bring into view request and the element coming into view we do not + // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the + // element that is being brought into view. We can do this by making just that element as a potential + // anchor candidate and ensure no other element of this repeater is an anchor candidate. + // Once the layout pass is done and we render the frame, the element will be in frame and we can + // switch back to letting the anchor provider pick a suitable anchor. + + // get the targetChild - i.e the immediate child of this repeater that is being brought into view. + // Note that the element being brought into view could be a descendant. + var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + + // Make sure that only the target child can be the anchor during the bring into view operation. + foreach (var child in _owner.Children) + { + ////if (child.CanBeScrollAnchor && child != targetChild) + ////{ + //// child.CanBeScrollAnchor = false; + ////} + } + + // Register to rendering event to go back to how things were before where any child can be the anchor. + _isBringIntoViewInProgress = true; + ////if (!m_renderingToken) + ////{ + //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; + //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); + ////} + } + } + + private IControl GetImmediateChildOfRepeater(IControl descendant) + { + var targetChild = descendant; + var parent = descendant.Parent; + while (parent != null && parent != _owner) + { + targetChild = parent; + parent = (IControl)parent.VisualParent; + } + + if (parent == null) + { + throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + } + + return targetChild; + } + + public void ResetScrollers() + { + _scroller = null; + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = null; + _ensuredScroller = false; + } + + private void OnEffectiveViewportChanged(TransformedBounds? bounds) + { + if (!bounds.HasValue) + { + return; + } + + var globalClip = bounds.Value.Clip; + var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; + var clip = globalClip.TransformToAABB(transform); + var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + + UpdateViewport(effectiveViewport); + + _pendingViewportShift = default; + _unshiftableShift = default; + if (_visibleWindow.IsEmpty) + { + // We got cleared. + _layoutExtent = default; + } + + // We got a new viewport, we dont need to wait for layout updated anymore to + // see if our request for a pending shift was handled. + if (_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + } + } + + private void EnsureScroller() + { + if (!_ensuredScroller) + { + ResetScrollers(); + + var parent = _owner.GetVisualParent(); + while (parent != null) + { + if (parent is IScrollAnchorProvider scroller) + { + _scroller = scroller; + break; + } + + parent = parent.VisualParent; + } + + if (_scroller == null) + { + // We usually update the viewport in the post arrange handler. But, since we don't have + // a scroller, let's do it now. + UpdateViewport(Rect.Empty); + } + else if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + + _ensuredScroller = true; + } + } + + private void UpdateViewport(Rect viewport) + { + var currentVisibleWindow = viewport; + + if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && + -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) + { + // We got cleared. + _visibleWindow = default; + } + else + { + _visibleWindow = currentVisibleWindow; + } + + TryInvalidateMeasure(); + } + + private static void ValidateCacheLength(double cacheLength) + { + if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) + { + throw new ArgumentException("The maximum cache length must be equal or superior to zero."); + } + } + + private void TryInvalidateMeasure() + { + // Don't invalidate measure if we have an invalid window. + if (!_visibleWindow.IsEmpty) + { + // We invalidate measure instead of just invalidating arrange because + // we don't invalidate measure in UpdateViewport if the view is changing to + // avoid layout cycles. + _owner.InvalidateMeasure(); + } + } + + private IDisposable SubscribeToEffectiveViewportChanged(IControl control) + { + // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - + // we can get this from TransformedBounds, but this property is updated after layout has + // run, resulting in the UI being updated too late when scrolling quickly. We can + // partially remedey this by triggering also on Bounds changes, but this won't work so + // well for nested ItemsRepeaters. + // + // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially + // for this case): we need to implement that in Avalonia. + return control.GetObservable(Visual.TransformedBoundsProperty) + .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + + private class ScrollerInfo + { + public ScrollerInfo(ScrollViewer scroller) + { + Scroller = scroller; + } + + public ScrollViewer Scroller { get; } + } + }; +} diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs new file mode 100644 index 0000000000..eb30c1b7cf --- /dev/null +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -0,0 +1,118 @@ +// 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; + +namespace Avalonia.Controls +{ + internal enum ElementOwner + { + // All elements are originally owned by the view generator. + ElementFactory, + // Ownership is transferred to the layout when it calls GetElement. + Layout, + // Ownership is transferred to the pinned pool if the element is cleared (outside of + // a 'remove' collection change of course). + PinnedPool, + // Ownership is transfered to the reset pool if the element is cleared by a reset and + // the data source supports unique ids. + UniqueIdResetPool, + // Ownership is transfered to the animator if the element is cleared due to a + // 'remove'-like collection change. + Animator + } + + internal class VirtualizationInfo + { + private int _pinCounter; + private object _data; + + public Rect ArrangeBounds { get; set; } + public bool AutoRecycleCandidate { get; set; } + public int Index { get; private set; } + public bool IsPinned => _pinCounter > 0; + public bool IsHeldByLayout => Owner == ElementOwner.Layout; + public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; + public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; + public bool KeepAlive { get; set; } + public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; + public string UniqueId { get; private set; } + + public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) + { + Owner = ElementOwner.Layout; + Index = index; + UniqueId = uniqueId; + } + + public void MoveOwnershipToLayoutFromUniqueIdResetPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToLayoutFromPinnedPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToElementFactory() + { + Owner = ElementOwner.ElementFactory; + _pinCounter = 0; + Index = -1; + UniqueId = string.Empty; + ArrangeBounds = ItemsRepeater.InvalidRect; + } + + public void MoveOwnershipToUniqueIdResetPoolFromLayout() + { + Owner = ElementOwner.UniqueIdResetPool; + // Keep the pinCounter the same. If the container survives the reset + // it can go on being pinned as if nothing happened. + } + + public void MoveOwnershipToAnimator() + { + // During a unique id reset, some elements might get removed. + // Their ownership will go from the UniqueIdResetPool to the Animator. + // The common path though is for ownership to go from Layout to Animator. + Owner = ElementOwner.Animator; + Index = -1; + _pinCounter = 0; + } + + public void MoveOwnershipToPinnedPool() + { + Owner = ElementOwner.PinnedPool; + } + + public int AddPin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't pin an unrealized element."); + } + + return ++_pinCounter; + } + + public int RemovePin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't unpin an unrealized element."); + } + + if (!IsPinned) + { + throw new InvalidOperationException("UnpinElement was called more often than PinElement."); + } + + return --_pinCounter; + } + + public void UpdateIndex(int newIndex) => Index = newIndex; + } +} diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index ad7312d515..1f2f738670 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public RowDefinition() + public RowDefinition() { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The height of the row. /// The height unit of the column. - public RowDefinition(double value, GridUnitType type) + public RowDefinition(double value, GridUnitType type) { Height = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The height of the column. - public RowDefinition(GridLength height) + public RowDefinition(GridLength height) { Height = height; } @@ -62,8 +62,15 @@ namespace Avalonia.Controls /// public double MaxHeight { - get { return GetValue(MaxHeightProperty); } - set { SetValue(MaxHeightProperty, value); } + get + { + return GetValue(MaxHeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MaxHeightProperty, value); + } } /// @@ -71,8 +78,15 @@ namespace Avalonia.Controls /// public double MinHeight { - get { return GetValue(MinHeightProperty); } - set { SetValue(MinHeightProperty, value); } + get + { + return GetValue(MinHeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(MinHeightProperty, value); + } } /// @@ -80,12 +94,19 @@ namespace Avalonia.Controls /// public GridLength Height { - get { return GetValue(HeightProperty); } - set { SetValue(HeightProperty, value); } + get + { + return GetValue(HeightProperty); + } + set + { + Parent?.InvalidateMeasure(); + SetValue(HeightProperty, value); + } } internal override GridLength UserSizeValueCache => this.Height; internal override double UserMinSizeValueCache => this.MinHeight; internal override double UserMaxSizeValueCache => this.MaxHeight; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index 3090844251..cf72cc8ba3 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -14,10 +14,8 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public RowDefinitions() + public RowDefinitions() : base() { - ResetBehavior = ResetBehavior.Remove; - CollectionChanged += OnCollectionChanged; } /// diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index 0f0adec790..8a0a0fa728 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,7 +12,7 @@ namespace Avalonia.Controls private readonly IScreenImpl _iScreenImpl; public int ScreenCount => _iScreenImpl.ScreenCount; - public IReadOnlyList All => _iScreenImpl?.AllScreens; + public IReadOnlyList All => _iScreenImpl?.AllScreens ?? Array.Empty(); public Screen Primary => All.FirstOrDefault(x => x.Primary); public Screens(IScreenImpl iScreenImpl) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 264b1fd2ce..c9b5cbb75b 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl, IScrollable + public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -333,6 +333,9 @@ namespace Avalonia.Controls get { return _viewport.Height; } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -373,6 +376,16 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); diff --git a/src/Avalonia.Controls/Shapes/Ellipse.cs b/src/Avalonia.Controls/Shapes/Ellipse.cs index 8cb73bf5c6..f80bf5d967 100644 --- a/src/Avalonia.Controls/Shapes/Ellipse.cs +++ b/src/Avalonia.Controls/Shapes/Ellipse.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness); + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); return new EllipseGeometry(rect); } diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index 375b0b719a..b803bde588 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness); + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); return new RectangleGeometry(rect); } diff --git a/src/Avalonia.Controls/ShutdownMode.cs b/src/Avalonia.Controls/ShutdownMode.cs index 46e27ff4e1..ee593c28a8 100644 --- a/src/Avalonia.Controls/ShutdownMode.cs +++ b/src/Avalonia.Controls/ShutdownMode.cs @@ -1,10 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.ApplicationLifetimes; + namespace Avalonia.Controls { /// - /// Describes the possible values for . + /// Describes the possible values for . /// public enum ShutdownMode { diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 260c708515..9eaa246434 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -47,8 +48,6 @@ namespace Avalonia.Controls Thumb.DragStartedEvent.AddClassHandler(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); - SmallChangeProperty.OverrideDefaultValue(1); - LargeChangeProperty.OverrideDefaultValue(10); } /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 59c3c33942..e7e0eb3f3d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -17,13 +17,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackLayout.SpacingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackLayout.OrientationProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/StartupEventArgs.cs b/src/Avalonia.Controls/StartupEventArgs.cs deleted file mode 100644 index 0e12f5c01a..0000000000 --- a/src/Avalonia.Controls/StartupEventArgs.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Controls -{ - /// - /// Contains the arguments for the event. - /// - public class StartupEventArgs : EventArgs - { - private string[] _args; - - /// - /// Gets the command line arguments that were passed to the application. - /// - public IReadOnlyList Args => _args ?? (_args = GetArgs()); - - private static string[] GetArgs() - { - try - { - var args = Environment.GetCommandLineArgs(); - - return args.Length > 1 ? args.Skip(1).ToArray() : new string[0]; - } - catch (NotSupportedException) - { - return new string[0]; - } - } - } -} diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 4c3c92309e..19458cc6b8 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -16,9 +16,15 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) + public FuncControlTemplate(Func build) : base(build) { } + + public new ControlTemplateResult Build(ITemplatedControl param) + { + var (control, scope) = BuildWithNameScope(param); + return new ControlTemplateResult(control, scope); + } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs index 8e49b51cb8..eec7a6030f 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs @@ -17,9 +17,9 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) - : base(x => build((T)x)) + public FuncControlTemplate(Func build) + : base((x, s) => build((T)x, s)) { } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 1d90fcd01e..204540e431 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Templates /// public static readonly FuncDataTemplate Default = new FuncDataTemplate( - data => + (data, s) => { if (data != null) { @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Type type, - Func build, + Func build, bool supportsRecycling = false) : this(o => IsInstance(o, type), build, supportsRecycling) { @@ -67,7 +67,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(build) { @@ -105,4 +105,4 @@ namespace Avalonia.Controls.Templates return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 9339aa6924..68b737928d 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Templates /// A function which when passed an object of returns a control. /// /// Whether the control can be recycled. - public FuncDataTemplate(Func build, bool supportsRecycling = false) + public FuncDataTemplate(Func build, bool supportsRecycling = false) : base(typeof(T), CastBuild(build), supportsRecycling) { } @@ -35,12 +35,30 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(CastMatch(match), CastBuild(build), supportsRecycling) { } + /// + /// Initializes a new instance of the class. + /// + /// + /// A function which determines whether the data template matches the specified data. + /// + /// + /// A function which when passed an object of returns a control. + /// + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) + : this(match, (a, _) => build(a), supportsRecycling) + { + } + /// /// Casts a strongly typed match function to a weakly typed one. /// @@ -57,9 +75,9 @@ namespace Avalonia.Controls.Templates /// The strong data type. /// The strongly typed function. /// The weakly typed function. - private static Func CastBuild(Func f) + private static Func CastBuild(Func f) { - return o => f((T)o); + return (o, s) => f((T)o, s); } } } diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs deleted file mode 100644 index 5ab186261e..0000000000 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object using a . - /// - public class FuncMemberSelector : IMemberSelector - { - private readonly Func _selector; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The selector. - public FuncMemberSelector(Func selector) - { - this._selector = selector; - } - - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - public object Select(object o) - { - return (o is TObject) ? _selector((TObject)o) : default(TMember); - } - } -} diff --git a/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs new file mode 100644 index 0000000000..76bc298e30 --- /dev/null +++ b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Controls.Templates +{ + public static class FuncTemplateNameScopeExtensions + { + public static T RegisterInNameScope(this T control, INameScope scope) + where T : StyledElement + { + scope.Register(control.Name, control); + return control; + } + } +} diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs index b1ce63fdf1..e0608d0471 100644 --- a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls.Templates public class FuncTemplate : ITemplate where TControl : IControl { - private readonly Func _func; + private readonly Func _func; /// /// Initializes a new instance of the class. /// /// The function used to create the control. - public FuncTemplate(Func func) + public FuncTemplate(Func func) { Contract.Requires(func != null); @@ -35,7 +35,14 @@ namespace Avalonia.Controls.Templates /// public TControl Build(TParam param) { - return _func(param); + return BuildWithNameScope(param).control; + } + + protected (TControl control, INameScope nameScope) BuildWithNameScope(TParam param) + { + var scope = new NameScope(); + var rv = _func(param, scope); + return (rv, scope); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index e7c9cf8608..998ef0e9f2 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Type type, - Func build, + Func build, Func itemsSelector) : this(o => IsInstance(o, type), build, itemsSelector) { @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base(match, build) { diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index 4ca96f60bd..41f870ab42 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Templates /// items. /// public FuncTreeDataTemplate( - Func build, + Func build, Func itemsSelector) : base( typeof(T), @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base( CastMatch(match), @@ -65,6 +65,17 @@ namespace Avalonia.Controls.Templates return o => (o is T) && f((T)o); } + /// + /// Casts a function with a typed parameter to an untyped function. + /// + /// The result. + /// The typed function. + /// The untyped function. + private static Func Cast(Func f) + { + return (o, s) => f((T)o, s); + } + /// /// Casts a function with a typed parameter to an untyped function. /// diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 408329c450..a3ef0fa954 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -9,7 +9,25 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate { } -} \ No newline at end of file + + public class ControlTemplateResult + { + public IControl Control { get; } + public INameScope NameScope { get; } + + public ControlTemplateResult(IControl control, INameScope nameScope) + { + Control = control; + NameScope = nameScope; + } + + public void Deconstruct(out IControl control, out INameScope scope) + { + control = Control; + scope = NameScope; + } + } +} diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs deleted file mode 100644 index e1ec42a849..0000000000 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object. - /// - public interface IMemberSelector - { - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - object Select(object o); - } -} diff --git a/src/Avalonia.Controls/Templates/ITemplate`2.cs b/src/Avalonia.Controls/Templates/ITemplate`2.cs index f61292c64b..a0c0e88eca 100644 --- a/src/Avalonia.Controls/Templates/ITemplate`2.cs +++ b/src/Avalonia.Controls/Templates/ITemplate`2.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.Templates /// /// The type of the parameter. /// The type of control. - public interface ITemplate where TControl : IControl + public interface ITemplate { /// /// Creates the control. @@ -19,4 +19,4 @@ namespace Avalonia.Controls.Templates /// TControl Build(TParam param); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 6b0c48b97b..b9603b91ed 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,12 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive; using System.Reactive.Linq; using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Metadata; namespace Avalonia.Controls @@ -106,6 +103,14 @@ namespace Avalonia.Controls FontWeightProperty, FontSizeProperty, FontStyleProperty); + + Observable.Merge( + TextProperty.Changed, + TextAlignmentProperty.Changed, + FontSizeProperty.Changed, + FontStyleProperty.Changed, + FontWeightProperty.Changed + ).AddClassHandler((x,_) => x.OnTextPropertiesChanged()); } /// @@ -114,18 +119,6 @@ namespace Avalonia.Controls public TextBlock() { _text = string.Empty; - - Observable.Merge( - this.GetObservable(TextProperty).Select(_ => Unit.Default), - this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), - this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), - this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), - this.GetObservable(FontWeightProperty).Select(_ => Unit.Default)) - .Subscribe(_ => - { - InvalidateFormattedText(); - InvalidateMeasure(); - }); } /// @@ -408,5 +401,11 @@ namespace Avalonia.Controls InvalidateFormattedText(); InvalidateMeasure(); } + + private void OnTextPropertiesChanged() + { + InvalidateFormattedText(); + InvalidateMeasure(); + } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index d43957313e..7b4bb469d0 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -38,6 +38,15 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + public static readonly StyledProperty SelectionBrushProperty = + AvaloniaProperty.Register(nameof(SelectionBrushProperty)); + + public static readonly StyledProperty SelectionForegroundBrushProperty = + AvaloniaProperty.Register(nameof(SelectionForegroundBrushProperty)); + + public static readonly StyledProperty CaretBrushProperty = + AvaloniaProperty.Register(nameof(CaretBrushProperty)); + public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionStart), @@ -169,6 +178,24 @@ namespace Avalonia.Controls set => SetValue(PasswordCharProperty, value); } + public IBrush SelectionBrush + { + get => GetValue(SelectionBrushProperty); + set => SetValue(SelectionBrushProperty, value); + } + + public IBrush SelectionForegroundBrush + { + get => GetValue(SelectionForegroundBrushProperty); + set => SetValue(SelectionForegroundBrushProperty, value); + } + + public IBrush CaretBrush + { + get => GetValue(CaretBrushProperty); + set => SetValue(CaretBrushProperty, value); + } + public int SelectionStart { get @@ -287,16 +314,11 @@ namespace Avalonia.Controls { DecideCaretVisibility(); } - - e.Handled = true; } private void DecideCaretVisibility() { - if (!IsReadOnly) - _presenter?.ShowCaret(); - else - _presenter?.HideCaret(); + _presenter.ShowCaret(); } protected override void OnLostFocus(RoutedEventArgs e) @@ -456,7 +478,7 @@ namespace Avalonia.Controls movement = true; selection = false; handled = true; - + } else if (Match(keymap.MoveCursorToTheEndOfLine)) { @@ -485,7 +507,7 @@ namespace Avalonia.Controls movement = true; selection = true; handled = true; - + } else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection)) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 21bd0e4e57..87100ceeb0 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -51,7 +51,6 @@ namespace Avalonia.Controls private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; - private readonly IApplicationLifecycle _applicationLifecycle; private readonly IPlatformRenderInterface _renderInterface; private Size _clientSize; private ILayoutManager _layoutManager; @@ -96,7 +95,6 @@ namespace Avalonia.Controls _accessKeyHandler = TryGetService(dependencyResolver); _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); - _applicationLifecycle = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -125,11 +123,6 @@ namespace Avalonia.Controls x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); - if (_applicationLifecycle != null) - { - _applicationLifecycle.Exit += OnApplicationExiting; - } - if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) { WeakSubscriptionManager.Subscribe( @@ -281,7 +274,6 @@ namespace Avalonia.Controls Closed?.Invoke(this, EventArgs.Empty); Renderer?.Dispose(); Renderer = null; - _applicationLifecycle.Exit -= OnApplicationExiting; } /// @@ -348,18 +340,6 @@ namespace Avalonia.Controls return result; } - private void OnApplicationExiting(object sender, EventArgs args) - { - HandleApplicationExiting(); - } - - /// - /// Handles the application exiting, either from the last window closing, or a call to . - /// - protected virtual void HandleApplicationExiting() - { - } - /// /// Handles input from . /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c3fbce1d83..4514109e12 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,32 +105,21 @@ namespace Avalonia.Controls get => _selectedItem; set { - SetAndRaise(SelectedItemProperty, ref _selectedItem, - (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - backing = val; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - val)); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); - if (val != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != val) - { - _syncingSelectedItems = true; - SelectSingleItem(val); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - }, value); + if (value != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } } } @@ -164,6 +153,48 @@ namespace Avalonia.Controls } } + /// + /// Expands the specified all descendent s. + /// + /// The item to expand. + public void ExpandSubTree(TreeViewItem item) + { + item.IsExpanded = true; + + var panel = item.Presenter.Panel; + + if (panel != null) + { + foreach (var child in panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + ExpandSubTree(treeViewItem); + } + } + } + } + + /// + /// Selects all items in the . + /// + /// + /// Note that this method only selects nodes currently visible due to their parent nodes + /// being expanded: it does not expand nodes. + /// + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } + + /// + /// Deselects all items in the . + /// + public void UnselectAll() + { + SelectedItems.Clear(); + } + /// /// Subscribes to the CollectionChanged event, if any. /// @@ -409,7 +440,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SelectAll(); e.Handled = true; } } @@ -479,7 +510,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } @@ -490,11 +522,13 @@ namespace Avalonia.Controls /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var item = ItemContainerGenerator.Index.ItemFromContainer(container); @@ -515,13 +549,20 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!toggle && !range) + if (rightButton) + { + if (!SelectedItems.Contains(item)) + { + SelectSingleItem(item); + } + } + else if (!toggle && !range) { SelectSingleItem(item); } else if (multi && range) { - SelectingItemsControl.SynchronizeItems( + SynchronizeItems( SelectedItems, GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem)); } @@ -684,6 +725,7 @@ namespace Avalonia.Controls /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -692,13 +734,14 @@ namespace Avalonia.Controls IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } @@ -778,5 +821,27 @@ namespace Avalonia.Controls container.Classes.Set(":selected", selected); } } + + /// + /// Makes a list of objects equal another (though doesn't preserve order). + /// + /// The items collection. + /// The desired items. + private static void SynchronizeItems(IList items, IEnumerable desired) + { + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); + + foreach (var i in toRemove) + { + items.Remove(i); + } + + foreach (var i in toAdd) + { + items.Add(i); + } + } } } diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index e42ca5e0e6..4f15839821 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -9,40 +9,8 @@ namespace Avalonia.Controls /// /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic. /// - public class UserControl : ContentControl, IStyleable, INameScope + public class UserControl : ContentControl, IStyleable { - private readonly NameScope _nameScope = new NameScope(); - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 01614ba87b..d2793fe0dd 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using System.ComponentModel; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -47,7 +48,7 @@ namespace Avalonia.Controls /// /// A top-level window. /// - public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope + public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { /// /// Defines the property. @@ -97,6 +98,20 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); + /// + /// Routed event that can be used for global tracking of window destruction + /// + public static readonly RoutedEvent WindowClosedEvent = + RoutedEvent.Register("WindowClosed", RoutingStrategies.Direct); + + /// + /// Routed event that can be used for global tracking of opening windows + /// + public static readonly RoutedEvent WindowOpenedEvent = + RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); + + + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -140,25 +155,8 @@ namespace Avalonia.Controls impl.Closing = HandleClosing; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); - Screens = new Screens(PlatformImpl?.Screen); - } - - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } } - public Screens Screens { get; private set; } - /// /// Gets the platform-specific window implementation. /// @@ -252,26 +250,6 @@ namespace Avalonia.Controls /// public event EventHandler Closing; - private static void AddWindow(Window window) - { - if (Application.Current == null) - { - return; - } - - Application.Current.Windows.Add(window); - } - - private static void RemoveWindow(Window window) - { - if (Application.Current == null) - { - return; - } - - Application.Current.Windows.Remove(window); - } - /// /// Closes the window. /// @@ -280,12 +258,6 @@ namespace Avalonia.Controls Close(false); } - protected override void HandleApplicationExiting() - { - base.HandleApplicationExiting(); - Close(true); - } - /// /// Closes a dialog window with the specified result. /// @@ -385,7 +357,7 @@ namespace Avalonia.Controls return; } - AddWindow(this); + this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); EnsureInitialized(); IsVisible = true; @@ -447,7 +419,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - AddWindow(this); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); EnsureInitialized(); IsVisible = true; @@ -508,24 +480,6 @@ namespace Avalonia.Controls } } - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } - /// protected override Size MeasureOverride(Size availableSize) { @@ -560,7 +514,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { - RemoveWindow(this); + RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); base.HandleClosed(); } @@ -588,16 +542,3 @@ namespace Avalonia.Controls protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); } } - -namespace Avalonia -{ - public static class WindowApplicationExtensions - { - public static void RunWithMainWindow(this Application app) where TWindow : Avalonia.Controls.Window, new() - { - var window = new TWindow(); - window.Show(); - app.Run(window); - } - } -} diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 363af05a0b..40c9fc94d2 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -63,6 +63,7 @@ namespace Avalonia.Controls public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(impl, dependencyResolver) { + Screens = new Screens(PlatformImpl?.Screen); impl.Activated = HandleActivated; impl.Deactivated = HandleDeactivated; impl.PositionChanged = HandlePositionChanged; @@ -108,6 +109,8 @@ namespace Avalonia.Controls impl.Position = value; } } + + public Screens Screens { get; private set; } /// /// Whether an auto-size operation is in progress. diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs deleted file mode 100644 index 328bb9f147..0000000000 --- a/src/Avalonia.Controls/WindowCollection.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections; -using System.Collections.Generic; - -using Avalonia.Controls; - -namespace Avalonia -{ - public class WindowCollection : IReadOnlyList - { - private readonly Application _application; - private readonly List _windows = new List(); - - public WindowCollection(Application application) - { - _application = application; - } - - /// - /// - /// Gets the number of elements in the collection. - /// - public int Count => _windows.Count; - - /// - /// - /// Gets the at the specified index. - /// - /// - /// The . - /// - /// The index. - /// - public Window this[int index] => _windows[index]; - - /// - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() - { - return _windows.GetEnumerator(); - } - - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Adds the specified window. - /// - /// The window. - internal void Add(Window window) - { - if (window == null) - { - return; - } - - _windows.Add(window); - } - - /// - /// Removes the specified window. - /// - /// The window. - internal void Remove(Window window) - { - if (window == null) - { - return; - } - - _windows.Remove(window); - - OnRemoveWindow(window); - } - - /// - /// Closes all windows and removes them from the underlying collection. - /// - internal void Clear() - { - while (_windows.Count > 0) - { - _windows[0].Close(true); - } - } - - private void OnRemoveWindow(Window window) - { - if (window == null) - { - return; - } - - if (_application.IsShuttingDown) - { - return; - } - - switch (_application.ShutdownMode) - { - case ShutdownMode.OnLastWindowClose: - if (Count == 0) - { - _application.Shutdown(); - } - - break; - case ShutdownMode.OnMainWindowClose: - if (window == _application.MainWindow) - { - _application.Shutdown(); - } - - break; - } - } - } -} diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 4df1b39400..3acf341c35 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -1,11 +1,10 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; using static System.Math; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 3b6d071583..a7a94130ea 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(instance) .Bind().ToConstant(threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 801ed74ca0..617fbc8005 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Reflection; +using System.Threading; using System.Xml; using Avalonia.Controls; using Avalonia.Input; @@ -9,7 +10,6 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; -using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -123,15 +123,6 @@ namespace Avalonia.DesignerSupport.Remote } private const string BuilderMethodName = "BuildAvaloniaApp"; - - class NeverClose : ICloseable - { - public event EventHandler Closed - { - add {} - remove {} - } - } public static void Main(string[] cmdline) { @@ -156,7 +147,7 @@ namespace Avalonia.DesignerSupport.Remote transport.OnException += (t, e) => Die(e.ToString()); Log("Sending StartDesignerSessionMessage"); transport.Send(new StartDesignerSessionMessage {SessionId = args.SessionId}); - app.Run(new NeverClose()); + Dispatcher.UIThread.MainLoop(CancellationToken.None); } @@ -206,7 +197,6 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { - var xamlException = e as XamlException; var xmlException = e as XmlException; s_transport.Send(new UpdateXamlResultMessage @@ -216,8 +206,8 @@ namespace Avalonia.DesignerSupport.Remote { ExceptionType = e.GetType().FullName, Message = e.Message.ToString(), - LineNumber = xamlException?.LineNumber ?? xmlException?.LineNumber, - LinePosition = xamlException?.LinePosition ?? xmlException?.LinePosition, + LineNumber = xmlException?.LineNumber, + LinePosition = xmlException?.LinePosition, } }); } diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 0f55d42e33..1df0f3a097 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,23 +1,24 @@ - - - - - - + - - - + + + + + + + + + Hold Ctrl+Shift over a control to inspect. - + Focused: - - + + Pointer Over: - + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index e0bacf326b..cc3c545d84 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,22 +17,22 @@ using Avalonia.VisualTree; namespace Avalonia { - public static class DevToolsExtensions - { - public static void AttachDevTools(this TopLevel control) - { - Avalonia.Diagnostics.DevTools.Attach(control); - } - } + public static class DevToolsExtensions + { + public static void AttachDevTools(this TopLevel control) + { + Diagnostics.DevTools.Attach(control); + } + } } namespace Avalonia.Diagnostics { - public class DevTools : UserControl + public class DevTools : UserControl { - private static Dictionary s_open = new Dictionary(); - private static HashSet s_visualTreeRoots = new HashSet(); - private IDisposable _keySubscription; + private static readonly Dictionary s_open = new Dictionary(); + private static readonly HashSet s_visualTreeRoots = new HashSet(); + private readonly IDisposable _keySubscription; public DevTools(IControl root) { @@ -46,7 +48,6 @@ namespace Avalonia.Diagnostics // HACK: needed for XAMLIL, will fix that later public DevTools() { - } public IControl Root { get; } @@ -64,9 +65,8 @@ namespace Avalonia.Diagnostics if (e.Key == Key.F12) { var control = (TopLevel)sender; - var devToolsWindow = default(Window); - if (s_open.TryGetValue(control, out devToolsWindow)) + if (s_open.TryGetValue(control, out var devToolsWindow)) { devToolsWindow.Activate(); } @@ -79,10 +79,8 @@ namespace Avalonia.Diagnostics Width = 1024, Height = 512, Content = devTools, - DataTemplates = - { - new ViewLocator(), - } + DataTemplates = { new ViewLocator() }, + Title = "Avalonia DevTools" }; devToolsWindow.Closed += devTools.DevToolsClosed; @@ -114,7 +112,6 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs index aab50a13dd..464187a048 100644 --- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs @@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models { Contract.Requires(handler != null); - this.Handler = handler; - this.Handled = handled; - this.Route = route; + Handler = handler; + Handled = handled; + Route = route; } public object Handler { get; } @@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models { return named.Name + " (" + Handler.GetType().Name + ")"; } + return Handler.GetType().Name; } } diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index b107338aec..a66703301d 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -7,7 +7,7 @@ using Avalonia.Controls.Templates; namespace Avalonia.Diagnostics { - public class ViewLocator : IDataTemplate + internal class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; @@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics return data is TViewModel; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index d723890196..4b832f7ce6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable Classes - { - get; - private set; - } - - public IEnumerable Properties - { - get; - private set; - } + public IEnumerable Properties { get; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index c6d3f02e8b..9f524a21eb 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Collections.ObjectModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; @@ -10,21 +11,23 @@ namespace Avalonia.Diagnostics.ViewModels { internal class DevToolsViewModel : ViewModelBase { - private ViewModelBase _content; - private int _selectedTab; - private TreePageViewModel _logicalTree; - private TreePageViewModel _visualTree; - private EventsViewModel _eventsView; + private IDevToolViewModel _selectedTool; private string _focusedControl; private string _pointerOverElement; public DevToolsViewModel(IControl root) { - _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); - _eventsView = new EventsViewModel(root); + Tools = new ObservableCollection + { + new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), + new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), + new EventsViewModel(root) + }; + + SelectedTool = Tools.First(); UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) @@ -33,58 +36,33 @@ namespace Avalonia.Diagnostics.ViewModels } }; - SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } - public ViewModelBase Content + public IDevToolViewModel SelectedTool { - get { return _content; } - private set { RaiseAndSetIfChanged(ref _content, value); } + get => _selectedTool; + set => RaiseAndSetIfChanged(ref _selectedTool, value); } - public int SelectedTab - { - get { return _selectedTab; } - set - { - _selectedTab = value; - - switch (value) - { - case 0: - Content = _logicalTree; - break; - case 1: - Content = _visualTree; - break; - case 2: - Content = _eventsView; - break; - } - - RaisePropertyChanged(); - } - } + public ObservableCollection Tools { get; } public string FocusedControl { - get { return _focusedControl; } - private set { RaiseAndSetIfChanged(ref _focusedControl, value); } + get => _focusedControl; + private set => RaiseAndSetIfChanged(ref _focusedControl, value); } public string PointerOverElement { - get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + get => _pointerOverElement; + private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); } public void SelectControl(IControl control) { - var tree = Content as TreePageViewModel; - - if (tree != null) + if (SelectedTool is TreePageViewModel tree) { tree.SelectControl(control); } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs index 0674918400..7e38749a6f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventOwnerTreeNode : EventTreeNodeBase { - private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] + private static readonly RoutedEvent[] s_defaultEvents = { - Button.ClickEvent, - InputElement.KeyDownEvent, - InputElement.KeyUpEvent, - InputElement.TextInputEvent, - InputElement.PointerReleasedEvent, - InputElement.PointerPressedEvent, + Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent }; public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) : base(null, type.Name) { - this.Children = new AvaloniaList(events.OrderBy(e => e.Name) + Children = new AvaloniaList(events.OrderBy(e => e.Name) .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); - this.IsExpanded = true; + IsExpanded = true; } public override bool? IsEnabled @@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (base.IsEnabled != value) { base.IsEnabled = value; + if (_updateChildren && value != null) { foreach (var child in Children) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs index 59c0f82414..36f1904253 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; using Avalonia.Threading; @@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventTreeNode : EventTreeNodeBase { - private RoutedEvent _event; - private EventsViewModel _parentViewModel; + private readonly RoutedEvent _event; + private readonly EventsViewModel _parentViewModel; private bool _isRegistered; private FiredEvent _currentEvent; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(@event != null); Contract.Requires(vm != null); - this._event = @event; - this._parentViewModel = vm; + _event = @event; + _parentViewModel = vm; } public override bool? IsEnabled @@ -56,6 +55,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (IsEnabled.GetValueOrDefault() && !_isRegistered) { + // FIXME: This leaks event handlers. _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true); _isRegistered = true; } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs index 146a8cea8e..4be4d8f74e 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels private bool _isExpanded; private bool? _isEnabled = false; - public EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase parent, string text) { - this.Parent = parent; - this.Text = text; + Parent = parent; + Text = text; } public IAvaloniaReadOnlyList Children @@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public virtual bool? IsEnabled { - get { return _isEnabled; } - set { RaiseAndSetIfChanged(ref _isEnabled, value); } + get => _isEnabled; + set => RaiseAndSetIfChanged(ref _isEnabled, value); } public EventTreeNodeBase Parent @@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Text { get; - private set; } internal void UpdateChecked() @@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels { if (Children == null) return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) { if (i == 0) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs index a23677afc8..1c868148ce 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -5,8 +5,6 @@ using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; -using System.Windows.Input; - using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Interactivity; @@ -14,21 +12,24 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { - internal class EventsViewModel : ViewModelBase + internal class EventsViewModel : ViewModelBase, IDevToolViewModel { private readonly IControl _root; private FiredEvent _selectedEvent; public EventsViewModel(IControl root) { - this._root = root; - this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + _root = root; + + Nodes = RoutedEventRegistry.Instance.GetAllRegistered() .GroupBy(e => e.OwnerType) .OrderBy(e => e.Key.Name) .Select(g => new EventOwnerTreeNode(g.Key, g, this)) .ToArray(); } + public string Name => "Events"; + public EventTreeNodeBase[] Nodes { get; } public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); @@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + return (bool)value ? Brushes.Green : Brushes.Transparent; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs index 049280c390..daf8ebd0f6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.ObjectModel; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; @@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class FiredEvent : ViewModelBase { - private RoutedEventArgs _eventArgs; + private readonly RoutedEventArgs _eventArgs; private EventChainLink _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) @@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(eventArgs != null); Contract.Requires(originator != null); - this._eventArgs = eventArgs; - this.Originator = originator; + _eventArgs = eventArgs; + Originator = originator; AddToChain(originator); } @@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; } } @@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink HandledBy { - get { return _handledBy; } + get => _handledBy; set { if (_handledBy != value) diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs new file mode 100644 index 0000000000..0434230a63 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Diagnostics.ViewModels +{ + /// + /// View model interface for tool showing up in DevTools + /// + public interface IDevToolViewModel + { + /// + /// Name of a tool. + /// + string Name { get; } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index 638cf6c88f..0b9bd85b4f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index 2609b74ce0..523be406c8 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels Value = diagnostic.Value ?? "(null)"; Priority = (diagnostic.Priority != BindingPriority.Unset) ? diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? "Inherited" : "Unset"; + diagnostic.Property.Inherits ? + "Inherited" : + "Unset"; Diagnostic = diagnostic.Diagnostic; }); } @@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels public string Priority { - get { return _priority; } - private set { RaiseAndSetIfChanged(ref _priority, value); } + get => _priority; + private set => RaiseAndSetIfChanged(ref _priority, value); } public string Diagnostic { - get { return _diagnostic; } - private set { RaiseAndSetIfChanged(ref _diagnostic, value); } + get => _diagnostic; + private set => RaiseAndSetIfChanged(ref _diagnostic, value); } public object Value { - get { return _value; } - private set { RaiseAndSetIfChanged(ref _value, value); } + get => _value; + private set => RaiseAndSetIfChanged(ref _value, value); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..902eb81bd9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs @@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => styleable.Classes.CollectionChanged += x, + x => styleable.Classes.CollectionChanged -= x) + .TakeUntil(styleable.StyleDetach); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) @@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { - get { return _classes; } - private set { RaiseAndSetIfChanged(ref _classes, value); } + get => _classes; + private set => RaiseAndSetIfChanged(ref _classes, value); } public IVisual Visual @@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public TreeNode Parent @@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Type { get; - private set; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index dba44c5d0c..b2b1aaa723 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,21 +6,24 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase + internal class TreePageViewModel : ViewModelBase, IDevToolViewModel { private TreeNode _selected; private ControlDetailsViewModel _details; - public TreePageViewModel(TreeNode[] nodes) + public TreePageViewModel(TreeNode[] nodes, string name) { Nodes = nodes; + Name = name; } + public string Name { get; } + public TreeNode[] Nodes { get; protected set; } public TreeNode SelectedNode { - get { return _selected; } + get => _selected; set { if (RaiseAndSetIfChanged(ref _selected, value)) @@ -32,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels public ControlDetailsViewModel Details { - get { return _details; } - private set { RaiseAndSetIfChanged(ref _details, value); } + get => _details; + private set => RaiseAndSetIfChanged(ref _details, value); } public TreeNode FindNode(IControl control) @@ -63,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { control = control.GetVisualParent(); } - } + } if (node != null) { @@ -87,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels { return node; } - else + + foreach (var child in node.Children) { - foreach (var child in node.Children) - { - var result = FindNode(child, control); + var result = FindNode(child, control); - if (result != null) - { - return result; - } + if (result != null) + { + return result; } } diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs index 00660754c0..a6ff4dd853 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace Avalonia.Diagnostics.ViewModels { - public class ViewModelBase : INotifyPropertyChanged + internal class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs index 8c070261d9..47ef91507a 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs @@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 381b2e04b4..fb867ab55e 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -7,7 +7,6 @@ using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Media; -using Avalonia.Styling; namespace Avalonia.Diagnostics.Views { @@ -15,6 +14,7 @@ namespace Avalonia.Diagnostics.Views { private static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + private SimpleGrid _grid; public ControlDetailsView() @@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views public ControlDetailsViewModel ViewModel { - get { return GetValue(ViewModelProperty); } + get => GetValue(ViewModelProperty); private set { SetValue(ViewModelProperty, value); @@ -38,48 +38,37 @@ namespace Avalonia.Diagnostics.Views { Func> pt = PropertyTemplate; - Content = new ScrollViewer - { - Content = _grid = new SimpleGrid - { - Styles = - { - new Style(x => x.Is()) - { - Setters = new[] - { - new Setter(MarginProperty, new Thickness(2)), - } - }, - }, - [GridRepeater.TemplateProperty] = pt, - } - }; + Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; } private IEnumerable PropertyTemplate(object i) { var property = (PropertyDetails)i; + var margin = new Thickness(2); + yield return new TextBlock { + Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), + [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() }; yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) - .ToBinding(), + .ToBinding() }; yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), + [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() }; } } diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml index 8d4d37f7b3..406dd433a2 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -2,53 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.EventsView"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -