Browse Source

Merge branch 'master' into pr/553

Conflicts:
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
pull/576/head
Steven Kirk 10 years ago
parent
commit
169ffca591
  1. 40
      Avalonia.sln
  2. 2
      nuget/template/Avalonia.Skia.Desktop.nuspec
  3. 2
      readme.md
  4. 3
      samples/BindingTest/App.xaml.cs
  5. 3
      samples/ControlCatalog.Desktop/Program.cs
  6. 1
      samples/ControlCatalog/Pages/MenuPage.xaml
  7. 3
      samples/TestApplication/Program.cs
  8. 6
      samples/VirtualizationTest/App.config
  9. 6
      samples/VirtualizationTest/App.xaml
  10. 16
      samples/VirtualizationTest/App.xaml.cs
  11. 51
      samples/VirtualizationTest/MainWindow.xaml
  12. 25
      samples/VirtualizationTest/MainWindow.xaml.cs
  13. 34
      samples/VirtualizationTest/Program.cs
  14. 36
      samples/VirtualizationTest/Properties/AssemblyInfo.cs
  15. 22
      samples/VirtualizationTest/ViewModels/ItemViewModel.cs
  16. 141
      samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs
  17. 180
      samples/VirtualizationTest/VirtualizationTest.csproj
  18. 26
      samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject
  19. 10
      samples/VirtualizationTest/packages.config
  20. 3
      samples/XamlTestApplication/Program.cs
  21. 55
      samples/XamlTestApplicationPcl/TestScrollable.cs
  22. 2
      samples/XamlTestApplicationPcl/Views/MainWindow.cs
  23. 1
      samples/XamlTestApplicationPcl/Views/MainWindow.xaml
  24. 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  25. 8
      src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject
  26. 4
      src/Avalonia.Base/AvaloniaObject.cs
  27. 51
      src/Avalonia.Base/Collections/AvaloniaList.cs
  28. 46
      src/Avalonia.Controls/AppBuilder.cs
  29. 51
      src/Avalonia.Controls/Application.cs
  30. 18
      src/Avalonia.Controls/Avalonia.Controls.csproj
  31. 2
      src/Avalonia.Controls/Canvas.cs
  32. 17
      src/Avalonia.Controls/Control.cs
  33. 22
      src/Avalonia.Controls/Design.cs
  34. 38
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  35. 11
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  36. 186
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  37. 34
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  38. 10
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  39. 27
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  40. 6
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  41. 13
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  42. 29
      src/Avalonia.Controls/IScrollable.cs
  43. 22
      src/Avalonia.Controls/ISetInheritanceParent.cs
  44. 22
      src/Avalonia.Controls/IVirtualizingController.cs
  45. 67
      src/Avalonia.Controls/IVirtualizingPanel.cs
  46. 21
      src/Avalonia.Controls/ItemVirtualizationMode.cs
  47. 24
      src/Avalonia.Controls/ItemsControl.cs
  48. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  49. 57
      src/Avalonia.Controls/ListBox.cs
  50. 7
      src/Avalonia.Controls/Menu.cs
  51. 7
      src/Avalonia.Controls/MenuItem.cs
  52. 17
      src/Avalonia.Controls/Mixins/SelectableMixin.cs
  53. 34
      src/Avalonia.Controls/Panel.cs
  54. 12
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  55. 96
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  56. 2
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  57. 198
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  58. 173
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  59. 504
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  60. 178
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  61. 54
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  62. 120
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  63. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  64. 69
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  65. 54
      src/Avalonia.Controls/Primitives/IScrollable.cs
  66. 47
      src/Avalonia.Controls/Primitives/Popup.cs
  67. 25
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  68. 4
      src/Avalonia.Controls/Primitives/TabStrip.cs
  69. 4
      src/Avalonia.Controls/ScrollViewer.cs
  70. 51
      src/Avalonia.Controls/StackPanel.cs
  71. 48
      src/Avalonia.Controls/Templates/DataTemplateExtensions.cs
  72. 39
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  73. 13
      src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
  74. 3
      src/Avalonia.Controls/Templates/FuncTemplate`1.cs
  75. 6
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  76. 6
      src/Avalonia.Controls/Templates/ITemplate`1.cs
  77. 19
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  78. 229
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  79. 11
      src/Avalonia.Controls/Window.cs
  80. 18
      src/Avalonia.Controls/WrapPanel.cs
  81. 52
      src/Avalonia.DesignerSupport/DesignerApi.cs
  82. 64
      src/Avalonia.DesignerSupport/DesignerAssist.cs
  83. 2
      src/Avalonia.Diagnostics/ViewLocator.cs
  84. 4
      src/Avalonia.Input/Avalonia.Input.csproj
  85. 2
      src/Avalonia.Input/IKeyboardNavigationHandler.cs
  86. 2
      src/Avalonia.Input/INavigableContainer.cs
  87. 11
      src/Avalonia.Input/InputExtensions.cs
  88. 34
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  89. 27
      src/Avalonia.Input/Navigation/DirectionalNavigation.cs
  90. 26
      src/Avalonia.Input/Navigation/TabNavigation.cs
  91. 14
      src/Avalonia.Input/NavigationDirection.cs
  92. 35
      src/Avalonia.Layout/LayoutManager.cs
  93. 3
      src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs
  94. 16
      src/Avalonia.SceneGraph/Rect.cs
  95. 4
      src/Avalonia.SceneGraph/Rendering/RendererMixin.cs
  96. 12
      src/Avalonia.SceneGraph/Visual.cs
  97. 4
      src/Avalonia.SceneGraph/VisualTree/IVisual.cs
  98. 22
      src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs
  99. 11
      src/Avalonia.Styling/Avalonia.Styling.csproj
  100. 0
      src/Avalonia.Styling/Controls/INameScope.cs

40
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

2
nuget/template/Avalonia.Skia.Desktop.nuspec

@ -13,7 +13,7 @@
<copyright>Copyright 2015</copyright>
<tags>Avalonia</tags>
<dependencies>
<dependency id="SkiaSharp" version="1.49.2.1-beta"/>
<dependency id="SkiaSharp" version="1.49.3.0-beta"/>
<dependency id="Avalonia" version="#VERSION#" />
</dependencies>
</metadata>

2
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/.

3
samples/BindingTest/App.xaml.cs

@ -19,8 +19,7 @@ namespace BindingTest
InitializeLogging();
AppBuilder.Configure<App>()
.UseWin32()
.UseDirect2D1()
.UsePlatformDetect()
.Start<MainWindow>();
}

3
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<App>()
.UseWin32()
.UseDirect2D1()
.UsePlatformDetect()
.Start<MainWindow>();
}

1
samples/ControlCatalog/Pages/MenuPage.xaml

@ -10,6 +10,7 @@
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/>
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>

3
samples/TestApplication/Program.cs

@ -35,8 +35,7 @@ namespace TestApplication
var app = new App();
AppBuilder.Configure(app)
.UseWin32()
.UseDirect2D1()
.UsePlatformDetect()
.SetupWithoutStarting();
app.Run();

6
samples/VirtualizationTest/App.config

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
</startup>
</configuration>

6
samples/VirtualizationTest/App.xaml

@ -0,0 +1,6 @@
<Application xmlns="https://github.com/avaloniaui">
<Application.Styles>
<StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
</Application.Styles>
</Application>

16
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);
}
}
}

51
samples/VirtualizationTest/MainWindow.xaml

@ -0,0 +1,51 @@
<Window xmlns="https://github.com/avaloniaui"
Title="Avalonia Virtualization Test">
<DockPanel LastChildFill="True" Margin="16">
<StackPanel DockPanel.Dock="Right"
Margin="16 0 0 0"
MinWidth="150"
Gap="4">
<DropDown Items="{Binding VirtualizationModes}"
SelectedItem="{Binding VirtualizationMode}"/>
<DropDown Items="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count"
UseFloatingWatermark="True"
Text="{Binding ItemCount}"/>
<TextBox Watermark="Extent"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Extent, Mode=OneWay}"/>
<TextBox Watermark="Offset"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Offset, Mode=OneWay}"/>
<TextBox Watermark="Viewport"
UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
<TextBox Watermark="Item to Create"
UseFloatingWatermark="True"
Text="{Binding NewItemString}"/>
<Button Command="{Binding AddItemCommand}">Add Item</Button>
<Button Command="{Binding RemoveItemCommand}">Remove Item</Button>
<Button Command="{Binding RecreateCommand}">Recreate</Button>
<Button Command="{Binding SelectFirstCommand}">Select First</Button>
<Button Command="{Binding SelectLastCommand}">Select Last</Button>
</StackPanel>
<ListBox Name="listBox"
Items="{Binding Items}"
SelectedItems="{Binding SelectedItems}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="{Binding Orientation}"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>

25
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);
}
}
}

34
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<App>()
.UseWin32()
.UseDirect2D1()
.Start<MainWindow>();
}
private static void InitializeLogging()
{
#if DEBUG
SerilogLogger.Initialize(new LoggerConfiguration()
.MinimumLevel.Warning()
.WriteTo.Trace(outputTemplate: "{Area}: {Message}")
.CreateLogger());
#endif
}
}
}

36
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")]

22
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}";
}
}

141
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<ItemViewModel> _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<ItemViewModel> SelectedItems { get; }
= new AvaloniaList<ItemViewModel>();
public IReactiveList<ItemViewModel> 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<Orientation> Orientations =>
Enum.GetValues(typeof(Orientation)).Cast<Orientation>();
public ItemVirtualizationMode VirtualizationMode
{
get { return _virtualizationMode; }
set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); }
}
public IEnumerable<ItemVirtualizationMode> VirtualizationModes =>
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast<ItemVirtualizationMode>();
public ReactiveCommand<object> AddItemCommand { get; private set; }
public ReactiveCommand<object> RecreateCommand { get; private set; }
public ReactiveCommand<object> RemoveItemCommand { get; private set; }
public ReactiveCommand<object> SelectFirstCommand { get; private set; }
public ReactiveCommand<object> 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<ItemViewModel>(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<ItemViewModel>(items);
}
private void SelectItem(int index)
{
SelectedItems.Clear();
SelectedItems.Add(Items[index]);
}
}
}

180
samples/VirtualizationTest/VirtualizationTest.csproj

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FBCAF3D0-2808-4934-8E96-3F607594517B}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>VirtualizationTest</RootNamespace>
<AssemblyName>VirtualizationTest</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewModels\ItemViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj">
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
<Name>Avalonia.Animation</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj">
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj">
<Project>{799a7bb5-3c2c-48b6-85a7-406a12c420da}</Project>
<Name>Avalonia.DesignerSupport</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj">
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project>
<Name>Avalonia.Diagnostics</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj">
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
<Name>Avalonia.Input</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj">
<Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project>
<Name>Avalonia.Interactivity</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj">
<Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project>
<Name>Avalonia.Layout</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Logging.Serilog\Avalonia.Logging.Serilog.csproj">
<Project>{B61B66A3-B82D-4875-8001-89D3394FE0C9}</Project>
<Name>Avalonia.Logging.Serilog</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj">
<Project>{6417b24e-49c2-4985-8db2-3ab9d898ec91}</Project>
<Name>Avalonia.ReactiveUI</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.SceneGraph\Avalonia.SceneGraph.csproj">
<Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project>
<Name>Avalonia.SceneGraph</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Avalonia.Styling</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj">
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project>
<Name>Avalonia.Themes.Default</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj">
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Avalonia.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Avalonia.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj">
<Project>{3e908f67-5543-4879-a1dc-08eace79b3cd}</Project>
<Name>Avalonia.Direct2D1</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj">
<Project>{811a76cf-1cf6-440f-963b-bbe31bd72a82}</Project>
<Name>Avalonia.Win32</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="App.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="MainWindow.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

26
samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject

@ -0,0 +1,26 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

10
samples/VirtualizationTest/packages.config

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net452" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net452" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net452" />
<package id="Rx-Main" version="2.2.5" targetFramework="net452" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net452" />
<package id="Serilog" version="1.5.14" targetFramework="net452" />
<package id="Splat" version="1.6.2" targetFramework="net452" />
</packages>

3
samples/XamlTestApplication/Program.cs

@ -21,8 +21,7 @@ namespace XamlTestApplication
InitializeLogging();
AppBuilder.Configure<XamlTestApp>()
.UseWin32()
.UseDirect2D1()
.UsePlatformDetect()
.Start<Views.MainWindow>();
}

55
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;
}
}
}
}
}

2
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;

1
samples/XamlTestApplicationPcl/Views/MainWindow.xaml

@ -24,6 +24,7 @@
<TabControl.Transition>
<PageSlide Duration="0.25" />
</TabControl.Transition>
<TabItem Header="Buttons">
<ScrollViewer CanScrollHorizontally="False">
<StackPanel Margin="10" Gap="4">

1
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -38,7 +38,6 @@ namespace Avalonia.Android
.Bind<IWindowingPlatform>().ToConstant(this);
SkiaPlatform.Initialize();
Application.RegisterPlatformCallback(() => { });
_scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;

8
src/Android/Avalonia.Android/Avalonia.Android.v2.ncrunchproject

@ -7,7 +7,7 @@
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
@ -17,9 +17,9 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>

4
src/Avalonia.Base/AvaloniaObject.cs

@ -26,7 +26,7 @@ namespace Avalonia
/// <summary>
/// The parent object that inherited values are inherited from.
/// </summary>
private AvaloniaObject _inheritanceParent;
private IAvaloniaObject _inheritanceParent;
/// <summary>
/// The set values/bindings on this object.
@ -120,7 +120,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected AvaloniaObject InheritanceParent
protected IAvaloniaObject InheritanceParent
{
get
{

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

@ -319,6 +319,57 @@ namespace Avalonia.Collections
}
}
/// <summary>
/// Moves an item to a new index.
/// </summary>
/// <param name="oldIndex">The index of the item to move.</param>
/// <param name="newIndex">The index to move the item to.</param>
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);
}
}
/// <summary>
/// Moves multiple items to a new index.
/// </summary>
/// <param name="oldIndex">The first index of the items to move.</param>
/// <param name="count">The number of items to move.</param>
/// <param name="newIndex">The index to move the items to.</param>
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);
}
}
/// <summary>
/// Removes an item from the collection.
/// </summary>

46
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
/// </summary>
/// <param name="initializer">The method to call to initialize the windowing subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder WithWindowingSubsystem(Action initializer)
public AppBuilder UseWindowingSubsystem(Action initializer)
{
WindowingSubsystem = initializer;
return this;
}
/// <summary>
/// Specifies a windowing subsystem to use.
/// </summary>
/// <param name="dll">The dll in which to look for subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll));
/// <summary>
/// Specifies a rendering subsystem to use.
/// </summary>
/// <param name="initializer">The method to call to initialize the rendering subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder WithRenderingSubsystem(Action initializer)
public AppBuilder UseRenderingSubsystem(Action initializer)
{
RenderingSubsystem = initializer;
return this;
}
/// <summary>
/// Specifies a rendering subsystem to use.
/// </summary>
/// <param name="dll">The dll in which to look for subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
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;
}
/// <summary>
/// Sets up the platform-speciic services for the <see cref="Application"/>.
/// </summary>

51
src/Avalonia.Controls/Application.cs

@ -26,16 +26,12 @@ namespace Avalonia
/// - A global set of <see cref="Styles"/>.
/// - A <see cref="FocusManager"/>.
/// - An <see cref="InputManager"/>.
/// - Loads and initializes rendering and windowing subsystems with
/// <see cref="InitializeSubsystems(int)"/> and <see cref="InitializeSubsystem(string)"/>.
/// - Registers services needed by the rest of Avalonia in the <see cref="RegisterServices"/>
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle
{
static Action _platformInitializationCallback;
/// <summary>
/// The application-global data templates.
/// </summary>
@ -121,11 +117,6 @@ namespace Avalonia
/// </summary>
IStyleHost IStyleHost.StylingParent => null;
public static void RegisterPlatformCallback(Action cb)
{
_platformInitializationCallback = cb;
}
/// <summary>
/// Initializes the application by loading XAML etc.
/// </summary>
@ -189,47 +180,5 @@ namespace Avalonia
.Bind<IRenderQueueManager>().ToTransient<RenderQueueManager>()
.Bind<IApplicationLifecycle>().ToConstant(this);
}
/// <summary>
/// Initializes the rendering and windowing subsystems according to platform.
/// </summary>
/// <param name="platformID">The value of Environment.OSVersion.Platform.</param>
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");
}
}
/// <summary>
/// Initializes the rendering or windowing subsystem defined by the specified assemblt.
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
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");
}
}
}

18
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -50,26 +50,31 @@
<Compile Include="Design.cs" />
<Compile Include="DockPanel.cs" />
<Compile Include="Expander.cs" />
<Compile Include="Generators\ItemContainer.cs" />
<Compile Include="Generators\ItemContainerInfo.cs" />
<Compile Include="Generators\MenuItemContainerGenerator.cs" />
<Compile Include="Generators\TreeContainerIndex.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="IApplicationLifecycle.cs" />
<Compile Include="INameScope.cs" />
<Compile Include="IScrollable.cs" />
<Compile Include="IPseudoClasses.cs" />
<Compile Include="DropDownItem.cs" />
<Compile Include="ISetInheritanceParent.cs" />
<Compile Include="ItemVirtualizationMode.cs" />
<Compile Include="IVirtualizingController.cs" />
<Compile Include="IVirtualizingPanel.cs" />
<Compile Include="LayoutTransformControl.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" />
<Compile Include="NameScopeEventArgs.cs" />
<Compile Include="NameScopeExtensions.cs" />
<Compile Include="Platform\ITopLevelRenderer.cs" />
<Compile Include="Platform\IWindowingPlatform.cs" />
<Compile Include="Platform\PlatformManager.cs" />
<Compile Include="Presenters\IContentPresenterHost.cs" />
<Compile Include="Presenters\IItemsPresenterHost.cs" />
<Compile Include="Presenters\ItemsPresenterBase.cs" />
<Compile Include="Presenters\ItemVirtualizerNone.cs" />
<Compile Include="Presenters\ItemVirtualizerSimple.cs" />
<Compile Include="Presenters\ItemVirtualizer.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.cs" />
<Compile Include="Primitives\ILogicalScrollable.cs" />
<Compile Include="Primitives\TabStripItem.cs" />
<Compile Include="Primitives\TemplateAppliedEventArgs.cs" />
<Compile Include="SelectionChangedEventArgs.cs" />
@ -170,6 +175,7 @@
<Compile Include="TopLevel.cs" />
<Compile Include="Primitives\PopupRoot.cs" />
<Compile Include="Utils\UndoRedoHelper.cs" />
<Compile Include="VirtualizingStackPanel.cs" />
<Compile Include="WindowState.cs" />
<Compile Include="Window.cs" />
<Compile Include="RowDefinition.cs" />

2
src/Avalonia.Controls/Canvas.cs

@ -136,7 +136,7 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
{
// TODO: Implement this
return null;

17
src/Avalonia.Controls/Control.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
/// - Implements <see cref="IStyleable"/> to allow styling to work on the control.
/// - Implements <see cref="ILogical"/> to form part of a logical tree.
/// </remarks>
public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize
public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize
{
/// <summary>
/// Defines the <see cref="DataContext"/> 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
}
}
/// <summary>
/// Sets the control's inheritance parent.
/// </summary>
/// <param name="parent">The parent.</param>
void ISetInheritanceParent.SetParent(IAvaloniaObject parent)
{
InheritanceParent = parent;
}
/// <summary>
/// Adds a pseudo-class to be set when a property is true.
/// </summary>

22
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<object, Control> Substitutes = new ConditionalWeakTable<object, Control>();
public static readonly AttachedProperty<Control> PreviewWithProperty = AvaloniaProperty
.RegisterAttached<AvaloniaObject, Control>("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))

38
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
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<ItemContainer> Containers { get; }
IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the data template used to display the items in the control.
@ -34,28 +33,33 @@ namespace Avalonia.Controls.Generators
event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Creates container controls for a collection of items.
/// Event raised whenever containers are recycled.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary>
/// Creates a container control for an item.
/// </summary>
/// <param name="index">
/// The index of the item of data in the control's items.
/// </param>
/// <param name="items">The items.</param>
/// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector);
/// <summary>
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
@ -69,17 +73,23 @@ namespace Avalonia.Controls.Generators
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item of the data in the containing collection.
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count);
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<ItemContainer> Clear();
IEnumerable<ItemContainerInfo> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.

11
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="container">The container.</param>
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
/// <param name="containers">The containers.</param>
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainer> containers)
IList<ItemContainerInfo> containers)
{
StartingIndex = startingIndex;
Containers = containers;
@ -41,7 +38,7 @@ namespace Avalonia.Controls.Generators
/// <summary>
/// Gets the containers.
/// </summary>
public IList<ItemContainer> Containers { get; }
public IList<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets the index of the first container in the source items.

186
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
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private List<ItemContainer> _containers = new List<ItemContainer>();
private Dictionary<int, ItemContainerInfo> _containers = new Dictionary<int, ItemContainerInfo>();
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public IEnumerable<ItemContainer> Containers => _containers.Where(x => x != null);
public IEnumerable<ItemContainerInfo> Containers => _containers.Values;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Materialized;
@ -37,6 +37,9 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Recycled;
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
@ -48,41 +51,29 @@ namespace Avalonia.Controls.Generators
public IControl Owner { get; }
/// <inheritdoc/>
public IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
public ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector)
{
Contract.Requires<ArgumentNullException>(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<ItemContainer>();
_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;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
var result = new List<ItemContainer>();
var result = new List<ItemContainerInfo>();
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
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
_containers.InsertRange(index, Enumerable.Repeat<ItemContainer>(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;
}
}
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
List<ItemContainer> result = new List<ItemContainer>();
var result = new List<ItemContainerInfo>();
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
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> Clear()
public virtual bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
return false;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()
{
var result = _containers.Where(x => x != null).ToList();
_containers = new List<ItemContainer>();
var result = Containers.ToList();
_containers.Clear();
if (result.Count > 0)
{
@ -128,27 +158,20 @@ namespace Avalonia.Controls.Generators
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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
/// <returns>The created container control.</returns>
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;
}
/// <summary>
/// Adds a collection of containers to the index.
/// Moves a container.
/// </summary>
/// <param name="containers">The containers.</param>
protected void AddContainers(IList<ItemContainer> containers)
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param>
/// <returns>The container info.</returns>
protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item)
{
Contract.Requires<ArgumentNullException>(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;
}
/// <summary>
@ -207,9 +226,18 @@ namespace Avalonia.Controls.Generators
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<ItemContainer> GetContainerRange(int index, int count)
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{
return _containers.Where(x => x.Key >= index && x.Key <= index + count).Select(x => x.Value);
}
/// <summary>
/// Raises the <see cref="Recycled"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseRecycled(ItemContainerEventArgs e)
{
return _containers.GetRange(index, count);
Recycled?.Invoke(this, e);
}
}
}

34
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;
}
}
/// <inheritdoc/>
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;
}
}
}

10
src/Avalonia.Controls/Generators/ItemContainer.cs → src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainer
public class ItemContainerInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainer"/> class.
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
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
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; }
public object Item { get; internal set; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; }
public int Index { get; internal set; }
}
}

27
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<MenuItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public MenuItemContainerGenerator(IControl owner)
: base(owner, MenuItem.HeaderProperty, null)
{
}
/// <inheritdoc/>
protected override IControl CreateContainer(object item)
{
var separator = item as Separator;
return separator != null ? separator : base.CreateContainer(item);
}
}
}

6
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)));
}
/// <summary>
@ -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)));
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="containers">The item containers.</param>
public void Remove(IEnumerable<ItemContainer> containers)
public void Remove(IEnumerable<ItemContainerInfo> containers)
{
foreach (var container in containers)
{

13
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<ItemContainer> Clear()
public override IEnumerable<ItemContainerInfo> Clear()
{
var items = base.Clear();
Index.Remove(items);
return items;
}
public override IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index.Remove(GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public override IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
public override IEnumerable<ItemContainerInfo> 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;
}
/// <summary>
/// Gets the data template for the specified item.
/// </summary>

29
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
{
/// <summary>
/// Interface implemented by scrollable controls.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
}
}

22
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
{
/// <summary>
/// Defines an interface through which a <see cref="Control"/>'s inheritance parent can be set.
/// </summary>
/// <remarks>
/// You should not usually need to use this interface - it is for advanced scenarios only.
/// Additionally, <see cref="ISetLogicalParent"/> also sets the inheritance parent; this
/// interface is only needed where the logical and inheritance parents differ.
/// </remarks>
public interface ISetInheritanceParent
{
/// <summary>
/// Sets the control's inheritance parent.
/// </summary>
/// <param name="parent">The parent.</param>
void SetParent(IAvaloniaObject parent);
}
}

22
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
{
/// <summary>
/// Interface implemented by controls that act as controllers for an
/// <see cref="IVirtualizingPanel"/>.
/// </summary>
public interface IVirtualizingController
{
/// <summary>
/// Called when the <see cref="IVirtualizingPanel"/>'s controls should be updated.
/// </summary>
/// <remarks>
/// The controller should respond to this method being called by either adding
/// children up until <see cref="IVirtualizingPanel.IsFull"/> becomes true or
/// removing <see cref="IVirtualizingPanel.OverflowCount"/> controls.
/// </remarks>
void UpdateControls();
}
}

67
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
{
/// <summary>
/// A panel that can be used to virtualize items.
/// </summary>
public interface IVirtualizingPanel : IPanel
{
/// <summary>
/// Gets or sets the controller for the virtualizing panel.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
IVirtualizingController Controller { get; set; }
/// <summary>
/// Gets a value indicating whether the panel is full.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
bool IsFull { get; }
/// <summary>
/// Gets the number of items that can be removed while keeping the panel full.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
int OverflowCount { get; }
/// <summary>
/// Gets the direction of scroll.
/// </summary>
Orientation ScrollDirection { get; }
/// <summary>
/// Gets the average size of the materialized items in the direction of scroll.
/// </summary>
double AverageItemSize { get; }
/// <summary>
/// Gets or sets a size in pixels by which the content is overflowing the panel, in the
/// direction of scroll.
/// </summary>
/// <remarks>
/// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
/// overflows the panel bounds.
/// </remarks>
double PixelOverflow { get; }
/// <summary>
/// Gets or sets the current pixel offset of the items in the direction of scroll.
/// </summary>
double PixelOffset { get; set; }
}
}

21
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
{
/// <summary>
/// Describes the item virtualization method to use for a list.
/// </summary>
public enum ItemVirtualizationMode
{
/// <summary>
/// Do not virtualize items.
/// </summary>
None,
/// <summary>
/// Virtualize items without smooth scrolling.
/// </summary>
Simple,
}
}

24
src/Avalonia.Controls/ItemsControl.cs

@ -25,7 +25,6 @@ namespace Avalonia.Controls
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
/// </summary>
[SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")]
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => 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);
}
/// <summary>
/// Called when containers are recycled for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
{
var toRemove = new List<ILogical>();
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);
}
/// <inheritdoc/>
protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{

2
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();

57
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
/// </summary>
public class ListBox : SelectingItemsControl
{
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
/// <summary>
/// Defines the <see cref="Scroll"/> property.
/// </summary>
public static readonly DirectProperty<ListBox, IScrollable> ScrollProperty =
AvaloniaProperty.RegisterDirect<ListBox, IScrollable>(nameof(Scroll), o => o.Scroll);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
@ -28,6 +39,31 @@ namespace Avalonia.Controls
public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty =
SelectingItemsControl.SelectionModeProperty;
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly AvaloniaProperty<ItemVirtualizationMode> VirtualizationModeProperty =
ItemsPresenter.VirtualizationModeProperty.AddOwner<ListBox>();
private IScrollable _scroll;
/// <summary>
/// Initializes static members of the <see cref="ItemsControl"/> class.
/// </summary>
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue<ListBox>(DefaultPanel);
}
/// <summary>
/// Gets the scroll information for the <see cref="ListBox"/>.
/// </summary>
public IScrollable Scroll
{
get { return _scroll; }
private set { SetAndRaise(ScrollProperty, ref _scroll, value); }
}
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
@ -38,6 +74,15 @@ namespace Avalonia.Controls
set { base.SelectionMode = value; }
}
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
public ItemVirtualizationMode VirtualizationMode
{
get { return GetValue(VirtualizationModeProperty); }
set { SetValue(VirtualizationModeProperty, value); }
}
/// <inheritdoc/>
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<IScrollable>("PART_ScrollViewer");
}
}
}

7
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();
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <summary>
/// Called when a key is pressed within the menu.
/// </summary>

7
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;
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
/// <summary>
/// Called when a key is pressed in the <see cref="MenuItem"/>.
/// </summary>

17
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
{

34
src/Avalonia.Controls/Panel.cs

@ -79,12 +79,28 @@ namespace Avalonia.Controls
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
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);
}
/// <summary>
/// Called when the <see cref="Children"/> collection changes.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
List<Control> controls;
@ -122,21 +138,5 @@ namespace Avalonia.Controls
InvalidateMeasure();
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
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);
}
}
}

12
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters
}
/// <inheritdoc/>
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<object>().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;

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

@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
private IDataTemplate _dataTemplate;
/// <summary>
/// Initializes static members of the <see cref="ContentPresenter"/> class.
@ -95,10 +96,6 @@ namespace Avalonia.Controls.Presenters
/// </summary>
public ContentPresenter()
{
var dataContext = this.GetObservable(ContentProperty)
.Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x);
Bind(Control.DataContextProperty, dataContext);
}
/// <summary>
@ -200,6 +197,13 @@ namespace Avalonia.Controls.Presenters
}
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_dataTemplate = null;
}
/// <summary>
/// Updates the <see cref="Child"/> control based on the control's <see cref="Content"/>.
/// </summary>
@ -213,34 +217,86 @@ namespace Avalonia.Controls.Presenters
/// </remarks>
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;

2
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);
}
}

198
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
{
/// <summary>
/// Base class for classes which handle virtualization for an <see cref="ItemsPresenter"/>.
/// </summary>
internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable
{
private bool disposedValue;
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizer"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizer(ItemsPresenter owner)
{
Owner = owner;
Items = owner.Items;
ItemCount = owner.Items.Count();
}
/// <summary>
/// Gets the <see cref="ItemsPresenter"/> which owns the virtualizer.
/// </summary>
public ItemsPresenter Owner { get; }
/// <summary>
/// Gets the <see cref="IVirtualizingPanel"/> which will host the items.
/// </summary>
public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
/// <summary>
/// Gets the items to display.
/// </summary>
public IEnumerable Items { get; private set; }
/// <summary>
/// Gets the number of items in <see cref="Items"/>.
/// </summary>
public int ItemCount { get; private set; }
/// <summary>
/// Gets or sets the index of the first item displayed in the panel.
/// </summary>
public int FirstIndex { get; protected set; }
/// <summary>
/// Gets or sets the index of the first item beyond those displayed in the panel.
/// </summary>
public int NextIndex { get; protected set; }
/// <summary>
/// Gets a value indicating whether the items should be scroll horizontally or vertically.
/// </summary>
public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical;
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled.
/// </summary>
public abstract bool IsLogicalScrollEnabled { get; }
/// <summary>
/// Gets the value of the scroll extent.
/// </summary>
public abstract double ExtentValue { get; }
/// <summary>
/// Gets or sets the value of the current scroll offset.
/// </summary>
public abstract double OffsetValue { get; set; }
/// <summary>
/// Gets the value of the scrollable viewport.
/// </summary>
public abstract double ViewportValue { get; }
/// <summary>
/// Gets the <see cref="ExtentValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0);
/// <summary>
/// Gets the <see cref="ViewportValue"/> as a <see cref="Size"/>.
/// </summary>
public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0);
/// <summary>
/// Gets or sets the <see cref="OffsetValue"/> as a <see cref="Vector"/>.
/// </summary>
public Vector Offset
{
get
{
return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0);
}
set
{
OffsetValue = Vertical ? value.Y : value.X;
}
}
/// <summary>
/// Creates an <see cref="ItemVirtualizer"/> based on an item presenter's
/// <see cref="ItemVirtualizationMode"/>.
/// </summary>
/// <param name="owner">The items presenter.</param>
/// <returns>An <see cref="ItemVirtualizer"/>.</returns>
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;
}
/// <inheritdoc/>
public virtual void UpdateControls()
{
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from)
{
return null;
}
/// <summary>
/// Called when the items for the presenter change, either because
/// <see cref="ItemsPresenterBase.Items"/> has been set, the items collection has been
/// modified, or the panel has been created.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="e">A description of the change.</param>
public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
{
Items = items;
ItemCount = items.Count();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public virtual void ScrollIntoView(object item)
{
}
/// <inheritdoc/>
public virtual void Dispose()
{
VirtualizingPanel.Controller = null;
VirtualizingPanel.Children.Clear();
Owner.ItemContainerGenerator.Clear();
}
/// <summary>
/// Invalidates the current scroll.
/// </summary>
protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll();
}
}

173
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
{
/// <summary>
/// Represents an item virtualizer for an <see cref="ItemsPresenter"/> that doesn't actually
/// virtualize items - it just creates a container for every item.
/// </summary>
internal class ItemVirtualizerNone : ItemVirtualizer
{
public ItemVirtualizerNone(ItemsPresenter owner)
: base(owner)
{
if (Items != null && owner.Panel != null)
{
AddContainers(0, Items);
}
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => false;
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ExtentValue
{
get { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double OffsetValue
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
/// <summary>
/// This property should never be accessed because <see cref="IsLogicalScrollEnabled"/> is
/// false.
/// </summary>
public override double ViewportValue
{
get { throw new NotSupportedException(); }
}
/// <inheritdoc/>
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();
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
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<ItemContainerInfo> AddContainers(int index, IEnumerable items)
{
var generator = Owner.ItemContainerGenerator;
var result = new List<ItemContainerInfo>();
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<ItemContainerInfo> items)
{
var panel = Owner.Panel;
foreach (var i in items)
{
if (i.ContainerControl != null)
{
panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

504
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
{
/// <summary>
/// Handles virtualization in an <see cref="ItemsPresenter"/> for
/// <see cref="ItemVirtualizationMode.Simple"/>.
/// </summary>
internal class ItemVirtualizerSimple : ItemVirtualizer
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemVirtualizerSimple"/> class.
/// </summary>
/// <param name="owner"></param>
public ItemVirtualizerSimple(ItemsPresenter owner)
: base(owner)
{
// Don't need to add children here as UpdateControls should be called by the panel
// measure/arrange.
}
/// <inheritdoc/>
public override bool IsLogicalScrollEnabled => true;
/// <inheritdoc/>
public override double ExtentValue => ItemCount;
/// <inheritdoc/>
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;
}
}
}
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
public override void UpdateControls()
{
CreateAndRemoveContainers();
InvalidateScroll();
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public override void ScrollIntoView(object item)
{
var index = Items.IndexOf(item);
if (index != -1)
{
ScrollIntoView(index);
}
}
/// <summary>
/// Creates and removes containers such that we have at most enough containers to fill
/// the panel.
/// </summary>
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);
}
}
/// <summary>
/// Updates the containers in the panel to make sure they are displaying the correct item
/// based on <see cref="ItemVirtualizer.FirstIndex"/>.
/// </summary>
/// <remarks>
/// This method requires that <see cref="ItemVirtualizer.FirstIndex"/> + the number of
/// materialized containers is not more than <see cref="ItemVirtualizer.ItemCount"/>.
/// </remarks>
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;
}
}
/// <summary>
/// Recycles containers when a move occurs.
/// </summary>
/// <param name="delta">The delta of the move.</param>
/// <remarks>
/// 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
/// <see cref="ItemVirtualizer.FirstIndex"/> and <see cref="ItemVirtualizer.NextIndex"/>
/// with their new values.
/// </remarks>
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;
}
/// <summary>
/// Recycles containers due to items being removed.
/// </summary>
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();
}
}
/// <summary>
/// Removes the specified number of containers from the end of the panel and updates
/// <see cref="ItemVirtualizer.NextIndex"/>.
/// </summary>
/// <param name="count">The number of containers to remove.</param>
private void RemoveContainers(int count)
{
var index = VirtualizingPanel.Children.Count - count;
VirtualizingPanel.Children.RemoveRange(index, count);
Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count);
NextIndex -= count;
}
/// <summary>
/// Scrolls the item with the specified index into view.
/// </summary>
/// <param name="index">The item index.</param>
/// <returns>The container that was brought into view.</returns>
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;
}
/// <summary>
/// Ensures an offset value is within the value range.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>The coerced value.</returns>
private double CoerceOffset(double value)
{
var max = Math.Max(ExtentValue - ViewportValue, 0);
return MathUtilities.Clamp(value, 0, max);
}
}
}

178
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
{
/// <summary>
/// Displays items inside an <see cref="ItemsControl"/>.
/// </summary>
public class ItemsPresenter : ItemsPresenterBase
public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable
{
/// <summary>
/// Defines the <see cref="VirtualizationMode"/> property.
/// </summary>
public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
AvaloniaProperty.Register<ItemsPresenter, ItemVirtualizationMode>(
nameof(VirtualizationMode),
defaultValue: ItemVirtualizationMode.Simple);
private ItemVirtualizer _virtualizer;
/// <summary>
/// Initializes static members of the <see cref="ItemsPresenter"/> class.
/// </summary>
@ -22,127 +32,101 @@ namespace Avalonia.Controls.Presenters
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
VirtualizationModeProperty.Changed
.AddClassHandler<ItemsPresenter>(x => x.VirtualizationModeChanged);
}
/// <inheritdoc/>
protected override void CreatePanel()
/// <summary>
/// Gets or sets the virtualization mode for the items.
/// </summary>
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); }
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
Size IScrollable.Extent => _virtualizer.Extent;
AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector));
break;
/// <inheritdoc/>
Vector IScrollable.Offset
{
get { return _virtualizer.Offset; }
set { _virtualizer.Offset = CoerceOffset(value); }
}
/// <inheritdoc/>
Size IScrollable.Viewport => _virtualizer.Viewport;
case NotifyCollectionChangedAction.Remove:
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
/// <inheritdoc/>
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);
/// <inheritdoc/>
Size ILogicalScrollable.ScrollSize => new Size(1, 1);
var i = e.NewStartingIndex;
/// <inheritdoc/>
Size ILogicalScrollable.PageScrollSize => new Size(0, 1);
foreach (var container in containers)
{
Panel.Children[i++] = container.ContainerControl;
}
/// <inheritdoc/>
bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect)
{
return false;
}
break;
/// <inheritdoc/>
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));
}
/// <inheritdoc/>
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<ItemContainer> 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<ItemContainer> 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<ItemContainer> 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();
}
}
}

54
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
}
}
/// <inheritdoc/>
public virtual void ScrollIntoView(object item)
{
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// </returns>
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;
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@ -178,11 +202,26 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
/// <summary>
/// Called when the <see cref="Panel"/> is created.
/// </summary>
/// <param name="panel">The panel.</param>
protected virtual void PanelCreated(IPanel panel)
{
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, the items collection has been modified, or the panel has been created.
/// </summary>
/// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary>
/// Creates the <see cref="Panel"/> when <see cref="ApplyTemplate"/> is called for the first
/// time.
/// </summary>
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));
}
/// <summary>
/// Called when the items for the presenter change, either because <see cref="Items"/>
/// has been set, or the items collection has been modified.
/// </summary>
/// <param name="e">A description of the change.</param>
protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e);
/// <summary>
/// Called when the <see cref="Items"/> collection changes.
/// </summary>

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

@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
{
/// <summary>
/// Defines the <see cref="Extent"/> 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;
/// <summary>
@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters
static ScrollContentPresenter()
{
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(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);
}
/// <summary>
@ -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
/// <inheritdoc/>
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;
}
}
}
}
}

2
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

69
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
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
/// <remarks>
/// Controls that implement this interface, when placed inside a <see cref="ScrollViewer"/>
/// 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.
/// </remarks>
public interface ILogicalScrollable : IScrollable
{
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled on the control.
/// </summary>
bool IsLogicalScrollEnabled { get; }
/// <summary>
/// Gets or sets the scroll invalidation method.
/// </summary>
/// <remarks>
/// <para>
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
/// the <see cref="IScrollable.Extent"/>, <see cref="IScrollable.Offset"/> or
/// <see cref="IScrollable.Viewport"/> properties.
/// </para>
/// <para>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="ILogicalScrollable"/> is placed inside it.
/// </para>
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the size to scroll by, in logical units.
/// </summary>
Size ScrollSize { get; }
/// <summary>
/// Gets the size to page by, in logical units.
/// </summary>
Size PageScrollSize { get; }
/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary>
/// <param name="target">The target visual.</param>
/// <param name="targetRect">The portion of the target visual to bring into view.</param>
/// <returns>True if the scroll offset was changed; otherwise false.</returns>
bool BringIntoView(IControl target, Rect targetRect);
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IControl GetControlInDirection(NavigationDirection direction, IControl from);
}
}

54
src/Avalonia.Controls/Primitives/IScrollable.cs

@ -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
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets or sets the scroll invalidation method.
/// </summary>
/// <remarks>
/// <para>
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
/// the <see cref="Extent"/>, <see cref="Offset"/> or <see cref="Viewport"/> properties.
/// </para>
/// <para>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="IScrollable"/> is placed inside it.
/// </para>
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
/// <summary>
/// Gets the size to scroll by, in logical units.
/// </summary>
Size ScrollSize { get; }
/// <summary>
/// Gets the size to page by, in logical units.
/// </summary>
Size PageScrollSize { get; }
}
}

47
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<PlacementMode> PlacementModeProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(HorizontalOffset));
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
@ -122,6 +135,24 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
/// </summary>
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
/// </summary>
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
@ -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");

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

@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
/// <summary>
/// Tries to get the container that was the source of an event.
/// </summary>
@ -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));
}
}
}
/// <inheritdoc/>
protected override void OnDataContextChanging()
{
@ -710,6 +729,12 @@ namespace Avalonia.Controls.Primitives
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
if (AutoScrollToSelectedItem)
{
ScrollIntoView(e.NewItems[0]);
}
added = e.NewItems;
break;

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

@ -9,6 +9,9 @@ namespace Avalonia.Controls.Primitives
{
public class TabStrip : SelectingItemsControl
{
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new WrapPanel { Orientation = Orientation.Horizontal });
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
@ -16,6 +19,7 @@ namespace Avalonia.Controls.Primitives
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected override IItemContainerGenerator CreateItemContainerGenerator()

4
src/Avalonia.Controls/ScrollViewer.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl
public class ScrollViewer : ContentControl, IScrollable
{
/// <summary>
/// Defines the <see cref="CanScrollHorizontally"/> 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)

51
src/Avalonia.Controls/StackPanel.cs

@ -72,37 +72,52 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
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;
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
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
/// <returns>The space taken.</returns>
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);
}
}
}

48
src/Avalonia.Controls/Templates/DataTemplateExtensions.cs

@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates
/// </summary>
public static class DataTemplateExtensions
{
/// <summary>
/// Materializes a piece of data based on a data template.
/// </summary>
/// <param name="control">The control materializing the data template.</param>
/// <param name="data">The data.</param>
/// <param name="primary">
/// An optional primary template that can will be tried before the
/// <see cref="IControl.DataTemplates"/> in the tree are searched.
/// </param>
/// <returns>The data materialized as a control.</returns>
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;
}
}
}
/// <summary>
/// Find a data template that matches a piece of data.
/// </summary>

39
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<object, IControl>, IDataTemplate
{
/// <summary>
/// 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.
/// </summary>
public static readonly FuncDataTemplate Default =
new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
new FuncDataTemplate<object>(
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);
/// <summary>
/// The implementation of the <see cref="Match"/> method.
@ -29,8 +46,12 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <paramref name="type"/> returns a control.
/// </param>
public FuncDataTemplate(Type type, Func<object, IControl> build)
: this(o => IsInstance(o, type), build)
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Type type,
Func<object, IControl> build,
bool supportsRecycling = false)
: this(o => IsInstance(o, type), build, supportsRecycling)
{
}
@ -43,14 +64,22 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which returns a control for matching data.
/// </param>
public FuncDataTemplate(Func<object, bool> match, Func<object, IControl> build)
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Func<object, bool> match,
Func<object, IControl> build,
bool supportsRecycling = false)
: base(build)
{
Contract.Requires<ArgumentNullException>(match != null);
_match = match;
SupportsRecycling = supportsRecycling;
}
/// <inheritdoc/>
public bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>

13
src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs

@ -17,8 +17,9 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public FuncDataTemplate(Func<T, IControl> build)
: base(typeof(T), CastBuild(build))
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(Func<T, IControl> build, bool supportsRecycling = false)
: base(typeof(T), CastBuild(build), supportsRecycling)
{
}
@ -31,8 +32,12 @@ namespace Avalonia.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public FuncDataTemplate(Func<T, bool> match, Func<T, IControl> build)
: base(CastMatch(match), CastBuild(build))
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(
Func<T, bool> match,
Func<T, IControl> build,
bool supportsRecycling = false)
: base(CastMatch(match), CastBuild(build), supportsRecycling)
{
}

3
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();
}
}

6
src/Avalonia.Controls/Templates/IDataTemplate.cs

@ -8,6 +8,12 @@ namespace Avalonia.Controls.Templates
/// </summary>
public interface IDataTemplate : ITemplate<object, IControl>
{
/// <summary>
/// Gets a value indicating whether the data template supports recycling of the generated
/// control.
/// </summary>
bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>

6
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
{
/// <summary>
/// Creates a control.
/// </summary>
/// <typeparam name="TControl">The type of control.</typeparam>
public interface ITemplate<TControl> where TControl : IControl
public interface ITemplate<TControl> : ITemplate where TControl : IControl
{
/// <summary>
/// Creates the control.
@ -15,6 +17,6 @@ namespace Avalonia.Controls
/// <returns>
/// The created control.
/// </returns>
TControl Build();
new TControl Build();
}
}

19
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils
public static int Count(this IEnumerable items)
{
Contract.Requires<ArgumentNullException>(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<object>());
}
}
else
{
return Enumerable.Count(items.Cast<object>());
return 0;
}
}

229
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;
}
}
}

11
src/Avalonia.Controls/Window.cs

@ -89,7 +89,16 @@ namespace Avalonia.Controls
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
public Window()
: base(PlatformManager.CreateWindow())
: this(PlatformManager.CreateWindow())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
/// <param name="impl">The window implementation.</param>
public Window(IWindowImpl impl)
: base(impl)
{
_maxPlatformClientSize = this.PlatformImpl.MaxClientSize;
}

18
src/Avalonia.Controls/WrapPanel.cs

@ -48,35 +48,35 @@ namespace Avalonia.Controls
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
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;
}

52
src/Avalonia.DesignerSupport/DesignerApi.cs

@ -7,33 +7,42 @@ using System.Threading.Tasks;
namespace Avalonia.DesignerSupport
{
class DesignerApi
class DesignerApiDictionary
{
private readonly Dictionary<string, object> _inner;
public Dictionary<string, object> Dictionary { get; set; }
public DesignerApi(Dictionary<string, object> inner)
public DesignerApiDictionary(Dictionary<string, object> 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<string> UpdateXaml
{
get { return (Action<string>) Get(); }
set {Set(value); }
}
public Action<Dictionary<string, object>> UpdateXaml2
{
get { return (Action<Dictionary<string, object>>)Get(); }
set { Set(value); }
}
public Action OnResize
{
get { return (Action) Get(); }
@ -52,5 +61,32 @@ namespace Avalonia.DesignerSupport
get { return (Action<double>) Get(); }
}
public DesignerApi(Dictionary<string, object> 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<string, object> dictionary) : base(dictionary)
{
}
public DesignerApiXamlFileInfo(): base(new Dictionary<string, object>())
{
}
}
}

64
src/Avalonia.DesignerSupport/DesignerAssist.cs

@ -36,7 +36,7 @@ namespace Avalonia.DesignerSupport
public static void Init(Dictionary<string, object> 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<string, object> 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 = "<Design.PreviewWith>"},
new TextBlock {Text = " <Border Padding=20><!-- YOUR CONTROL FOR PREVIEW HERE--></Border>"},
new TextBlock {Text = "<Design.PreviewWith>"},
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();
}

2
src/Avalonia.Diagnostics/ViewLocator.cs

@ -9,6 +9,8 @@ namespace Avalonia.Diagnostics
{
public class ViewLocator<TViewModel> : IDataTemplate
{
public bool SupportsRecycling => false;
public IControl Build(object data)
{
var name = data.GetType().FullName.Replace("ViewModel", "View");

4
src/Avalonia.Input/Avalonia.Input.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -79,7 +79,7 @@
<Compile Include="IMainMenu.cs" />
<Compile Include="IAccessKeyHandler.cs" />
<Compile Include="FocusManager.cs" />
<Compile Include="FocusNavigationDirection.cs" />
<Compile Include="NavigationDirection.cs" />
<Compile Include="ICloseable.cs" />
<Compile Include="IFocusManager.cs" />
<Compile Include="IFocusScope.cs" />

2
src/Avalonia.Input/IKeyboardNavigationHandler.cs

@ -25,7 +25,7 @@ namespace Avalonia.Input
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
void Move(
IInputElement element,
FocusNavigationDirection direction,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None);
}
}

2
src/Avalonia.Input/INavigableContainer.cs

@ -14,6 +14,6 @@ namespace Avalonia.Input
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <returns>The control.</returns>
IInputElement GetControl(FocusNavigationDirection direction, IInputElement from);
IInputElement GetControl(NavigationDirection direction, IInputElement from);
}
}

11
src/Avalonia.Input/InputExtensions.cs

@ -24,14 +24,14 @@ namespace Avalonia.Input
public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
{
Contract.Requires<ArgumentNullException>(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<IInputElement>()))
{
@ -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<ZOrderElement>
@ -95,4 +94,4 @@ namespace Avalonia.Input
}
}
}
}
}

34
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -48,11 +48,11 @@ namespace Avalonia.Input
/// </returns>
public static IInputElement GetNext(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(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
/// <param name="modifiers">Any input modifiers active at the time of focus.</param>
public void Move(
IInputElement element,
FocusNavigationDirection direction,
NavigationDirection direction,
InputModifiers modifiers = InputModifiers.None)
{
Contract.Requires<ArgumentNullException>(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;
}

27
src/Avalonia.Input/Navigation/DirectionalNavigation.cs

@ -24,18 +24,17 @@ namespace Avalonia.Input.Navigation
/// </returns>
public static IInputElement GetNext(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction != FocusNavigationDirection.Next &&
direction != FocusNavigationDirection.Previous);
direction != NavigationDirection.Next &&
direction != NavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
if (container != null)
{
var isForward = IsForward(direction);
var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container);
switch (mode)
@ -63,12 +62,12 @@ namespace Avalonia.Input.Navigation
/// </summary>
/// <param name="direction">The direction.</param>
/// <returns>True if the direction is forward.</returns>
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;
}
/// <summary>
@ -77,7 +76,7 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
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
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
var isForward = IsForward(direction);

26
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation
/// </returns>
public static IInputElement GetNextInTabOrder(
IInputElement element,
FocusNavigationDirection direction)
NavigationDirection direction)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction == FocusNavigationDirection.Next ||
direction == FocusNavigationDirection.Previous);
direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous);
var container = element.GetVisualParent<IInputElement>();
@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
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
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
IInputElement container,
FocusNavigationDirection direction)
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
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<IInputElement>()
.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();
}

14
src/Avalonia.Input/FocusNavigationDirection.cs → src/Avalonia.Input/NavigationDirection.cs

@ -4,9 +4,9 @@
namespace Avalonia.Input
{
/// <summary>
/// Describes how focus should be moved.
/// Describes how focus should be moved by directional or tab keys.
/// </summary>
public enum FocusNavigationDirection
public enum NavigationDirection
{
/// <summary>
/// Move the focus to the next control in the tab order.
@ -47,5 +47,15 @@ namespace Avalonia.Input
/// Move the focus down.
/// </summary>
Down,
/// <summary>
/// Move the focus up a page.
/// </summary>
PageUp,
/// <summary>
/// Move the focus down a page.
/// </summary>
PageDown,
}
}

35
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
/// </summary>
public class LayoutManager : ILayoutManager
{
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
private bool _queued;
private bool _running;
@ -29,8 +30,8 @@ namespace Avalonia.Layout
Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(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()

3
src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs

@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging
/// </summary>
public IBitmapImpl PlatformImpl
{
get; }
get;
}
/// <summary>
/// Saves the bitmap to a file.

16
src/Avalonia.SceneGraph/Rect.cs

@ -224,14 +224,24 @@ namespace Avalonia
}
/// <summary>
/// Determines whether a points in in the bounds of the rectangle.
/// Determines whether a point in in the bounds of the rectangle.
/// </summary>
/// <param name="p">The point.</param>
/// <returns>true if the point is in the bounds of the rectangle; otherwise false.</returns>
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;
}
/// <summary>
/// Determines whether the rectangle fully contains another rectangle.
/// </summary>
/// <param name="r">The rectangle.</param>
/// <returns>true if the rectangle is fully contained; otherwise false.</returns>
public bool Contains(Rect r)
{
return Contains(r.TopLeft) && Contains(r.BottomRight);
}
/// <summary>

4
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);

12
src/Avalonia.SceneGraph/Visual.cs

@ -70,10 +70,10 @@ namespace Avalonia
AvaloniaProperty.Register<Visual, Transform>(nameof(RenderTransform));
/// <summary>
/// Defines the <see cref="TransformOrigin"/> property.
/// Defines the <see cref="RenderTransformOrigin"/> property.
/// </summary>
public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(TransformOrigin), defaultValue: RelativePoint.Center);
public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="IVisual.VisualParent"/> property.
@ -196,10 +196,10 @@ namespace Avalonia
/// <summary>
/// Gets the transform origin of the scene graph node.
/// </summary>
public RelativePoint TransformOrigin
public RelativePoint RenderTransformOrigin
{
get { return GetValue(TransformOriginProperty); }
set { SetValue(TransformOriginProperty, value); }
get { return GetValue(RenderTransformOriginProperty); }
set { SetValue(RenderTransformOriginProperty, value); }
}
/// <summary>

4
src/Avalonia.SceneGraph/VisualTree/IVisual.cs

@ -77,9 +77,9 @@ namespace Avalonia.VisualTree
Transform RenderTransform { get; set; }
/// <summary>
/// Gets or sets the transform origin of the scene graph node.
/// Gets or sets the render transform origin of the scene graph node.
/// </summary>
RelativePoint TransformOrigin { get; set; }
RelativePoint RenderTransformOrigin { get; set; }
/// <summary>
/// Gets the scene graph node's child nodes.

22
src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs

@ -38,20 +38,18 @@ namespace Avalonia.VisualTree
/// </summary>
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;
}
}
}
}

11
src/Avalonia.Styling/Avalonia.Styling.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -43,12 +43,17 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Controls\NameScopeEventArgs.cs" />
<Compile Include="Controls\NameScopeExtensions.cs" />
<Compile Include="LogicalTree\ILogical.cs" />
<Compile Include="LogicalTree\LogicalExtensions.cs" />
<Compile Include="LogicalTree\LogicalTreeAttachmentEventArgs.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Styling\ActivatedSubject.cs" />
<Compile Include="Styling\ActivatedValue.cs" />
<Compile Include="Controls\INameScope.cs" />
<Compile Include="Styling\ITemplate.cs" />
<Compile Include="Controls\NameScope.cs" />
<Compile Include="Styling\TemplateSelector.cs" />
<Compile Include="Styling\DescendentSelector.cs" />
<Compile Include="Styling\ChildSelector.cs" />
@ -92,6 +97,10 @@
<None Include="Styling\packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj">
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
<Name>Avalonia.Animation</Name>
</ProjectReference>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj">
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Avalonia.Base</Name>

0
src/Avalonia.Controls/INameScope.cs → src/Avalonia.Styling/Controls/INameScope.cs

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

Loading…
Cancel
Save