diff --git a/Avalonia.sln b/Avalonia.sln index 3c2e8fc83d..21c317549c 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.24720.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -15,6 +15,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Designer", "src\Windows\Avalonia.Designer\Avalonia.Designer.csproj", "{EC42600F-049B-43FF-AED1-8314D61B2749}" + ProjectSection(ProjectDependencies) = postProject + {2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {2B888490-D14A-4BCA-AB4B-48676FA93C9B} + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}" EndProject @@ -158,6 +161,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 @@ -1909,6 +1914,38 @@ Global {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.ActiveCfg = Release|Any CPU {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1960,5 +1997,6 @@ Global {52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {48840EDD-24BF-495D-911E-2EB12AE75D3B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection EndGlobal diff --git a/nuget/template/Avalonia.Skia.Desktop.nuspec b/nuget/template/Avalonia.Skia.Desktop.nuspec index aa52ac4fa1..ab962c5521 100644 --- a/nuget/template/Avalonia.Skia.Desktop.nuspec +++ b/nuget/template/Avalonia.Skia.Desktop.nuspec @@ -13,7 +13,7 @@ Copyright 2015 Avalonia - + diff --git a/readme.md b/readme.md index 1c77a38c08..63047fdc85 100644 --- a/readme.md +++ b/readme.md @@ -45,7 +45,7 @@ framework. As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](docs/tutorial/gettingstarted.md) 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/Avalonia/Avalonia). +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](docs/spec/architecture.md) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs index fd457f7ba9..7bcaf837a8 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingTest/App.xaml.cs @@ -19,8 +19,7 @@ namespace BindingTest InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index ae8c9f5d8f..03842f78c1 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -17,8 +17,7 @@ namespace ControlCatalog // TODO: Make this work with GTK/Skia/Cairo depending on command-line args // again. AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index ca28fadea0..98171f29d6 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -10,6 +10,7 @@ + diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 5e79916a8d..9e331c8634 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -35,8 +35,7 @@ namespace TestApplication var app = new App(); AppBuilder.Configure(app) - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .SetupWithoutStarting(); app.Run(); diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationTest/App.config new file mode 100644 index 0000000000..88fa4027bd --- /dev/null +++ b/samples/VirtualizationTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationTest/App.xaml new file mode 100644 index 0000000000..d9630eef58 --- /dev/null +++ b/samples/VirtualizationTest/App.xaml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationTest/App.xaml.cs new file mode 100644 index 0000000000..14ab5b3f84 --- /dev/null +++ b/samples/VirtualizationTest/App.xaml.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. + +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace VirtualizationTest +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml new file mode 100644 index 0000000000..55bd729fec --- /dev/null +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationTest/MainWindow.xaml.cs new file mode 100644 index 0000000000..952383dffb --- /dev/null +++ b/samples/VirtualizationTest/MainWindow.xaml.cs @@ -0,0 +1,25 @@ +// 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; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using VirtualizationTest.ViewModels; + +namespace VirtualizationTest +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + DataContext = new MainWindowViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationTest/Program.cs new file mode 100644 index 0000000000..8958605a3b --- /dev/null +++ b/samples/VirtualizationTest/Program.cs @@ -0,0 +1,34 @@ +// 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 Avalonia; +using Avalonia.Controls; +using Avalonia.Logging.Serilog; +using Serilog; + +namespace VirtualizationTest +{ + class Program + { + static void Main(string[] args) + { + InitializeLogging(); + + AppBuilder.Configure() + .UseWin32() + .UseDirect2D1() + .Start(); + } + + private static void InitializeLogging() + { +#if DEBUG + SerilogLogger.Initialize(new LoggerConfiguration() + .MinimumLevel.Warning() + .WriteTo.Trace(outputTemplate: "{Area}: {Message}") + .CreateLogger()); +#endif + } + } +} diff --git a/samples/VirtualizationTest/Properties/AssemblyInfo.cs b/samples/VirtualizationTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e6af64555b --- /dev/null +++ b/samples/VirtualizationTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("VirtualizationTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VirtualizationTest")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fbcaf3d0-2808-4934-8e96-3f607594517b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs new file mode 100644 index 0000000000..75777012c1 --- /dev/null +++ b/samples/VirtualizationTest/ViewModels/ItemViewModel.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 ReactiveUI; + +namespace VirtualizationTest.ViewModels +{ + internal class ItemViewModel : ReactiveObject + { + private string _prefix; + private int _index; + + public ItemViewModel(int index, string prefix = "Item") + { + _prefix = prefix; + _index = index; + } + + public string Header => $"{_prefix} {_index}"; + } +} diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..86869eb46a --- /dev/null +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,141 @@ +// 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 Avalonia.Collections; +using Avalonia.Controls; +using ReactiveUI; + +namespace VirtualizationTest.ViewModels +{ + internal class MainWindowViewModel : ReactiveObject + { + private int _itemCount = 200; + private string _newItemString = "New Item"; + private int _newItemIndex; + private IReactiveList _items; + private string _prefix = "Item"; + private Orientation _orientation; + private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; + + public MainWindowViewModel() + { + this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); + RecreateCommand = ReactiveCommand.Create(); + RecreateCommand.Subscribe(_ => Recreate()); + + AddItemCommand = ReactiveCommand.Create(); + AddItemCommand.Subscribe(_ => AddItem()); + + RemoveItemCommand = ReactiveCommand.Create(); + RemoveItemCommand.Subscribe(_ => Remove()); + + SelectFirstCommand = ReactiveCommand.Create(); + SelectFirstCommand.Subscribe(_ => SelectItem(0)); + + SelectLastCommand = ReactiveCommand.Create(); + SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1)); + } + + public string NewItemString + { + get { return _newItemString; } + set { this.RaiseAndSetIfChanged(ref _newItemString, value); } + } + + public int ItemCount + { + get { return _itemCount; } + set { this.RaiseAndSetIfChanged(ref _itemCount, value); } + } + + public AvaloniaList SelectedItems { get; } + = new AvaloniaList(); + + public IReactiveList Items + { + get { return _items; } + private set { this.RaiseAndSetIfChanged(ref _items, value); } + } + + public Orientation Orientation + { + get { return _orientation; } + set { this.RaiseAndSetIfChanged(ref _orientation, value); } + } + + public IEnumerable Orientations => + Enum.GetValues(typeof(Orientation)).Cast(); + + public ItemVirtualizationMode VirtualizationMode + { + get { return _virtualizationMode; } + set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); } + } + + public IEnumerable VirtualizationModes => + Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); + + public ReactiveCommand AddItemCommand { get; private set; } + public ReactiveCommand RecreateCommand { get; private set; } + public ReactiveCommand RemoveItemCommand { get; private set; } + public ReactiveCommand SelectFirstCommand { get; private set; } + public ReactiveCommand SelectLastCommand { get; private set; } + + private void ResizeItems(int count) + { + if (Items == null) + { + var items = Enumerable.Range(0, count) + .Select(x => new ItemViewModel(x)); + Items = new ReactiveList(items); + } + else if (count > Items.Count) + { + var items = Enumerable.Range(Items.Count, count - Items.Count) + .Select(x => new ItemViewModel(x)); + Items.AddRange(items); + } + else if (count < Items.Count) + { + Items.RemoveRange(count, Items.Count - count); + } + } + + private void AddItem() + { + var index = Items.Count; + + if (SelectedItems.Count > 0) + { + index = Items.IndexOf(SelectedItems[0]); + } + + Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); + } + + private void Remove() + { + if (SelectedItems.Count > 0) + { + Items.RemoveAll(SelectedItems); + } + } + + private void Recreate() + { + _prefix = _prefix == "Item" ? "Recreated" : "Item"; + var items = Enumerable.Range(0, _itemCount) + .Select(x => new ItemViewModel(x, _prefix)); + Items = new ReactiveList(items); + } + + private void SelectItem(int index) + { + SelectedItems.Clear(); + SelectedItems.Add(Items[index]); + } + } +} diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj new file mode 100644 index 0000000000..ed7a340234 --- /dev/null +++ b/samples/VirtualizationTest/VirtualizationTest.csproj @@ -0,0 +1,180 @@ + + + + + Debug + AnyCPU + {FBCAF3D0-2808-4934-8E96-3F607594517B} + WinExe + Properties + VirtualizationTest + VirtualizationTest + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll + True + + + ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll + True + + + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll + True + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + + + + + + + + + App.xaml + + + MainWindow.xaml + + + + + + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Avalonia.Animation + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Avalonia.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Avalonia.Controls + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Avalonia.DesignerSupport + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Avalonia.Diagnostics + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Avalonia.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Avalonia.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Avalonia.Layout + + + {B61B66A3-B82D-4875-8001-89D3394FE0C9} + Avalonia.Logging.Serilog + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Avalonia.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Avalonia.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Avalonia.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Avalonia.Themes.Default + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Avalonia.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Avalonia.Markup + + + {3e908f67-5543-4879-a1dc-08eace79b3cd} + Avalonia.Direct2D1 + + + {811a76cf-1cf6-440f-963b-bbe31bd72a82} + Avalonia.Win32 + + + + + Designer + + + + + Designer + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject new file mode 100644 index 0000000000..f744eecae0 --- /dev/null +++ b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject @@ -0,0 +1,26 @@ + + true + 1000 + false + false + false + true + false + false + true + false + false + true + true + false + true + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/samples/VirtualizationTest/packages.config b/samples/VirtualizationTest/packages.config new file mode 100644 index 0000000000..782e7fbf2c --- /dev/null +++ b/samples/VirtualizationTest/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index 50b3c364d4..6485796ce7 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -21,8 +21,7 @@ namespace XamlTestApplication InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 9ec70a4eb1..9d4c1d9b29 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -2,11 +2,13 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Media; +using Avalonia.VisualTree; namespace XamlTestApplication { - public class TestScrollable : Control, IScrollable + public class TestScrollable : Control, ILogicalScrollable { private int itemCount = 100; private Size _extent; @@ -14,6 +16,7 @@ namespace XamlTestApplication private Size _viewport; private Size _lineSize; + public bool IsLogicalScrollEnabled => true; public Action InvalidateScroll { get; set; } Size IScrollable.Extent @@ -53,6 +56,36 @@ namespace XamlTestApplication } } + public override void Render(DrawingContext context) + { + var y = 0.0; + + for (var i = (int)_offset.Y; i < itemCount; ++i) + { + using (var line = new FormattedText( + "Item " + (i + 1), + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); + y += _lineSize.Height; + } + } + } + + public bool BringIntoView(IControl target, Rect targetRect) + { + throw new NotImplementedException(); + } + + public IControl GetControlInDirection(NavigationDirection direction, IControl from) + { + throw new NotImplementedException(); + } + protected override Size MeasureOverride(Size availableSize) { using (var line = new FormattedText( @@ -76,25 +109,5 @@ namespace XamlTestApplication InvalidateScroll?.Invoke(); return finalSize; } - - public override void Render(DrawingContext context) - { - var y = 0.0; - - for (var i = (int)_offset.Y; i < itemCount; ++i) - { - using (var line = new FormattedText( - "Item " + (i + 1), - TextBlock.GetFontFamily(this), - TextBlock.GetFontSize(this), - TextBlock.GetFontStyle(this), - TextAlignment.Left, - TextBlock.GetFontWeight(this))) - { - context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); - y += _lineSize.Height; - } - } - } } } \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index 2c0012d65f..8a19cef15d 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs @@ -1,6 +1,8 @@ // 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.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Diagnostics; diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.xaml b/samples/XamlTestApplicationPcl/Views/MainWindow.xaml index b93072d2d5..a844986169 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.xaml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.xaml @@ -24,6 +24,7 @@ + diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 012e19da50..3192629494 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -38,7 +38,6 @@ namespace Avalonia.Android .Bind().ToConstant(this); SkiaPlatform.Initialize(); - Application.RegisterPlatformCallback(() => { }); _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; diff --git a/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject b/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject index 30815b1937..f744eecae0 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject +++ b/src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - false + true false false true @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index dd047db279..b0db89f7ea 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -26,7 +26,7 @@ namespace Avalonia /// /// The parent object that inherited values are inherited from. /// - private AvaloniaObject _inheritanceParent; + private IAvaloniaObject _inheritanceParent; /// /// The set values/bindings on this object. @@ -120,7 +120,7 @@ namespace Avalonia /// /// The inheritance parent. /// - protected AvaloniaObject InheritanceParent + protected IAvaloniaObject InheritanceParent { get { diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index e893fcb75b..65b559482d 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -319,6 +319,57 @@ namespace Avalonia.Collections } } + /// + /// Moves an item to a new index. + /// + /// The index of the item to move. + /// The index to move the item to. + public void Move(int oldIndex, int newIndex) + { + var item = this[oldIndex]; + _inner.RemoveAt(oldIndex); + _inner.Insert(newIndex, item); + + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, + item, + newIndex, + oldIndex); + _collectionChanged(this, e); + } + } + + /// + /// Moves multiple items to a new index. + /// + /// The first index of the items to move. + /// The number of items to move. + /// The index to move the items to. + public void MoveRange(int oldIndex, int count, int newIndex) + { + var items = _inner.GetRange(oldIndex, count); + _inner.RemoveRange(oldIndex, count); + + if (newIndex > oldIndex) + { + newIndex -= count; + } + + _inner.InsertRange(newIndex, items); + + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, + items, + newIndex, + oldIndex); + _collectionChanged(this, e); + } + } + /// /// Removes an item from the collection. /// diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 86ba8c3786..8b3e0e731a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reflection; namespace Avalonia.Controls { @@ -98,23 +99,64 @@ namespace Avalonia.Controls /// /// The method to call to initialize the windowing subsystem. /// An instance. - public AppBuilder WithWindowingSubsystem(Action initializer) + public AppBuilder UseWindowingSubsystem(Action initializer) { WindowingSubsystem = initializer; return this; } + /// + /// Specifies a windowing subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll)); + /// /// Specifies a rendering subsystem to use. /// /// The method to call to initialize the rendering subsystem. /// An instance. - public AppBuilder WithRenderingSubsystem(Action initializer) + public AppBuilder UseRenderingSubsystem(Action initializer) { RenderingSubsystem = initializer; return this; } + /// + /// Specifies a rendering subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll)); + + static Action GetInitializer(string assemblyName) => () => + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; + var platformClassFullName = assemblyName + "." + platformClassName; + var platformClass = assembly.GetType(platformClassFullName); + var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); + init.Invoke(null, null); + }; + + public AppBuilder UsePlatformDetect() + { + var platformId = (int) + ((dynamic) Type.GetType("System.Environment").GetRuntimeProperty("OSVersion").GetValue(null)).Platform; + if (platformId == 4 || platformId == 6) + { + UseRenderingSubsystem("Avalonia.Cairo"); + UseWindowingSubsystem("Avalonia.Gtk"); + } + else + { + UseRenderingSubsystem("Avalonia.Direct2D1"); + UseWindowingSubsystem("Avalonia.Win32"); + } + return this; + } + /// /// Sets up the platform-speciic services for the . /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index f73ead5576..1991f49ac0 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -26,16 +26,12 @@ namespace Avalonia /// - A global set of . /// - A . /// - An . - /// - Loads and initializes rendering and windowing subsystems with - /// and . /// - Registers services needed by the rest of Avalonia in the /// method. /// - Tracks the lifetime of the application. /// public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle { - static Action _platformInitializationCallback; - /// /// The application-global data templates. /// @@ -121,11 +117,6 @@ namespace Avalonia /// IStyleHost IStyleHost.StylingParent => null; - public static void RegisterPlatformCallback(Action cb) - { - _platformInitializationCallback = cb; - } - /// /// Initializes the application by loading XAML etc. /// @@ -189,47 +180,5 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this); } - - /// - /// Initializes the rendering and windowing subsystems according to platform. - /// - /// The value of Environment.OSVersion.Platform. - protected void InitializeSubsystems(int platformID) - { - if (_platformInitializationCallback != null) - { - _platformInitializationCallback(); - } - else if (platformID == 4 || platformID == 6) - { - InitializeSubsystem("Avalonia.Cairo"); - InitializeSubsystem("Avalonia.Gtk"); - } - else - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } - } - - /// - /// Initializes the rendering or windowing subsystem defined by the specified assemblt. - /// - /// The name of the assembly. - protected static void InitializeSubsystem(string assemblyName) - { - var assembly = Assembly.Load(new AssemblyName(assemblyName)); - var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; - var platformClassFullName = assemblyName + "." + platformClassName; - var platformClass = assembly.GetType(platformClassFullName); - var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); - init.Invoke(null, null); - } - - internal static void InitializeWin32Subsystem() - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 6358a8c9d9..3f8985aa12 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -50,26 +50,31 @@ - + + - + + + + + - - - + + + - + @@ -170,6 +175,7 @@ + diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index ebccb7435e..74eebea4bc 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { // TODO: Implement this return null; diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index e246635906..944f6b82ac 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls /// - Implements to allow styling to work on the control. /// - Implements to form part of a logical tree. /// - public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize + public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize { /// /// Defines the property. @@ -435,7 +435,11 @@ namespace Avalonia.Controls OnDetachedFromLogicalTreeCore(e); } - InheritanceParent = parent as AvaloniaObject; + if (InheritanceParent == null || parent == null) + { + InheritanceParent = parent as AvaloniaObject; + } + _parent = (IControl)parent; if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true) @@ -455,6 +459,15 @@ namespace Avalonia.Controls } } + /// + /// Sets the control's inheritance parent. + /// + /// The parent. + void ISetInheritanceParent.SetParent(IAvaloniaObject parent) + { + InheritanceParent = parent; + } + /// /// Adds a pseudo-class to be set when a property is true. /// diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs index 231914740e..a0b0d5d0ca 100644 --- a/src/Avalonia.Controls/Design.cs +++ b/src/Avalonia.Controls/Design.cs @@ -1,4 +1,6 @@ +using System.Runtime.CompilerServices; + namespace Avalonia.Controls { public static class Design @@ -43,7 +45,25 @@ namespace Avalonia.Controls { return control.GetValue(DataContextProperty); } - + + static readonly ConditionalWeakTable Substitutes = new ConditionalWeakTable(); + + public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty + .RegisterAttached("PreviewWith", typeof (Design)); + + public static void SetPreviewWith(object target, Control control) + { + Substitutes.Remove(target); + Substitutes.Add(target, control); + } + + public static Control GetPreviewWith(object target) + { + Control rv; + Substitutes.TryGetValue(target, out rv); + return rv; + } + internal static void ApplyDesignerProperties(Control target, Control source) { if (source.IsSet(WidthProperty)) diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 8199333f47..ba584e33b9 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.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 System.Collections; using System.Collections.Generic; using Avalonia.Controls.Templates; @@ -16,7 +15,7 @@ namespace Avalonia.Controls.Generators /// /// Gets the currently realized containers. /// - IEnumerable Containers { get; } + IEnumerable Containers { get; } /// /// Gets or sets the data template used to display the items in the control. @@ -34,28 +33,33 @@ namespace Avalonia.Controls.Generators event EventHandler Dematerialized; /// - /// Creates container controls for a collection of items. + /// Event raised whenever containers are recycled. /// - /// - /// The index of the first item of the data in the containing collection. + event EventHandler Recycled; + + /// + /// Creates a container control for an item. + /// + /// + /// The index of the item of data in the control's items. /// - /// The items. + /// The item. /// An optional member selector. /// The created controls. - IEnumerable Materialize( - int startingIndex, - IEnumerable items, + ItemContainerInfo Materialize( + int index, + object item, IMemberSelector selector); /// /// Removes a set of created containers. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. - IEnumerable Dematerialize(int startingIndex, int count); + IEnumerable Dematerialize(int startingIndex, int count); /// /// Inserts space for newly inserted containers in the index. @@ -69,17 +73,23 @@ namespace Avalonia.Controls.Generators /// the gap. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. - IEnumerable RemoveRange(int startingIndex, int count); + IEnumerable RemoveRange(int startingIndex, int count); + + bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector); /// /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IEnumerable Clear(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs index 304e0a99fa..6821a842e3 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs @@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators /// /// Initializes a new instance of the class. /// - /// The index of the first container in the source items. /// The container. - public ItemContainerEventArgs( - int startingIndex, - ItemContainer container) + public ItemContainerEventArgs(ItemContainerInfo container) { - StartingIndex = startingIndex; + StartingIndex = container.Index; Containers = new[] { container }; } @@ -32,7 +29,7 @@ namespace Avalonia.Controls.Generators /// The containers. public ItemContainerEventArgs( int startingIndex, - IList containers) + IList containers) { StartingIndex = startingIndex; Containers = containers; @@ -41,7 +38,7 @@ namespace Avalonia.Controls.Generators /// /// Gets the containers. /// - public IList Containers { get; } + public IList Containers { get; } /// /// Gets the index of the first container in the source items. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 114d31c5a6..6bbf757106 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -2,11 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Reactive.Subjects; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private List _containers = new List(); + private Dictionary _containers = new Dictionary(); /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators } /// - public IEnumerable Containers => _containers.Where(x => x != null); + public IEnumerable Containers => _containers.Values; /// public event EventHandler Materialized; @@ -37,6 +37,9 @@ namespace Avalonia.Controls.Generators /// public event EventHandler Dematerialized; + /// + public event EventHandler Recycled; + /// /// Gets or sets the data template used to display the items in the control. /// @@ -48,41 +51,29 @@ namespace Avalonia.Controls.Generators public IControl Owner { get; } /// - public IEnumerable Materialize( - int startingIndex, - IEnumerable items, + public ItemContainerInfo Materialize( + int index, + object item, IMemberSelector selector) { - Contract.Requires(items != null); + var i = selector != null ? selector.Select(item) : item; + var container = new ItemContainerInfo(CreateContainer(i), item, index); - int index = startingIndex; - var result = new List(); + _containers.Add(container.Index, container); + Materialized?.Invoke(this, new ItemContainerEventArgs(container)); - foreach (var item in items) - { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainer(CreateContainer(i), item, index++); - result.Add(container); - } - - AddContainers(result); - Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); - - return result.Where(x => x != null).ToList(); + return container; } /// - public virtual IEnumerable Dematerialize(int startingIndex, int count) + public virtual IEnumerable Dematerialize(int startingIndex, int count) { - var result = new List(); + var result = new List(); for (int i = startingIndex; i < startingIndex + count; ++i) { - if (i < _containers.Count) - { - result.Add(_containers[i]); - _containers[i] = null; - } + result.Add(_containers[i]); + _containers.Remove(i); } Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); @@ -93,18 +84,47 @@ namespace Avalonia.Controls.Generators /// public virtual void InsertSpace(int index, int count) { - _containers.InsertRange(index, Enumerable.Repeat(null, count)); + if (count > 0) + { + var toMove = _containers.Where(x => x.Key >= index).ToList(); + + foreach (var i in toMove) + { + _containers.Remove(i.Key); + i.Value.Index += count; + _containers[i.Value.Index] = i.Value; + } + } } /// - public virtual IEnumerable RemoveRange(int startingIndex, int count) + public virtual IEnumerable RemoveRange(int startingIndex, int count) { - List result = new List(); + var result = new List(); - if (startingIndex < _containers.Count) + if (count > 0) { - result.AddRange(_containers.GetRange(startingIndex, count)); - _containers.RemoveRange(startingIndex, count); + for (var i = startingIndex; i < startingIndex + count; ++i) + { + ItemContainerInfo found; + + if (_containers.TryGetValue(i, out found)) + { + result.Add(found); + } + + _containers.Remove(i); + } + + var toMove = _containers.Where(x => x.Key >= startingIndex).ToList(); + + foreach (var i in toMove) + { + _containers.Remove(i.Key); + i.Value.Index -= count; + _containers.Add(i.Value.Index, i.Value); + } + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); } @@ -112,10 +132,20 @@ namespace Avalonia.Controls.Generators } /// - public virtual IEnumerable Clear() + public virtual bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + return false; + } + + /// + public virtual IEnumerable Clear() { - var result = _containers.Where(x => x != null).ToList(); - _containers = new List(); + var result = Containers.ToList(); + _containers.Clear(); if (result.Count > 0) { @@ -128,27 +158,20 @@ namespace Avalonia.Controls.Generators /// public IControl ContainerFromIndex(int index) { - if (index < _containers.Count) - { - return _containers[index]?.ContainerControl; - } - - return null; + ItemContainerInfo result; + _containers.TryGetValue(index, out result); + return result?.ContainerControl; } /// public int IndexFromContainer(IControl container) { - var index = 0; - foreach (var i in _containers) { - if (i?.ContainerControl == container) + if (i.Value.ContainerControl == container) { - return index; + return i.Key; } - - ++index; } return -1; @@ -161,44 +184,40 @@ namespace Avalonia.Controls.Generators /// The created container control. protected virtual IControl CreateContainer(object item) { - var result = Owner.MaterializeDataTemplate(item, ItemTemplate); + var result = item as IControl; - if (result != null && !(item is IControl)) + if (result == null) { - result.DataContext = item; + result = new ContentPresenter(); + result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); + + if (ItemTemplate != null) + { + result.SetValue( + ContentPresenter.ContentTemplateProperty, + ItemTemplate, + BindingPriority.TemplatedParent); + } } return result; } /// - /// Adds a collection of containers to the index. + /// Moves a container. /// - /// The containers. - protected void AddContainers(IList containers) + /// The old index. + /// The new index. + /// The new item. + /// The container info. + protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) { - Contract.Requires(containers != null); - - foreach (var c in containers) - { - while (_containers.Count < c.Index) - { - _containers.Add(null); - } - - if (_containers.Count == c.Index) - { - _containers.Add(c); - } - else if (_containers[c.Index] == null) - { - _containers[c.Index] = c; - } - else - { - throw new InvalidOperationException("Container already created."); - } - } + var container = _containers[oldIndex]; + container.Index = newIndex; + container.Item = item; + _containers.Remove(oldIndex); + _containers.Add(newIndex, container); + return container; } /// @@ -207,9 +226,18 @@ namespace Avalonia.Controls.Generators /// The first index. /// The number of elements in the range. /// The containers. - protected IEnumerable GetContainerRange(int index, int count) + protected IEnumerable GetContainerRange(int index, int count) + { + return _containers.Where(x => x.Key >= index && x.Key <= index + count).Select(x => x.Value); + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseRecycled(ItemContainerEventArgs e) { - return _containers.GetRange(index, count); + Recycled?.Invoke(this, e); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index c4bc730e15..3aa2181cd4 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -5,6 +5,7 @@ using System; using System.Linq.Expressions; using System.Reflection; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -62,10 +63,10 @@ namespace Avalonia.Controls.Generators if (ContentTemplateProperty != null) { - result.SetValue(ContentTemplateProperty, ItemTemplate); + result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); } - result.SetValue(ContentProperty, item); + result.SetValue(ContentProperty, item, BindingPriority.Style); if (!(item is IControl)) { @@ -75,5 +76,34 @@ namespace Avalonia.Controls.Generators return result; } } + + /// + public override bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + var container = ContainerFromIndex(oldIndex); + + if (container == null) + { + throw new IndexOutOfRangeException("Could not recycle container: not materialized."); + } + + var i = selector != null ? selector.Select(item) : item; + + container.SetValue(ContentProperty, i); + + if (!(item is IControl)) + { + container.DataContext = i; + } + + var info = MoveContainer(oldIndex, newIndex, i); + RaiseRecycled(new ItemContainerEventArgs(info)); + + return true; + } } } diff --git a/src/Avalonia.Controls/Generators/ItemContainer.cs b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs similarity index 85% rename from src/Avalonia.Controls/Generators/ItemContainer.cs rename to src/Avalonia.Controls/Generators/ItemContainerInfo.cs index ed2e433a28..b9387f1022 100644 --- a/src/Avalonia.Controls/Generators/ItemContainer.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs @@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators /// Holds information about an item container generated by an /// . /// - public class ItemContainer + public class ItemContainerInfo { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The container control. /// The item that the container represents. /// /// The index of the item in the collection. /// - public ItemContainer(IControl container, object item, int index) + public ItemContainerInfo(IControl container, object item, int index) { ContainerControl = container; Item = item; @@ -35,11 +35,11 @@ namespace Avalonia.Controls.Generators /// /// Gets the item that the container represents. /// - public object Item { get; } + public object Item { get; internal set; } /// /// Gets the index of the item in the collection. /// - public int Index { get; } + public int Index { get; internal set; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs new file mode 100644 index 0000000000..c9b3a55aaa --- /dev/null +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls.Generators +{ + public class MenuItemContainerGenerator : ItemContainerGenerator + { + /// + /// Initializes a new instance of the class. + /// + /// The owner control. + public MenuItemContainerGenerator(IControl owner) + : base(owner, MenuItem.HeaderProperty, null) + { + } + + /// + protected override IControl CreateContainer(object item) + { + var separator = item as Separator; + return separator != null ? separator : base.CreateContainer(item); + } + } +} diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index b7a9bb2dc1..e0c52beb11 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators Materialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// @@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators Dematerialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// /// Removes a set of containers from the index. /// /// The item containers. - public void Remove(IEnumerable containers) + public void Remove(IEnumerable containers) { foreach (var container in containers) { diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 681bea865a..8a70aa7307 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.Generators var template = GetTreeDataTemplate(item, ItemTemplate); var result = new T(); - result.SetValue(ContentProperty, template.Build(item)); + result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); var itemsSelector = template.ItemsSelector(item); @@ -99,25 +99,30 @@ namespace Avalonia.Controls.Generators } } - public override IEnumerable Clear() + public override IEnumerable Clear() { var items = base.Clear(); Index.Remove(items); return items; } - public override IEnumerable Dematerialize(int startingIndex, int count) + public override IEnumerable Dematerialize(int startingIndex, int count) { Index.Remove(GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } - public override IEnumerable RemoveRange(int startingIndex, int count) + public override IEnumerable RemoveRange(int startingIndex, int count) { Index.Remove(GetContainerRange(startingIndex, count)); return base.RemoveRange(startingIndex, count); } + public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) + { + return false; + } + /// /// Gets the data template for the specified item. /// diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs new file mode 100644 index 0000000000..9bbd0d8518 --- /dev/null +++ b/src/Avalonia.Controls/IScrollable.cs @@ -0,0 +1,29 @@ +// 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 Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Interface implemented by scrollable controls. + /// + public interface IScrollable + { + /// + /// Gets the extent of the scrollable content, in logical units + /// + Size Extent { get; } + + /// + /// Gets or sets the current scroll offset, in logical units. + /// + Vector Offset { get; set; } + + /// + /// Gets the size of the viewport, in logical units. + /// + Size Viewport { get; } + } +} diff --git a/src/Avalonia.Controls/ISetInheritanceParent.cs b/src/Avalonia.Controls/ISetInheritanceParent.cs new file mode 100644 index 0000000000..788ab77246 --- /dev/null +++ b/src/Avalonia.Controls/ISetInheritanceParent.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. + +namespace Avalonia.Controls +{ + /// + /// Defines an interface through which a 's inheritance parent can be set. + /// + /// + /// You should not usually need to use this interface - it is for advanced scenarios only. + /// Additionally, also sets the inheritance parent; this + /// interface is only needed where the logical and inheritance parents differ. + /// + public interface ISetInheritanceParent + { + /// + /// Sets the control's inheritance parent. + /// + /// The parent. + void SetParent(IAvaloniaObject parent); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/IVirtualizingController.cs b/src/Avalonia.Controls/IVirtualizingController.cs new file mode 100644 index 0000000000..0b997f4948 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingController.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. + +namespace Avalonia.Controls +{ + /// + /// Interface implemented by controls that act as controllers for an + /// . + /// + public interface IVirtualizingController + { + /// + /// Called when the 's controls should be updated. + /// + /// + /// The controller should respond to this method being called by either adding + /// children up until becomes true or + /// removing controls. + /// + void UpdateControls(); + } +} diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs new file mode 100644 index 0000000000..5d35fa1ec8 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -0,0 +1,67 @@ +// 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 +{ + /// + /// A panel that can be used to virtualize items. + /// + public interface IVirtualizingPanel : IPanel + { + /// + /// Gets or sets the controller for the virtualizing panel. + /// + /// + /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// panel. This property will be set by the controller when virtualization is initialized. + /// Note that this property may remain null if the panel is added to a control that does + /// not act as a virtualizing controller. + /// + IVirtualizingController Controller { get; set; } + + /// + /// Gets a value indicating whether the panel is full. + /// + /// + /// This property should return false until enough children are added to fill the space + /// passed into the last measure in the direction of scroll. It should be updated + /// immediately after a child is added or removed. + /// + bool IsFull { get; } + + /// + /// Gets the number of items that can be removed while keeping the panel full. + /// + /// + /// This property should return the number of children that are completely out of the + /// panel's current bounds in the direction of scroll. It should be updated after an + /// arrange. + /// + int OverflowCount { get; } + + /// + /// Gets the direction of scroll. + /// + Orientation ScrollDirection { get; } + + /// + /// Gets the average size of the materialized items in the direction of scroll. + /// + double AverageItemSize { get; } + + /// + /// Gets or sets a size in pixels by which the content is overflowing the panel, in the + /// direction of scroll. + /// + /// + /// This may be non-zero even when is zero if the last item + /// overflows the panel bounds. + /// + double PixelOverflow { get; } + + /// + /// Gets or sets the current pixel offset of the items in the direction of scroll. + /// + double PixelOffset { get; set; } + } +} diff --git a/src/Avalonia.Controls/ItemVirtualizationMode.cs b/src/Avalonia.Controls/ItemVirtualizationMode.cs new file mode 100644 index 0000000000..f17e8c07ad --- /dev/null +++ b/src/Avalonia.Controls/ItemVirtualizationMode.cs @@ -0,0 +1,21 @@ +// 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 +{ + /// + /// Describes the item virtualization method to use for a list. + /// + public enum ItemVirtualizationMode + { + /// + /// Do not virtualize items. + /// + None, + + /// + /// Virtualize items without smooth scrolling. + /// + Simple, + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3a77f61f1b..8e8603ca24 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -25,7 +25,6 @@ namespace Avalonia.Controls /// /// The default value for the property. /// - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")] private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); @@ -90,6 +89,7 @@ namespace Avalonia.Controls _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } } @@ -265,6 +265,28 @@ namespace Avalonia.Controls LogicalChildren.RemoveAll(toRemove); } + /// + /// Called when containers are recycled for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersRecycled(ItemContainerEventArgs e) + { + var toRemove = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be removed from the logical tree + // when it is removed from the Items collection. + if (container?.ContainerControl != container?.Item) + { + toRemove.Add(container.ContainerControl); + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ded75d708a..ed7a50d9b0 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls if (null != TransformRoot) { TransformRoot.RenderTransform = _matrixTransform; - TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + TransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); } ApplyLayoutTransform(); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 0f32bc91da..84e4751d37 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -2,12 +2,11 @@ // 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.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -16,6 +15,18 @@ namespace Avalonia.Controls /// public class ListBox : SelectingItemsControl { + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll); + /// /// Defines the property. /// @@ -28,6 +39,31 @@ namespace Avalonia.Controls public static readonly new AvaloniaProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty VirtualizationModeProperty = + ItemsPresenter.VirtualizationModeProperty.AddOwner(); + + private IScrollable _scroll; + + /// + /// Initializes static members of the class. + /// + static ListBox() + { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + } + + /// + /// Gets the scroll information for the . + /// + public IScrollable Scroll + { + get { return _scroll; } + private set { SetAndRaise(ScrollProperty, ref _scroll, value); } + } + /// public new IList SelectedItems => base.SelectedItems; @@ -38,6 +74,15 @@ namespace Avalonia.Controls set { base.SelectionMode = value; } } + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode + { + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -75,5 +120,11 @@ namespace Avalonia.Controls (e.InputModifiers & InputModifiers.Control) != 0); } } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + Scroll = e.NameScope.Find("PART_ScrollViewer"); + } } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index d5d70ab1ea..2185fd982b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -131,6 +132,12 @@ namespace Avalonia.Controls _subscription.Dispose(); } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + /// /// Called when a key is pressed within the menu. /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e4bb44049d..3d15ed99e7 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -204,6 +205,12 @@ namespace Avalonia.Controls IsSelected = true; } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } + /// /// Called when a key is pressed in the . /// diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index 0472b604db..2369e9f530 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins if (sender != null) { - var itemsControl = sender.Parent as SelectingItemsControl; - - if ((bool)x.NewValue) - { - ((IPseudoClasses)sender.Classes).Add(":selected"); - - if (((IVisual)sender).IsAttachedToVisualTree && - itemsControl?.AutoScrollToSelectedItem == true) - { - sender.BringIntoView(); - } - } - else - { - ((IPseudoClasses)sender.Classes).Remove(":selected"); - } + ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); sender.RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 55bdbc9dd4..793399841c 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -79,12 +79,28 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + /// + /// Renders the visual to a . + /// + /// The drawing context. + public override void Render(DrawingContext context) + { + var background = Background; + if (background != null) + { + var renderSize = Bounds.Size; + context.FillRectangle(background, new Rect(renderSize)); + } + + base.Render(context); + } + /// /// Called when the collection changes. /// /// The event sender. /// The event args. - private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { List controls; @@ -122,21 +138,5 @@ namespace Avalonia.Controls InvalidateMeasure(); } - - /// - /// Renders the visual to a . - /// - /// The drawing context. - public override void Render(DrawingContext context) - { - var background = Background; - if (background != null) - { - var renderSize = Bounds.Size; - context.FillRectangle(background, new Rect(renderSize)); - } - - base.Render(context); - } } } diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a4f9c0cb43..8b408002a1 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters } /// - protected override void CreatePanel() + protected override void PanelCreated(IPanel panel) { - base.CreatePanel(); var task = MoveToPage(-1, SelectedIndex); } @@ -175,12 +174,9 @@ namespace Avalonia.Controls.Presenters if (container == null) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize( - index, - new[] { item }, - MemberSelector); - container = materialized.First().ContainerControl; - Panel.Children.Add(container); + var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + Panel.Children.Add(materialized.ContainerControl); + container = materialized.ContainerControl; } return container; diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 3470a43c1c..4c5a488003 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; + private IDataTemplate _dataTemplate; /// /// Initializes static members of the class. @@ -95,10 +96,6 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { - var dataContext = this.GetObservable(ContentProperty) - .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); - - Bind(Control.DataContextProperty, dataContext); } /// @@ -200,6 +197,13 @@ namespace Avalonia.Controls.Presenters } } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _dataTemplate = null; + } + /// /// Updates the control based on the control's . /// @@ -213,34 +217,86 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { - var old = Child; var content = Content; - var result = this.MaterializeDataTemplate(content, ContentTemplate); + var oldChild = Child; + var newChild = content as IControl; + + if (content != null && newChild == null) + { + // We have content and it isn't a control, so first try to recycle the existing + // child control to display the new data by querying if the template that created + // the child can recycle items and that it also matches the new data. + if (oldChild != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) + { + newChild = oldChild; + } + else + { + // We couldn't recycle an existing control so find a data template for the data + // and use it to create a control. + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + newChild = _dataTemplate.Build(content); + + // Try to give the new control its own name scope. + var controlResult = newChild as Control; + + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } + } + } + else + { + _dataTemplate = null; + } + + // Remove the old child if we're not recycling it. + if (oldChild != null && newChild != oldChild) + { + VisualChildren.Remove(oldChild); + } - if (old != null) + // Set the DataContext if the data isn't a control. + if (!(content is IControl)) { - VisualChildren.Remove(old); + DataContext = content; } - if (result != null) + // Update the Child. + if (newChild == null) + { + Child = null; + } + else if (newChild != oldChild) { - if (!(content is IControl)) + ((ISetInheritanceParent)newChild).SetParent(this); + + Child = newChild; + + if (oldChild?.Parent == this) { - result.DataContext = content; + LogicalChildren.Remove(oldChild); } - Child = result; - - if (result.Parent == null) + if (newChild.Parent == null) { - ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); + var templatedLogicalParent = TemplatedParent as ILogical; + + if (templatedLogicalParent != null) + { + ((ISetLogicalParent)newChild).SetParent(templatedLogicalParent); + } + else + { + LogicalChildren.Add(newChild); + } } - VisualChildren.Add(result); - } - else - { - Child = null; + VisualChildren.Add(newChild); } _createdChild = true; diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 95df903ed3..42311dc781 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs new file mode 100644 index 0000000000..964ce82849 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -0,0 +1,198 @@ +// 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; +using System.Collections.Specialized; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Utils; +using Avalonia.Input; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Presenters +{ + /// + /// Base class for classes which handle virtualization for an . + /// + internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable + { + private bool disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// + public ItemVirtualizer(ItemsPresenter owner) + { + Owner = owner; + Items = owner.Items; + ItemCount = owner.Items.Count(); + } + + /// + /// Gets the which owns the virtualizer. + /// + public ItemsPresenter Owner { get; } + + /// + /// Gets the which will host the items. + /// + public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; + + /// + /// Gets the items to display. + /// + public IEnumerable Items { get; private set; } + + /// + /// Gets the number of items in . + /// + public int ItemCount { get; private set; } + + /// + /// Gets or sets the index of the first item displayed in the panel. + /// + public int FirstIndex { get; protected set; } + + /// + /// Gets or sets the index of the first item beyond those displayed in the panel. + /// + public int NextIndex { get; protected set; } + + /// + /// Gets a value indicating whether the items should be scroll horizontally or vertically. + /// + public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; + + /// + /// Gets a value indicating whether logical scrolling is enabled. + /// + public abstract bool IsLogicalScrollEnabled { get; } + + /// + /// Gets the value of the scroll extent. + /// + public abstract double ExtentValue { get; } + + /// + /// Gets or sets the value of the current scroll offset. + /// + public abstract double OffsetValue { get; set; } + + /// + /// Gets the value of the scrollable viewport. + /// + public abstract double ViewportValue { get; } + + /// + /// Gets the as a . + /// + public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); + + /// + /// Gets the as a . + /// + public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); + + /// + /// Gets or sets the as a . + /// + public Vector Offset + { + get + { + return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); + } + + set + { + OffsetValue = Vertical ? value.Y : value.X; + } + } + + /// + /// Creates an based on an item presenter's + /// . + /// + /// The items presenter. + /// An . + public static ItemVirtualizer Create(ItemsPresenter owner) + { + var virtualizingPanel = owner.Panel as IVirtualizingPanel; + var scrollable = (ILogicalScrollable)owner; + ItemVirtualizer result = null; + + if (virtualizingPanel != null && scrollable.InvalidateScroll != null) + { + switch (owner.VirtualizationMode) + { + case ItemVirtualizationMode.Simple: + result = new ItemVirtualizerSimple(owner); + break; + } + } + + if (result == null) + { + result = new ItemVirtualizerNone(owner); + } + + if (virtualizingPanel != null) + { + virtualizingPanel.Controller = result; + } + + return result; + } + + /// + public virtual void UpdateControls() + { + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from) + { + return null; + } + + /// + /// Called when the items for the presenter change, either because + /// has been set, the items collection has been + /// modified, or the panel has been created. + /// + /// The items. + /// A description of the change. + public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + Items = items; + ItemCount = items.Count(); + } + + /// + /// Scrolls the specified item into view. + /// + /// The item. + public virtual void ScrollIntoView(object item) + { + } + + /// + public virtual void Dispose() + { + VirtualizingPanel.Controller = null; + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + } + + /// + /// Invalidates the current scroll. + /// + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs new file mode 100644 index 0000000000..411f309368 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -0,0 +1,173 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls.Presenters +{ + /// + /// Represents an item virtualizer for an that doesn't actually + /// virtualize items - it just creates a container for every item. + /// + internal class ItemVirtualizerNone : ItemVirtualizer + { + public ItemVirtualizerNone(ItemsPresenter owner) + : base(owner) + { + if (Items != null && owner.Panel != null) + { + AddContainers(0, Items); + } + } + + /// + public override bool IsLogicalScrollEnabled => false; + + /// + /// This property should never be accessed because is + /// false. + /// + public override double ExtentValue + { + get { throw new NotSupportedException(); } + } + + /// + /// This property should never be accessed because is + /// false. + /// + public override double OffsetValue + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// This property should never be accessed because is + /// false. + /// + public override double ViewportValue + { + get { throw new NotSupportedException(); } + } + + /// + public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + base.ItemsChanged(items, e); + + var generator = Owner.ItemContainerGenerator; + var panel = Owner.Panel; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex + e.NewItems.Count < Items.Count()) + { + generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); + } + + AddContainers(e.NewStartingIndex, e.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); + break; + + case NotifyCollectionChangedAction.Replace: + RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); + var containers = AddContainers(e.NewStartingIndex, e.NewItems); + + var i = e.NewStartingIndex; + + foreach (var container in containers) + { + panel.Children[i++] = container.ContainerControl; + } + + break; + + case NotifyCollectionChangedAction.Move: + // TODO: Handle move in a more efficient manner. At the moment we just + // drop through to Reset to recreate all the containers. + + case NotifyCollectionChangedAction.Reset: + RemoveContainers(generator.Clear()); + + if (Items != null) + { + AddContainers(0, Items); + } + + break; + } + + Owner.InvalidateMeasure(); + } + + /// + /// Scrolls the specified item into view. + /// + /// The item. + public override void ScrollIntoView(object item) + { + if (Items != null) + { + var index = Items.IndexOf(item); + + if (index != -1) + { + var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); + container.BringIntoView(); + } + } + } + + private IList AddContainers(int index, IEnumerable items) + { + var generator = Owner.ItemContainerGenerator; + var result = new List(); + var panel = Owner.Panel; + + foreach (var item in items) + { + var i = generator.Materialize(index++, item, Owner.MemberSelector); + + if (i.ContainerControl != null) + { + if (i.Index < panel.Children.Count) + { + // TODO: This will insert at the wrong place when there are null items. + panel.Children.Insert(i.Index, i.ContainerControl); + } + else + { + panel.Children.Add(i.ContainerControl); + } + } + + result.Add(i); + } + + return result; + } + + private void RemoveContainers(IEnumerable items) + { + var panel = Owner.Panel; + + foreach (var i in items) + { + if (i.ContainerControl != null) + { + panel.Children.Remove(i.ContainerControl); + } + } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs new file mode 100644 index 0000000000..27a29a1e04 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -0,0 +1,504 @@ +// 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; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Controls.Utils; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Presenters +{ + /// + /// Handles virtualization in an for + /// . + /// + internal class ItemVirtualizerSimple : ItemVirtualizer + { + /// + /// Initializes a new instance of the class. + /// + /// + public ItemVirtualizerSimple(ItemsPresenter owner) + : base(owner) + { + // Don't need to add children here as UpdateControls should be called by the panel + // measure/arrange. + } + + /// + public override bool IsLogicalScrollEnabled => true; + + /// + public override double ExtentValue => ItemCount; + + /// + public override double OffsetValue + { + get + { + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + return FirstIndex + offset; + } + + set + { + var panel = VirtualizingPanel; + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + var delta = (int)(value - (FirstIndex + offset)); + + if (delta != 0) + { + var newLastIndex = (NextIndex - 1) + delta; + + if (newLastIndex < ItemCount) + { + if (panel.PixelOffset > 0) + { + panel.PixelOffset = 0; + delta += 1; + } + + if (delta != 0) + { + RecycleContainersForMove(delta); + } + } + else + { + // We're moving to a partially obscured item at the end of the list so + // offset the panel by the height of the first item. + var firstIndex = ItemCount - panel.Children.Count; + RecycleContainersForMove(firstIndex - FirstIndex); + + panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? + panel.Children[0].Bounds.Height : + panel.Children[0].Bounds.Width; + } + } + } + } + + /// + public override double ViewportValue + { + get + { + // If we can't fit the last item in the panel fully, subtract 1 from the viewport. + var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; + return VirtualizingPanel.Children.Count - overflow; + } + } + + /// + public override void UpdateControls() + { + CreateAndRemoveContainers(); + InvalidateScroll(); + } + + /// + public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + base.ItemsChanged(items, e); + + var panel = VirtualizingPanel; + + if (items != null) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + CreateAndRemoveContainers(); + + if (e.NewStartingIndex >= FirstIndex && + e.NewStartingIndex + e.NewItems.Count <= NextIndex) + { + RecycleContainers(); + } + + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldStartingIndex >= FirstIndex && + e.OldStartingIndex + e.OldItems.Count <= NextIndex) + { + RecycleContainersOnRemove(); + } + + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + RecycleContainers(); + break; + + case NotifyCollectionChangedAction.Reset: + RecycleContainersOnRemove(); + CreateAndRemoveContainers(); + break; + } + } + else + { + Owner.ItemContainerGenerator.Clear(); + VirtualizingPanel.Children.Clear(); + FirstIndex = NextIndex = 0; + } + + // If we are scrolled to view a partially visible last item but controls were added + // then we need to return to a non-offset scroll position. + if (panel.PixelOffset != 0 && FirstIndex + panel.Children.Count < ItemCount) + { + panel.PixelOffset = 0; + RecycleContainersForMove(1); + } + + InvalidateScroll(); + } + + public override IControl GetControlInDirection(NavigationDirection direction, IControl from) + { + var generator = Owner.ItemContainerGenerator; + var panel = VirtualizingPanel; + var itemIndex = generator.IndexFromContainer(from); + var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; + + if (itemIndex == -1) + { + return null; + } + + var newItemIndex = -1; + + switch (direction) + { + case NavigationDirection.First: + newItemIndex = 0; + break; + + case NavigationDirection.Last: + newItemIndex = ItemCount - 1; + break; + + case NavigationDirection.Up: + if (vertical) + { + newItemIndex = itemIndex - 1; + } + + break; + case NavigationDirection.Down: + if (vertical) + { + newItemIndex = itemIndex + 1; + } + + break; + + case NavigationDirection.Left: + if (!vertical) + { + newItemIndex = itemIndex - 1; + } + break; + + case NavigationDirection.Right: + if (!vertical) + { + newItemIndex = itemIndex + 1; + } + break; + + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; + } + + return ScrollIntoView(newItemIndex); + } + + /// + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); + + if (index != -1) + { + ScrollIntoView(index); + } + } + + /// + /// Creates and removes containers such that we have at most enough containers to fill + /// the panel. + /// + private void CreateAndRemoveContainers() + { + var generator = Owner.ItemContainerGenerator; + var panel = VirtualizingPanel; + + if (!panel.IsFull && Items != null) + { + var memberSelector = Owner.MemberSelector; + var index = NextIndex; + var step = 1; + + while (!panel.IsFull) + { + if (index >= ItemCount) + { + // We can fit more containers in the panel, but we're at the end of the + // items. If we're scrolled to the top (FirstIndex == 0), then there are + // no more items to create. Otherwise, go backwards adding containers to + // the beginning of the panel. + if (FirstIndex == 0) + { + break; + } + else + { + index = FirstIndex - 1; + step = -1; + } + } + + var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + + if (step == 1) + { + panel.Children.Add(materialized.ContainerControl); + } + else + { + panel.Children.Insert(0, materialized.ContainerControl); + } + + index += step; + } + + if (step == 1) + { + NextIndex = index; + } + else + { + NextIndex = ItemCount; + FirstIndex = index + 1; + } + } + + if (panel.OverflowCount > 0) + { + RemoveContainers(panel.OverflowCount); + } + } + + /// + /// Updates the containers in the panel to make sure they are displaying the correct item + /// based on . + /// + /// + /// This method requires that + the number of + /// materialized containers is not more than . + /// + private void RecycleContainers() + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var containers = generator.Containers.ToList(); + var itemIndex = FirstIndex; + + foreach (var container in containers) + { + var item = Items.ElementAt(itemIndex); + + if (!object.Equals(container.Item, item)) + { + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + ++itemIndex; + } + } + + /// + /// Recycles containers when a move occurs. + /// + /// The delta of the move. + /// + /// If the move is less than a page, then this method moves the containers for the items + /// that are still visible to the correct place, and recyles and moves the others. For + /// example: if there are 20 items and 10 containers visible and the user scrolls 5 + /// items down, then the bottom 5 containers will be moved to the top and the top 5 will + /// be moved to the bottom and recycled to display the newly visible item. Updates + /// and + /// with their new values. + /// + private void RecycleContainersForMove(int delta) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var sign = delta < 0 ? -1 : 1; + var count = Math.Min(Math.Abs(delta), panel.Children.Count); + var move = count < panel.Children.Count; + var first = delta < 0 && move ? panel.Children.Count + delta : 0; + var containers = panel.Children.GetRange(first, count).ToList(); + + for (var i = 0; i < count; ++i) + { + var oldItemIndex = FirstIndex + first + i; + var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); + + var item = Items.ElementAt(newItemIndex); + + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + if (move) + { + if (delta > 0) + { + panel.Children.MoveRange(first, count, panel.Children.Count); + } + else + { + panel.Children.MoveRange(first, count, 0); + } + } + + FirstIndex += delta; + NextIndex += delta; + } + + /// + /// Recycles containers due to items being removed. + /// + private void RecycleContainersOnRemove() + { + var panel = VirtualizingPanel; + + if (NextIndex <= ItemCount) + { + // Items have been removed but FirstIndex..NextIndex is still a valid range in the + // items, so just recycle the containers to adapt to the new state. + RecycleContainers(); + } + else + { + // Items have been removed and now the range FirstIndex..NextIndex goes out of + // the item bounds. Remove any excess containers, try to scroll up and then recycle + // the containers to make sure they point to the correct item. + var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); + var delta = newFirstIndex - FirstIndex; + var newNextIndex = NextIndex + delta; + + if (newNextIndex > ItemCount) + { + RemoveContainers(newNextIndex - ItemCount); + } + + if (delta != 0) + { + RecycleContainersForMove(delta); + } + + RecycleContainers(); + } + } + + /// + /// Removes the specified number of containers from the end of the panel and updates + /// . + /// + /// The number of containers to remove. + private void RemoveContainers(int count) + { + var index = VirtualizingPanel.Children.Count - count; + + VirtualizingPanel.Children.RemoveRange(index, count); + Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); + NextIndex -= count; + } + + /// + /// Scrolls the item with the specified index into view. + /// + /// The item index. + /// The container that was brought into view. + private IControl ScrollIntoView(int index) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var newOffset = -1.0; + + if (index >= 0 && index < ItemCount) + { + if (index < FirstIndex) + { + newOffset = index; + } + else if (index >= NextIndex) + { + newOffset = index - Math.Ceiling(ViewportValue - 1); + } + else if (OffsetValue + ViewportValue >= ItemCount) + { + newOffset = OffsetValue - 1; + } + + if (newOffset != -1) + { + OffsetValue = newOffset; + } + + var container = generator.ContainerFromIndex(index); + var layoutManager = LayoutManager.Instance; + + // We need to do a layout here because it's possible that the container we moved to + // is only partially visible due to differing item sizes. If the container is only + // partially visible, scroll again. Don't do this if there's no layout manager: + // it means we're running a unit test. + if (layoutManager != null) + { + layoutManager.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += 1; + } + } + + return container; + } + + return null; + } + + /// + /// Ensures an offset value is within the value range. + /// + /// The value. + /// The coerced value. + private double CoerceOffset(double value) + { + var max = Math.Max(ExtentValue - ViewportValue, 0); + return MathUtilities.Clamp(value, 0, max); + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8c8ab5c0f8..ffc2680c83 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -1,19 +1,29 @@ // 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; using System.Collections.Specialized; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Utils; +using Avalonia.Controls.Primitives; using Avalonia.Input; +using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters { /// /// Displays items inside an . /// - public class ItemsPresenter : ItemsPresenterBase + public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable { + /// + /// Defines the property. + /// + public static readonly StyledProperty VirtualizationModeProperty = + AvaloniaProperty.Register( + nameof(VirtualizationMode), + defaultValue: ItemVirtualizationMode.Simple); + + private ItemVirtualizer _virtualizer; + /// /// Initializes static members of the class. /// @@ -22,127 +32,101 @@ namespace Avalonia.Controls.Presenters KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( typeof(ItemsPresenter), KeyboardNavigationMode.Once); + + VirtualizationModeProperty.Changed + .AddClassHandler(x => x.VirtualizationModeChanged); } - /// - protected override void CreatePanel() + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode { - base.CreatePanel(); - - if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) - { - KeyboardNavigation.SetDirectionalNavigation( - (InputElement)Panel, - KeyboardNavigationMode.Contained); - } - - KeyboardNavigation.SetTabNavigation( - (InputElement)Panel, - KeyboardNavigation.GetTabNavigation(this)); + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } } /// - protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) + bool ILogicalScrollable.IsLogicalScrollEnabled { - var generator = ItemContainerGenerator; + get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } + } - // TODO: Handle Move and Replace etc. - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (e.NewStartingIndex + e.NewItems.Count < Items.Count()) - { - generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); - } + /// + Size IScrollable.Extent => _virtualizer.Extent; - AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); - break; + /// + Vector IScrollable.Offset + { + get { return _virtualizer.Offset; } + set { _virtualizer.Offset = CoerceOffset(value); } + } + + /// + Size IScrollable.Viewport => _virtualizer.Viewport; - case NotifyCollectionChangedAction.Remove: - RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); - break; + /// + Action ILogicalScrollable.InvalidateScroll { get; set; } - case NotifyCollectionChangedAction.Replace: - RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); - var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); - AddContainers(containers); + /// + Size ILogicalScrollable.ScrollSize => new Size(1, 1); - var i = e.NewStartingIndex; + /// + Size ILogicalScrollable.PageScrollSize => new Size(0, 1); - foreach (var container in containers) - { - Panel.Children[i++] = container.ContainerControl; - } + /// + bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) + { + return false; + } - break; + /// + IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) + { + return _virtualizer?.GetControlInDirection(direction, from); + } - case NotifyCollectionChangedAction.Move: - // TODO: Implement Move in a more efficient manner. - case NotifyCollectionChangedAction.Reset: - RemoveContainers(generator.Clear()); + public override void ScrollIntoView(object item) + { + _virtualizer?.ScrollIntoView(item); + } - if (Items != null) - { - AddContainers(generator.Materialize(0, Items, MemberSelector)); - } + /// + protected override void PanelCreated(IPanel panel) + { + _virtualizer = ItemVirtualizer.Create(this); + ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); - break; + if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) + { + KeyboardNavigation.SetDirectionalNavigation( + (InputElement)Panel, + KeyboardNavigationMode.Contained); } - InvalidateMeasure(); + KeyboardNavigation.SetTabNavigation( + (InputElement)Panel, + KeyboardNavigation.GetTabNavigation(this)); } - private void AddContainersToPanel(IEnumerable items) + protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) { - foreach (var i in items) - { - if (i.ContainerControl != null) - { - if (i.Index < this.Panel.Children.Count) - { - // HACK: This will insert at the wrong place when there are null items, - // but all of this will need to be rewritten when we implement - // virtualization so hope no-one notices until then :) - this.Panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - this.Panel.Children.Add(i.ContainerControl); - } - } - } + _virtualizer?.ItemsChanged(Items, e); } - private void AddContainers(IEnumerable items) + private Vector CoerceOffset(Vector value) { - foreach (var i in items) - { - if (i.ContainerControl != null) - { - if (i.Index < this.Panel.Children.Count) - { - // HACK: This will insert at the wrong place when there are null items, - // but all of this will need to be rewritten when we implement - // virtualization so hope no-one notices until then :) - this.Panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - this.Panel.Children.Add(i.ContainerControl); - } - } - } + var scrollable = (ILogicalScrollable)this; + var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); + var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); } - private void RemoveContainers(IEnumerable items) + private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) { - foreach (var i in items) - { - if (i.ContainerControl != null) - { - this.Panel.Children.Remove(i.ContainerControl); - } - } + _virtualizer?.Dispose(); + _virtualizer = ItemVirtualizer.Create(this); + ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 91ffec9c4c..5a56e52029 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -101,8 +101,7 @@ namespace Avalonia.Controls.Presenters { if (_generator == null) { - var i = TemplatedParent as ItemsControl; - _generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this); + _generator = CreateItemContainerGenerator(); } return _generator; @@ -164,6 +163,31 @@ namespace Avalonia.Controls.Presenters } } + /// + public virtual void ScrollIntoView(object item) + { + } + + /// + /// Creates the for the control. + /// + /// + /// An or null. + /// + protected virtual IItemContainerGenerator CreateItemContainerGenerator() + { + var i = TemplatedParent as ItemsControl; + var result = i?.ItemContainerGenerator; + + if (result == null) + { + result = new ItemContainerGenerator(this); + result.ItemTemplate = ItemTemplate; + } + + return result; + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -178,11 +202,26 @@ namespace Avalonia.Controls.Presenters return finalSize; } + /// + /// Called when the is created. + /// + /// The panel. + protected virtual void PanelCreated(IPanel panel) + { + } + + /// + /// Called when the items for the presenter change, either because + /// has been set, the items collection has been modified, or the panel has been created. + /// + /// A description of the change. + protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); + /// /// Creates the when is called for the first /// time. /// - protected virtual void CreatePanel() + private void CreatePanel() { Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); @@ -201,16 +240,11 @@ namespace Avalonia.Controls.Presenters incc.CollectionChanged += ItemsCollectionChanged; } + PanelCreated(Panel); + ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - /// - /// Called when the items for the presenter change, either because - /// has been set, or the items collection has been modified. - /// - /// A description of the change. - protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); - /// /// Called when the collection changes. /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0077497b10..d9f0260e39 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents a scrolling view of content inside a . /// - public class ScrollContentPresenter : ContentPresenter, IPresenter + public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable { /// /// Defines the property. @@ -50,7 +50,7 @@ namespace Avalonia.Controls.Presenters private Size _extent; private Size _measuredExtent; private Vector _offset; - private IDisposable _scrollableSubscription; + private IDisposable _logicalScrollSubscription; private Size _viewport; /// @@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); + ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); AffectsArrange(OffsetProperty); } @@ -69,7 +70,7 @@ namespace Avalonia.Controls.Presenters { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); - this.GetObservable(ChildProperty).Subscribe(ChildChanged); + this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } /// @@ -117,6 +118,14 @@ namespace Avalonia.Controls.Presenters return false; } + var scrollable = Child as ILogicalScrollable; + var control = target as IControl; + + if (scrollable?.IsLogicalScrollEnabled == true && control != null) + { + return scrollable.BringIntoView(control, targetRect); + } + var transform = target.TransformToVisual(Child); if (transform == null) @@ -169,7 +178,7 @@ namespace Avalonia.Controls.Presenters { var measureSize = availableSize; - if (_scrollableSubscription == null) + if (_logicalScrollSubscription == null) { measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); @@ -194,21 +203,25 @@ namespace Avalonia.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; - var offset = default(Vector); + var logicalScroll = _logicalScrollSubscription != null; - if (_scrollableSubscription == null) + if (!logicalScroll) { Viewport = finalSize; Extent = _measuredExtent; - offset = Offset; - } - if (child != null) - { - var size = new Size( + if (child != null) + { + var size = new Size( Math.Max(finalSize.Width, child.DesiredSize.Width), Math.Max(finalSize.Height, child.DesiredSize.Height)); - child.Arrange(new Rect((Point)(-offset), size)); + child.Arrange(new Rect((Point)(-Offset), size)); + return finalSize; + } + } + else if (child != null) + { + child.Arrange(new Rect(finalSize)); return finalSize; } @@ -218,26 +231,32 @@ namespace Avalonia.Controls.Presenters /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - if (Extent.Height > Viewport.Height) + if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) { - var scrollable = Child as IScrollable; + var scrollable = Child as ILogicalScrollable; + bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + + double x = Offset.X; + double y = Offset.Y; - if (scrollable != null) - { - var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height); + if (Extent.Height > Viewport.Height) + { + double height = isLogical ? scrollable.ScrollSize.Height : 50; + y += -e.Delta.Y * height; y = Math.Max(y, 0); y = Math.Min(y, Extent.Height - Viewport.Height); - Offset = new Vector(Offset.X, y); - e.Handled = true; } - else + + if (Extent.Width > Viewport.Width) { - var y = Offset.Y + (-e.Delta.Y * 50); - y = Math.Max(y, 0); - y = Math.Min(y, Extent.Height - Viewport.Height); - Offset = new Vector(Offset.X, y); - e.Handled = true; + double width = isLogical ? scrollable.ScrollSize.Width : 50; + x += -e.Delta.X * width; + x = Math.Max(x, 0); + x = Math.Min(x, Extent.Width - Viewport.Width); } + + Offset = new Vector(x, y); + e.Handled = true; } } @@ -246,28 +265,53 @@ namespace Avalonia.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } - private void ChildChanged(IControl child) + private void ChildChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateScrollableSubscription((IControl)e.NewValue); + + if (e.OldValue != null) + { + Offset = default(Vector); + } + } + + private void UpdateScrollableSubscription(IControl child) { - var scrollable = child as IScrollable; + var scrollable = child as ILogicalScrollable; - _scrollableSubscription?.Dispose(); - _scrollableSubscription = null; + _logicalScrollSubscription?.Dispose(); + _logicalScrollSubscription = null; if (scrollable != null) { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - _scrollableSubscription = new CompositeDisposable( - this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), - Disposable.Create(() => scrollable.InvalidateScroll = null)); - UpdateFromScrollable(scrollable); + + if (scrollable.IsLogicalScrollEnabled == true) + { + _logicalScrollSubscription = new CompositeDisposable( + this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + Disposable.Create(() => scrollable.InvalidateScroll = null)); + UpdateFromScrollable(scrollable); + } } } - private void UpdateFromScrollable(IScrollable scrollable) + private void UpdateFromScrollable(ILogicalScrollable scrollable) { - Viewport = scrollable.Viewport; - Extent = scrollable.Extent; - Offset = scrollable.Offset; + var logicalScroll = _logicalScrollSubscription != null; + + if (logicalScroll != scrollable.IsLogicalScrollEnabled) + { + UpdateScrollableSubscription(Child); + Offset = default(Vector); + InvalidateMeasure(); + } + else if (scrollable.IsLogicalScrollEnabled) + { + Viewport = scrollable.Viewport; + Extent = scrollable.Extent; + Offset = scrollable.Offset; + } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index b732387cf5..1dfa2d7af2 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -60,7 +60,7 @@ namespace Avalonia.Controls.Primitives if (info != null) { child.RenderTransform = new MatrixTransform(info.Bounds.Transform); - child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); + child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); child.Arrange(info.Bounds.Bounds); } else diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs new file mode 100644 index 0000000000..6c8f463a96 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -0,0 +1,69 @@ +// 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 Avalonia.Input; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Interface implemented by controls that handle their own scrolling when placed inside a + /// . + /// + /// + /// Controls that implement this interface, when placed inside a + /// can override the physical scrolling behavior of the scroll viewer with logical scrolling. + /// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas + /// whereas logical scrolling means that the scrolling is handled by the child control itself + /// and it can choose to do handle the scroll information as it sees fit. + /// + public interface ILogicalScrollable : IScrollable + { + /// + /// Gets a value indicating whether logical scrolling is enabled on the control. + /// + bool IsLogicalScrollEnabled { get; } + + /// + /// Gets or sets the scroll invalidation method. + /// + /// + /// + /// This method notifies the attached of a change in + /// the , or + /// properties. + /// + /// + /// This property is set by the parent when the + /// is placed inside it. + /// + /// + Action InvalidateScroll { get; set; } + + /// + /// Gets the size to scroll by, in logical units. + /// + Size ScrollSize { get; } + + /// + /// Gets the size to page by, in logical units. + /// + Size PageScrollSize { get; } + + /// + /// Attempts to bring a portion of the target visual into view by scrolling the content. + /// + /// The target visual. + /// The portion of the target visual to bring into view. + /// True if the scroll offset was changed; otherwise false. + bool BringIntoView(IControl target, Rect targetRect); + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + IControl GetControlInDirection(NavigationDirection direction, IControl from); + } +} diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs deleted file mode 100644 index a5165fcfb4..0000000000 --- a/src/Avalonia.Controls/Primitives/IScrollable.cs +++ /dev/null @@ -1,54 +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.Primitives -{ - /// - /// Interface implemented by controls that handle their own scrolling when placed inside a - /// . - /// - public interface IScrollable - { - /// - /// Gets or sets the scroll invalidation method. - /// - /// - /// - /// This method notifies the attached of a change in - /// the , or properties. - /// - /// - /// This property is set by the parent when the - /// is placed inside it. - /// - /// - Action InvalidateScroll { get; set; } - - /// - /// Gets the extent of the scrollable content, in logical units - /// - Size Extent { get; } - - /// - /// Gets or sets the current scroll offset, in logical units. - /// - Vector Offset { get; set; } - - /// - /// Gets the size of the viewport, in logical units. - /// - Size Viewport { get; } - - /// - /// Gets the size to scroll by, in logical units. - /// - Size ScrollSize { get; } - - /// - /// Gets the size to page by, in logical units. - /// - Size PageScrollSize { get; } - } -} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c24d1cef2d..81d01ff74f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -10,6 +10,7 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.VisualTree; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -39,6 +40,18 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalOffsetProperty = + AvaloniaProperty.Register(nameof(HorizontalOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalOffsetProperty = + AvaloniaProperty.Register(nameof(VerticalOffset)); + /// /// Defines the property. /// @@ -122,6 +135,24 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets the Horizontal offset of the popup in relation to the + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + + /// + /// Gets or sets the Vertical offset of the popup in relation to the + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + /// /// Gets or sets the control that is used to determine the popup's position. /// @@ -287,18 +318,26 @@ namespace Avalonia.Controls.Primitives if (target?.GetVisualRoot() == null) { mode = PlacementMode.Pointer; - } + } switch (mode) { case PlacementMode.Pointer: - return MouseDevice.Instance?.Position ?? default(Point); + if (MouseDevice.Instance != null) + { + // Scales the Horizontal and Vertical offset to screen co-ordinates. + var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); + return MouseDevice.Instance.Position + screenOffset; + } + + return default(Point); case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0, target.Bounds.Height)) ?? zero; + + return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width, 0)) ?? zero; + return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero; default: throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 018507bbc8..27f3407fd9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item); + /// /// Tries to get the container that was the source of an event. /// @@ -394,6 +400,19 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnContainersRecycled(ItemContainerEventArgs e) + { + foreach (var i in e.Containers) + { + if (i.ContainerControl != null && i.Item != null) + { + MarkContainerSelected( + i.ContainerControl, + SelectedItems.Contains(i.Item)); + } + } + } + /// protected override void OnDataContextChanging() { @@ -710,6 +729,12 @@ namespace Avalonia.Controls.Primitives { case NotifyCollectionChangedAction.Add: SelectedItemsAdded(e.NewItems.Cast().ToList()); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(e.NewItems[0]); + } + added = e.NewItems; break; diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 4a6c9af95f..0e15ae4d7b 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -9,6 +9,9 @@ namespace Avalonia.Controls.Primitives { public class TabStrip : SelectingItemsControl { + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); + private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); static TabStrip() @@ -16,6 +19,7 @@ namespace Avalonia.Controls.Primitives MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 4f5cf304b2..c52c1ad505 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl + public class ScrollViewer : ContentControl, IScrollable { /// /// Defines the property. @@ -370,7 +370,7 @@ namespace Avalonia.Controls private static double Max(double x, double y) { var result = Math.Max(x, y); - return double.IsNaN(result) ? 0 : Math.Round(result); + return double.IsNaN(result) ? 0 : result; } private static Vector ValidateOffset(AvaloniaObject o, Vector value) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 1a032d4def..26a755e5f1 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -72,37 +72,52 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + { + var fromControl = from as IControl; + return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; + default: + index = -1; + break; } if (index >= 0 && index < Children.Count) @@ -181,6 +196,7 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { + var orientation = Orientation; double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; double gap = Gap; @@ -199,11 +215,11 @@ namespace Avalonia.Controls double childWidth = child.DesiredSize.Width; double childHeight = child.DesiredSize.Height; - if (Orientation == Orientation.Vertical) + if (orientation == Orientation.Vertical) { double width = Math.Max(childWidth, arrangedWidth); Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); - child.Arrange(childFinal); + ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); arrangedHeight += childHeight + gap; } @@ -211,13 +227,13 @@ namespace Avalonia.Controls { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); - child.Arrange(childFinal); + ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth += childWidth + gap; arrangedHeight = Math.Max(arrangedHeight, childHeight); } } - if (Orientation == Orientation.Vertical) + if (orientation == Orientation.Vertical) { arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); } @@ -228,5 +244,14 @@ namespace Avalonia.Controls return new Size(arrangedWidth, arrangedHeight); } + + internal virtual void ArrangeChild( + IControl child, + Rect rect, + Size panelSize, + Orientation orientation) + { + child.Arrange(rect); + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs index 9eff4243b1..df25733524 100644 --- a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs +++ b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs @@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates /// public static class DataTemplateExtensions { - /// - /// Materializes a piece of data based on a data template. - /// - /// The control materializing the data template. - /// The data. - /// - /// An optional primary template that can will be tried before the - /// in the tree are searched. - /// - /// The data materialized as a control. - public static IControl MaterializeDataTemplate( - this IControl control, - object data, - IDataTemplate primary = null) - { - if (data == null) - { - return null; - } - else - { - var asControl = data as IControl; - - if (asControl != null) - { - return asControl; - } - else - { - IDataTemplate template = control.FindDataTemplate(data, primary); - IControl result; - - if (template != null) - { - result = template.Build(data); - } - else - { - result = FuncDataTemplate.Default.Build(data); - } - - NameScope.SetNameScope((Control)result, new NameScope()); - - return result; - } - } - } - /// /// Find a data template that matches a piece of data. /// diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 8ce3f69f36..1d90fcd01e 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -2,6 +2,7 @@ // 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.Reflection; namespace Avalonia.Controls.Templates @@ -12,10 +13,26 @@ namespace Avalonia.Controls.Templates public class FuncDataTemplate : FuncTemplate, IDataTemplate { /// - /// The default data template used in the case where not matching data template is found. + /// The default data template used in the case where no matching data template is found. /// public static readonly FuncDataTemplate Default = - new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null); + new FuncDataTemplate( + data => + { + if (data != null) + { + var result = new TextBlock(); + result.Bind( + TextBlock.TextProperty, + result.GetObservable(Control.DataContextProperty).Select(x => x?.ToString())); + return result; + } + else + { + return null; + } + }, + true); /// /// The implementation of the method. @@ -29,8 +46,12 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Type type, Func build) - : this(o => IsInstance(o, type), build) + /// Whether the control can be recycled. + public FuncDataTemplate( + Type type, + Func build, + bool supportsRecycling = false) + : this(o => IsInstance(o, type), build, supportsRecycling) { } @@ -43,14 +64,22 @@ namespace Avalonia.Controls.Templates /// /// A function which returns a control for matching data. /// - public FuncDataTemplate(Func match, Func build) + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) : base(build) { Contract.Requires(match != null); _match = match; + SupportsRecycling = supportsRecycling; } + /// + public bool SupportsRecycling { get; } + /// /// Checks to see if this data template matches the specified data. /// diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index cb2d064605..7154c0e558 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -17,8 +17,9 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Func build) - : base(typeof(T), CastBuild(build)) + /// Whether the control can be recycled. + public FuncDataTemplate(Func build, bool supportsRecycling = false) + : base(typeof(T), CastBuild(build), supportsRecycling) { } @@ -31,8 +32,12 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Func match, Func build) - : base(CastMatch(match), CastBuild(build)) + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) + : base(CastMatch(match), CastBuild(build), supportsRecycling) { } diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTemplate`1.cs index c04b08de4d..88321ee1a3 100644 --- a/src/Avalonia.Controls/Templates/FuncTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTemplate`1.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Styling; namespace Avalonia.Controls.Templates { @@ -34,5 +35,7 @@ namespace Avalonia.Controls.Templates { return _func(); } + + object ITemplate.Build() => Build(); } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Templates/IDataTemplate.cs b/src/Avalonia.Controls/Templates/IDataTemplate.cs index ee490da4de..f9c97e55f3 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplate.cs @@ -8,6 +8,12 @@ namespace Avalonia.Controls.Templates /// public interface IDataTemplate : ITemplate { + /// + /// Gets a value indicating whether the data template supports recycling of the generated + /// control. + /// + bool SupportsRecycling { get; } + /// /// Checks to see if this data template matches the specified data. /// diff --git a/src/Avalonia.Controls/Templates/ITemplate`1.cs b/src/Avalonia.Controls/Templates/ITemplate`1.cs index 79c6ae47ba..2bf4b5a285 100644 --- a/src/Avalonia.Controls/Templates/ITemplate`1.cs +++ b/src/Avalonia.Controls/Templates/ITemplate`1.cs @@ -1,13 +1,15 @@ // 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.Styling; + namespace Avalonia.Controls { /// /// Creates a control. /// /// The type of control. - public interface ITemplate where TControl : IControl + public interface ITemplate : ITemplate where TControl : IControl { /// /// Creates the control. @@ -15,6 +17,6 @@ namespace Avalonia.Controls /// /// The created control. /// - TControl Build(); + new TControl Build(); } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 1e05c23e5a..ad95b0269a 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils public static int Count(this IEnumerable items) { - Contract.Requires(items != null); - - var collection = items as ICollection; - - if (collection != null) + if (items != null) { - return collection.Count; + var collection = items as ICollection; + + if (collection != null) + { + return collection.Count; + } + else + { + return Enumerable.Count(items.Cast()); + } } else { - return Enumerable.Count(items.Cast()); + return 0; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs new file mode 100644 index 0000000000..46a17d601f --- /dev/null +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -0,0 +1,229 @@ +// 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.Specialized; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel + { + private Size _availableSpace; + private double _takenSpace; + private int _canBeRemoved; + private double _averageItemSize; + private int _averageCount; + private double _pixelOffset; + + bool IVirtualizingPanel.IsFull + { + get + { + return Orientation == Orientation.Horizontal ? + _takenSpace >= _availableSpace.Width : + _takenSpace >= _availableSpace.Height; + } + } + + IVirtualizingController IVirtualizingPanel.Controller { get; set; } + int IVirtualizingPanel.OverflowCount => _canBeRemoved; + Orientation IVirtualizingPanel.ScrollDirection => Orientation; + double IVirtualizingPanel.AverageItemSize => _averageItemSize; + + double IVirtualizingPanel.PixelOverflow + { + get + { + var bounds = Orientation == Orientation.Horizontal ? + _availableSpace.Width : _availableSpace.Height; + return Math.Max(0, _takenSpace - bounds); + } + } + + double IVirtualizingPanel.PixelOffset + { + get { return _pixelOffset; } + + set + { + if (_pixelOffset != value) + { + _pixelOffset = value; + InvalidateArrange(); + } + } + } + + private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + + protected override Size MeasureOverride(Size availableSize) + { + if (availableSize != ((ILayoutable)this).PreviousMeasure) + { + // TODO: We need to put a reasonable limit on this, probably based on the max + // window size. + _availableSpace = availableSize; + Controller?.UpdateControls(); + } + + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + _availableSpace = finalSize; + _canBeRemoved = 0; + _takenSpace = 0; + _averageItemSize = 0; + _averageCount = 0; + var result = base.ArrangeOverride(finalSize); + _takenSpace += _pixelOffset; + Controller?.UpdateControls(); + return result; + } + + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (IControl control in e.NewItems) + { + UpdateAdd(control); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (IControl control in e.OldItems) + { + UpdateRemove(control); + } + + break; + } + } + + protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) + { + var logicalScrollable = Parent as ILogicalScrollable; + var fromControl = from as IControl; + + if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) + { + return logicalScrollable.GetControlInDirection(direction, fromControl); + } + else + { + return base.GetControlInDirection(direction, from); + } + } + + internal override void ArrangeChild( + IControl child, + Rect rect, + Size panelSize, + Orientation orientation) + { + if (orientation == Orientation.Vertical) + { + rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.Y >= _availableSpace.Height) + { + ++_canBeRemoved; + } + + if (rect.Bottom >= _takenSpace) + { + _takenSpace = rect.Bottom; + } + + AddToAverageItemSize(rect.Height); + } + else + { + rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.X >= _availableSpace.Width) + { + ++_canBeRemoved; + } + + if (rect.Right >= _takenSpace) + { + _takenSpace = rect.Right; + } + + AddToAverageItemSize(rect.Width); + } + } + + private void UpdateAdd(IControl child) + { + var bounds = Bounds; + var gap = Gap; + + child.Measure(_availableSpace); + ++_averageCount; + + if (Orientation == Orientation.Vertical) + { + var height = child.DesiredSize.Height; + _takenSpace += height + gap; + AddToAverageItemSize(height); + } + else + { + var width = child.DesiredSize.Width; + _takenSpace += width + gap; + AddToAverageItemSize(width); + } + } + + private void UpdateRemove(IControl child) + { + var bounds = Bounds; + var gap = Gap; + + if (Orientation == Orientation.Vertical) + { + var height = child.DesiredSize.Height; + _takenSpace -= height + gap; + RemoveFromAverageItemSize(height); + } + else + { + var width = child.DesiredSize.Width; + _takenSpace -= width + gap; + RemoveFromAverageItemSize(width); + } + + if (_canBeRemoved > 0) + { + --_canBeRemoved; + } + } + + private void AddToAverageItemSize(double value) + { + ++_averageCount; + _averageItemSize += (value - _averageItemSize) / _averageCount; + } + + private void RemoveFromAverageItemSize(double value) + { + _averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1); + --_averageCount; + } + } +} diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1000e3ef20..36bc87af02 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -89,7 +89,16 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// public Window() - : base(PlatformManager.CreateWindow()) + : this(PlatformManager.CreateWindow()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The window implementation. + public Window(IWindowImpl impl) + : base(impl) { _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; } diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index d0f1e93c56..9d3a5b6536 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -48,35 +48,35 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; } diff --git a/src/Avalonia.DesignerSupport/DesignerApi.cs b/src/Avalonia.DesignerSupport/DesignerApi.cs index 7c17051ce2..cbbfbffec8 100644 --- a/src/Avalonia.DesignerSupport/DesignerApi.cs +++ b/src/Avalonia.DesignerSupport/DesignerApi.cs @@ -7,33 +7,42 @@ using System.Threading.Tasks; namespace Avalonia.DesignerSupport { - class DesignerApi + class DesignerApiDictionary { - private readonly Dictionary _inner; + public Dictionary Dictionary { get; set; } - public DesignerApi(Dictionary inner) + public DesignerApiDictionary(Dictionary dictionary) { - _inner = inner; + Dictionary = dictionary; } - object Get([CallerMemberName] string name = null) + protected object Get([CallerMemberName] string name = null) { object rv; - _inner.TryGetValue(name, out rv); + Dictionary.TryGetValue(name, out rv); return rv; } - void Set(object value, [CallerMemberName] string name = null) + protected void Set(object value, [CallerMemberName] string name = null) { - _inner[name] = value; + Dictionary[name] = value; } + } + class DesignerApi : DesignerApiDictionary + { public Action UpdateXaml { get { return (Action) Get(); } set {Set(value); } } + public Action> UpdateXaml2 + { + get { return (Action>)Get(); } + set { Set(value); } + } + public Action OnResize { get { return (Action) Get(); } @@ -52,5 +61,32 @@ namespace Avalonia.DesignerSupport get { return (Action) Get(); } } + public DesignerApi(Dictionary dictionary) : base(dictionary) + { + } + } + + class DesignerApiXamlFileInfo : DesignerApiDictionary + { + public string Xaml + { + get { return (string)Get(); } + set { Set(value); } + } + + public string AssemblyPath + { + get { return (string) Get(); } + set { Set(value); } + } + + public DesignerApiXamlFileInfo(Dictionary dictionary) : base(dictionary) + { + } + + public DesignerApiXamlFileInfo(): base(new Dictionary()) + { + + } } } diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs index 64aa9106c7..8787144665 100644 --- a/src/Avalonia.DesignerSupport/DesignerAssist.cs +++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs @@ -36,7 +36,7 @@ namespace Avalonia.DesignerSupport public static void Init(Dictionary shared) { Design.IsDesignMode = true; - Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, SetScalingFactor = SetScalingFactor}; + Api = new DesignerApi(shared) {UpdateXaml = UpdateXaml, UpdateXaml2 = UpdateXaml2, SetScalingFactor = SetScalingFactor}; var plat = (IPclPlatformWrapper) Activator.CreateInstance(Assembly.Load(new AssemblyName("Avalonia.Win32")) .DefinedTypes.First(typeof (IPclPlatformWrapper).GetTypeInfo().IsAssignableFrom).AsType()); @@ -58,10 +58,9 @@ namespace Avalonia.DesignerSupport //Ignore, Assembly.DefinedTypes threw an exception, we can't do anything about that } } - AppBuilder.Configure(app == null ? new DesignerApp() : (Application) Activator.CreateInstance(app.AsType())) - .WithWindowingSubsystem(Application.InitializeWin32Subsystem) - .WithRenderingSubsystem(() => { }) + .UseWindowingSubsystem("Avalonia.Win32") + .UseRenderingSubsystem("Avalonia.Direct2D1") .SetupWithoutStarting(); } @@ -74,22 +73,65 @@ namespace Avalonia.DesignerSupport static Window s_currentWindow; - private static void UpdateXaml(string xaml) + private static void UpdateXaml(string xaml) => UpdateXaml2(new DesignerApiXamlFileInfo + { + Xaml = xaml + }.Dictionary); + + private static void UpdateXaml2(Dictionary dic) { + var xamlInfo = new DesignerApiXamlFileInfo(dic); Window window; - Control original; + Control control; using (PlatformManager.DesignerMode()) { var loader = new AvaloniaXamlLoader(); - var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(xamlInfo.Xaml)); - original = (Control)loader.Load(stream); - window = original as Window; + + Uri baseUri = null; + if (xamlInfo.AssemblyPath != null) + { + //Fabricate fake Uri + baseUri = + new Uri("resm:Fake.xaml?assembly=" + Path.GetFileNameWithoutExtension(xamlInfo.AssemblyPath)); + } + + var loaded = loader.Load(stream, null, baseUri); + var styles = loaded as Styles; + if (styles != null) + { + var substitute = Design.GetPreviewWith(styles) ?? + styles.Select(Design.GetPreviewWith).FirstOrDefault(s => s != null); + if (substitute != null) + { + substitute.Styles.AddRange(styles); + control = substitute; + } + else + control = new StackPanel + { + Children = + { + new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, + new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""}, + new TextBlock {Text = "before setters in your first Style"} + } + }; + } + if (loaded is Application) + control = new TextBlock {Text = "Application can't be previewed in design view"}; + else + control = (Control) loaded; + + window = control as Window; if (window == null) { - window = new Window() {Content = original}; + window = new Window() {Content = (Control)control}; } if (!window.IsSet(Window.SizeToContentProperty)) @@ -99,7 +141,7 @@ namespace Avalonia.DesignerSupport s_currentWindow?.Close(); s_currentWindow = window; window.Show(); - Design.ApplyDesignerProperties(window, original); + Design.ApplyDesignerProperties(window, control); Api.OnWindowCreated?.Invoke(window.PlatformImpl.Handle.Handle); Api.OnResize?.Invoke(); } diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index ddaa11b092..b107338aec 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -9,6 +9,8 @@ namespace Avalonia.Diagnostics { public class ViewLocator : IDataTemplate { + public bool SupportsRecycling => false; + public IControl Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index 569e1bb2cc..3722c80def 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,4 +1,4 @@ - + @@ -79,7 +79,7 @@ - + diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs index 14e2144da9..db3a3cf114 100644 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs @@ -25,7 +25,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None); } } \ No newline at end of file diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index 48f85819c0..13d734bd0b 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -14,6 +14,6 @@ namespace Avalonia.Input /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement GetControl(FocusNavigationDirection direction, IInputElement from); + IInputElement GetControl(NavigationDirection direction, IInputElement from); } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index a2321f3493..35ffeca7bb 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -24,14 +24,14 @@ namespace Avalonia.Input public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { Contract.Requires(element != null); - var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element); - var geometry = transformedBounds.GetTransformedBoundsGeometry(); if (element.IsVisible && element.IsHitTestVisible && element.IsEnabledCore) { - if (element.VisualChildren.Any()) + bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)element).Contains(p); + + if ((containsPoint || !element.ClipToBounds) && element.VisualChildren.Any()) { foreach (var child in ZSort(element.VisualChildren.OfType())) { @@ -42,7 +42,7 @@ namespace Avalonia.Input } } - if (geometry.FillContains(p)) + if (containsPoint) { yield return element; } @@ -71,7 +71,6 @@ namespace Avalonia.Input }) .OrderBy(x => x, null) .Select(x => x.Element); - } private class ZOrderElement : IComparable @@ -95,4 +94,4 @@ namespace Avalonia.Input } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 9991f7ec10..57da49fa03 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -48,11 +48,11 @@ namespace Avalonia.Input /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); - if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { return TabNavigation.GetNextInTabOrder(element, direction); } @@ -70,7 +70,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. public void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None) { Contract.Requires(element != null); @@ -79,8 +79,8 @@ namespace Avalonia.Input if (next != null) { - var method = direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous ? + var method = direction == NavigationDirection.Next || + direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; FocusManager.Instance.Focus(next, method, modifiers); } @@ -97,25 +97,37 @@ namespace Avalonia.Input if (current != null) { - FocusNavigationDirection? direction = null; + NavigationDirection? direction = null; switch (e.Key) { case Key.Tab: direction = (e.Modifiers & InputModifiers.Shift) == 0 ? - FocusNavigationDirection.Next : FocusNavigationDirection.Previous; + NavigationDirection.Next : NavigationDirection.Previous; break; case Key.Up: - direction = FocusNavigationDirection.Up; + direction = NavigationDirection.Up; break; case Key.Down: - direction = FocusNavigationDirection.Down; + direction = NavigationDirection.Down; break; case Key.Left: - direction = FocusNavigationDirection.Left; + direction = NavigationDirection.Left; break; case Key.Right: - direction = FocusNavigationDirection.Right; + direction = NavigationDirection.Right; + break; + case Key.PageUp: + direction = NavigationDirection.PageUp; + break; + case Key.PageDown: + direction = NavigationDirection.PageDown; + break; + case Key.Home: + direction = NavigationDirection.First; + break; + case Key.End: + direction = NavigationDirection.Last; break; } diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index 4d6e706516..6d4da5d976 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -24,18 +24,17 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction != FocusNavigationDirection.Next && - direction != FocusNavigationDirection.Previous); + direction != NavigationDirection.Next && + direction != NavigationDirection.Previous); var container = element.GetVisualParent(); if (container != null) { - var isForward = IsForward(direction); var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); switch (mode) @@ -63,12 +62,12 @@ namespace Avalonia.Input.Navigation /// /// The direction. /// True if the direction is forward. - private static bool IsForward(FocusNavigationDirection direction) + private static bool IsForward(NavigationDirection direction) { - return direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Last || - direction == FocusNavigationDirection.Right || - direction == FocusNavigationDirection.Down; + return direction == NavigationDirection.Next || + direction == NavigationDirection.Last || + direction == NavigationDirection.Right || + direction == NavigationDirection.Down; } /// @@ -77,7 +76,7 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { return IsForward(direction) ? GetFocusableDescendents(container).FirstOrDefault() : @@ -121,9 +120,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Down) + if (direction == NavigationDirection.Down) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -156,7 +155,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Up) + if (element != null && direction == NavigationDirection.Up) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -180,7 +179,7 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); var isForward = IsForward(direction); diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 90502cdbf9..bc3826d90e 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNextInTabOrder( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous); + direction == NavigationDirection.Next || + direction == NavigationDirection.Previous); var container = element.GetVisualParent(); @@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { - return direction == FocusNavigationDirection.Next ? + return direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } @@ -128,9 +128,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Next) + if (direction == NavigationDirection.Next) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -165,7 +165,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Previous) + if (element != null && direction == NavigationDirection.Previous) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -189,14 +189,14 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { - if (direction == FocusNavigationDirection.Previous && parent.CanFocus()) + if (direction == NavigationDirection.Previous && parent.CanFocus()) { return parent; } @@ -204,7 +204,7 @@ namespace Avalonia.Input.Navigation var siblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendents); - var sibling = direction == FocusNavigationDirection.Next ? + var sibling = direction == NavigationDirection.Next ? siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : siblings.TakeWhile(x => x != container).LastOrDefault(); @@ -216,7 +216,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(sibling).FirstOrDefault() : GetFocusableDescendents(sibling).LastOrDefault(); } @@ -229,7 +229,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } diff --git a/src/Avalonia.Input/FocusNavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs similarity index 78% rename from src/Avalonia.Input/FocusNavigationDirection.cs rename to src/Avalonia.Input/NavigationDirection.cs index c8f670a13c..fbaa7e74c7 100644 --- a/src/Avalonia.Input/FocusNavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -4,9 +4,9 @@ namespace Avalonia.Input { /// - /// Describes how focus should be moved. + /// Describes how focus should be moved by directional or tab keys. /// - public enum FocusNavigationDirection + public enum NavigationDirection { /// /// Move the focus to the next control in the tab order. @@ -47,5 +47,15 @@ namespace Avalonia.Input /// Move the focus down. /// Down, + + /// + /// Move the focus up a page. + /// + PageUp, + + /// + /// Move the focus down a page. + /// + PageDown, } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 42ed45b0bb..85daff28b9 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Logging; using Avalonia.Threading; @@ -13,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly Queue _toMeasure = new Queue(); - private readonly Queue _toArrange = new Queue(); + private readonly HashSet _toMeasure = new HashSet(); + private readonly HashSet _toArrange = new HashSet(); private bool _queued; private bool _running; @@ -29,8 +30,8 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); + _toMeasure.Add(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -40,7 +41,7 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Enqueue(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -107,7 +108,7 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.Dequeue(); + var next = _toMeasure.First(); Measure(next); } } @@ -116,7 +117,7 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.Dequeue(); + var next = _toArrange.First(); Arrange(next); } } @@ -124,29 +125,45 @@ namespace Avalonia.Layout private void Measure(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Measure(root.MaxClientSize); } - else if (control.PreviousMeasure.HasValue) + else if (parent != null) + { + Measure(parent); + } + + if (!control.IsMeasureValid) { control.Measure(control.PreviousMeasure.Value); } + + _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Arrange(new Rect(root.DesiredSize)); } - else if (control.PreviousArrange.HasValue) + else if (parent != null) + { + Arrange(parent); + } + + if (control.PreviousArrange.HasValue) { control.Arrange(control.PreviousArrange.Value); } + + _toArrange.Remove(control); } private void QueueLayoutPass() diff --git a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs index 5b84b0ddd3..3473c4a094 100644 --- a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs @@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging /// public IBitmapImpl PlatformImpl { - get; } + get; + } /// /// Saves the bitmap to a file. diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs index de4d13890b..10253ec829 100644 --- a/src/Avalonia.SceneGraph/Rect.cs +++ b/src/Avalonia.SceneGraph/Rect.cs @@ -224,14 +224,24 @@ namespace Avalonia } /// - /// Determines whether a points in in the bounds of the rectangle. + /// Determines whether a point in in the bounds of the rectangle. /// /// The point. /// true if the point is in the bounds of the rectangle; otherwise false. public bool Contains(Point p) { - return p.X >= _x && p.X < _x + _width && - p.Y >= _y && p.Y < _y + _height; + return p.X >= _x && p.X <= _x + _width && + p.Y >= _y && p.Y <= _y + _height; + } + + /// + /// Determines whether the rectangle fully contains another rectangle. + /// + /// The rectangle. + /// true if the rectangle is fully contained; otherwise false. + public bool Contains(Rect r) + { + return Contains(r.TopLeft) && Contains(r.BottomRight); } /// diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index 688b0d9ead..0bb5bf63f8 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -104,7 +104,7 @@ namespace Avalonia.Rendering if (visual.RenderTransform != null) { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); renderTransform = (-offset) * visual.RenderTransform.Value * (offset); } @@ -172,7 +172,7 @@ namespace Avalonia.Rendering } else { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); var m = (-offset) * visual.RenderTransform.Value * (offset); return visual.Bounds.TransformToAABB(m); diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs index 82ce1b85c7..fb4cb21a7c 100644 --- a/src/Avalonia.SceneGraph/Visual.cs +++ b/src/Avalonia.SceneGraph/Visual.cs @@ -70,10 +70,10 @@ namespace Avalonia AvaloniaProperty.Register(nameof(RenderTransform)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty TransformOriginProperty = - AvaloniaProperty.Register(nameof(TransformOrigin), defaultValue: RelativePoint.Center); + public static readonly StyledProperty RenderTransformOriginProperty = + AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); /// /// Defines the property. @@ -196,10 +196,10 @@ namespace Avalonia /// /// Gets the transform origin of the scene graph node. /// - public RelativePoint TransformOrigin + public RelativePoint RenderTransformOrigin { - get { return GetValue(TransformOriginProperty); } - set { SetValue(TransformOriginProperty, value); } + get { return GetValue(RenderTransformOriginProperty); } + set { SetValue(RenderTransformOriginProperty, value); } } /// diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs index 9b4a4fccf5..b9b37b1a20 100644 --- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs +++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs @@ -77,9 +77,9 @@ namespace Avalonia.VisualTree Transform RenderTransform { get; set; } /// - /// Gets or sets the transform origin of the scene graph node. + /// Gets or sets the render transform origin of the scene graph node. /// - RelativePoint TransformOrigin { get; set; } + RelativePoint RenderTransformOrigin { get; set; } /// /// Gets the scene graph node's child nodes. diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index da8ed01ccc..4c548669bd 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -38,20 +38,18 @@ namespace Avalonia.VisualTree /// public Matrix Transform { get; } - public Geometry GetTransformedBoundsGeometry() + public bool Contains(Point point) { - StreamGeometry geometry = new StreamGeometry(); - using (var context = geometry.Open()) + if (Transform.HasInverse) { - context.SetFillRule(FillRule.EvenOdd); - context.BeginFigure(Bounds.TopLeft * Transform, true); - context.LineTo(Bounds.TopRight * Transform); - context.LineTo(Bounds.BottomRight * Transform); - context.LineTo(Bounds.BottomLeft * Transform); - context.LineTo(Bounds.TopLeft * Transform); - context.EndFigure(true); + Point trPoint = point * Transform.Invert(); + + return Bounds.Contains(trPoint); + } + else + { + return Bounds.Contains(point); } - return geometry; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index d40627a16e..5117aeb8c3 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -1,4 +1,4 @@ - + @@ -43,12 +43,17 @@ Properties\SharedAssemblyInfo.cs + + + + + @@ -92,6 +97,10 @@ + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Avalonia.Animation + {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Avalonia.Base diff --git a/src/Avalonia.Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs similarity index 100% rename from src/Avalonia.Controls/INameScope.cs rename to src/Avalonia.Styling/Controls/INameScope.cs diff --git a/src/Avalonia.Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs similarity index 93% rename from src/Avalonia.Controls/NameScope.cs rename to src/Avalonia.Styling/Controls/NameScope.cs index aeba4158ef..ddfb3ea173 100644 --- a/src/Avalonia.Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls /// Defines the NameScope attached property. /// public static readonly AttachedProperty NameScopeProperty = - AvaloniaProperty.RegisterAttached("NameScope"); + AvaloniaProperty.RegisterAttached("NameScope"); private readonly Dictionary _inner = new Dictionary(); @@ -34,7 +34,7 @@ namespace Avalonia.Controls /// /// The visual. /// The value of the NameScope attached property. - public static INameScope GetNameScope(Control visual) + public static INameScope GetNameScope(Visual visual) { return visual.GetValue(NameScopeProperty); } @@ -44,7 +44,7 @@ namespace Avalonia.Controls /// /// The visual. /// The value to set. - public static void SetNameScope(Control visual, INameScope value) + public static void SetNameScope(Visual visual, INameScope value) { visual.SetValue(NameScopeProperty, value); } diff --git a/src/Avalonia.Controls/NameScopeEventArgs.cs b/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/NameScopeEventArgs.cs rename to src/Avalonia.Styling/Controls/NameScopeEventArgs.cs diff --git a/src/Avalonia.Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs similarity index 100% rename from src/Avalonia.Controls/NameScopeExtensions.cs rename to src/Avalonia.Styling/Controls/NameScopeExtensions.cs diff --git a/src/Avalonia.Styling/Styling/ITemplate.cs b/src/Avalonia.Styling/Styling/ITemplate.cs new file mode 100644 index 0000000000..1593326e37 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ITemplate.cs @@ -0,0 +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. + +namespace Avalonia.Styling +{ + public interface ITemplate + { + object Build(); + } +} diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 09faf37360..7f696a1f0a 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -4,6 +4,7 @@ using System; using System.Reactive.Disposables; using System.Reactive.Subjects; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Metadata; using Avalonia.Reactive; @@ -19,6 +20,8 @@ namespace Avalonia.Styling /// public class Setter : ISetter { + private object _value; + /// /// Initializes a new instance of the class. /// @@ -54,8 +57,22 @@ namespace Avalonia.Styling [DependsOn(nameof(Property))] public object Value { - get; - set; + get + { + return _value; + } + + set + { + if (value is IStyleable) + { + throw new ArgumentException( + "Cannot assign a control to Style.Value. Wrap the control in a