diff --git a/.editorconfig b/.editorconfig index 5f08d1e940..f6bce9cb76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +space_within_single_line_array_initializer_braces = true # Wrapping preferences csharp_wrap_before_ternary_opsigns = false # Xaml files [*.xaml] -indent_size = 4 +indent_size = 2 # Xml project files [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f4fbb0d50..7e3532ee23 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: - job: Windows pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' steps: - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index cf8e0fd13a..c03ad0fefd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a99ac4d026..dd2f27116d 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,6 +89,29 @@ partial class Build : NukeBuild } + IReadOnlyCollection MsBuildCommon( + string projectFile, + Configure configurator = null) + { + return MSBuild(projectFile, c => + { + // This is required for VS2019 image on Azure Pipelines + if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) + { + var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); + if (javaSdk != null) + c = c.AddProperty("JavaSdkDirectory", javaSdk); + } + + c = c.AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal); + c = configurator?.Invoke(c) ?? c; + return c; + }); + } Target Clean => _ => _.Executes(() => { DeleteDirectories(Parameters.BuildDirs); @@ -105,13 +128,8 @@ partial class Build : NukeBuild .Executes(() => { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c + MsBuildCommon(Parameters.MSBuildSolution, c => c .SetArgumentConfigurator(a => a.Add("/r")) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) .AddTargets("Build") ); @@ -237,12 +255,7 @@ partial class Build : NukeBuild { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) + MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else DotNetPack(Parameters.MSBuildSolution, c => diff --git a/readme.md b/readme.md index 7a03fda384..ee44a0cc3f 100644 --- a/readme.md +++ b/readme.md @@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop ## Bleeding Edge Builds -Try out the latest build of Avalonia available for download here: -https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts - or use nightly build feeds as described here: https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index d8f0f39977..bc76a39f08 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..de9ca02ed1 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore var builder = BuildAvaloniaApp(); if (args.Contains("--fbdev")) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - return builder.StartLinuxFramebuffer(args); + SilenceConsole(); + return builder.StartLinuxFbDev(args); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args); } else return builder.StartWithClassicDesktopLifetime(args); @@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..5f24c8062e 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -26,6 +26,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 0ca3567970..f90a0c4658 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -37,10 +37,6 @@ - - + + + ItemsRepeater + A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization. + + + + Stack - Vertical + Stack - Horizontal + UniformGrid - Vertical + UniformGrid - Horizontal + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs new file mode 100644 index 0000000000..214de89253 --- /dev/null +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -0,0 +1,71 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ItemsRepeaterPage : UserControl + { + private ItemsRepeater _repeater; + private ScrollViewer _scroller; + + public ItemsRepeaterPage() + { + this.InitializeComponent(); + _repeater = this.FindControl("repeater"); + _scroller = this.FindControl("scroller"); + DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void LayoutChanged(object sender, SelectionChangedEventArgs e) + { + if (_repeater == null) + { + return; + } + + var comboBox = (ComboBox)sender; + + switch (comboBox.SelectedIndex) + { + case 0: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Vertical }; + break; + case 1: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal }; + break; + case 2: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Vertical, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + case 3: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new UniformGridLayout + { + Orientation = Orientation.Horizontal, + MinItemWidth = 200, + MinItemHeight = 200, + }; + break; + } + } + } +} diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml b/samples/ControlCatalog/Pages/TabStripPage.xaml new file mode 100644 index 0000000000..a824336f75 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml @@ -0,0 +1,33 @@ + + + TabStrip + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 0000000000..f0630cf534 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class TabStripPage : UserControl + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new[] + { + new TabStripItemViewModel + { + Header = "Item 1", + }, + new TabStripItemViewModel + { + Header = "Item 2", + }, + new TabStripItemViewModel + { + Header = "Disabled", + IsEnabled = false, + }, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private class TabStripItemViewModel + { + public string Header { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3047b1e519..3513e94107 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -29,8 +29,7 @@ Name="PART_ItemsPresenter" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" - ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> - \ No newline at end of file + diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 80e0fb2586..93fe09a156 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI.Legacy; using ReactiveUI; +using Avalonia.Layout; namespace VirtualizationDemo.ViewModels { diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 9c3d4fb3a1..0089ea3b8d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid44 + monoandroid80 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 9c72321472..1b2b205d45 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..0e2f0feada 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -163,6 +163,37 @@ namespace Avalonia SetValue(property, AvaloniaProperty.UnsetValue); } + /// + /// Compares two objects using reference equality. + /// + /// The object to compare. + /// + /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: + /// + /// - AvaloniaObjects are by their nature mutable + /// - The presence of attached properties means that the semantics of equality are + /// difficult to define + /// + /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted + /// this. + /// + public sealed override bool Equals(object obj) => base.Equals(obj); + + /// + /// Gets the hash code for the object. + /// + /// + /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: + /// + /// - AvaloniaObjects are by their nature mutable + /// - The presence of attached properties means that the semantics of equality are + /// difficult to define + /// + /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted + /// this. + /// + public sealed override int GetHashCode() => base.GetHashCode(); + /// /// Gets a value. /// @@ -466,7 +497,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected void RaisePropertyChanged( + protected internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, @@ -508,45 +539,6 @@ namespace Avalonia } } - /// - /// A callback type for encapsulating complex logic for setting direct properties. - /// - /// The type of the property. - /// The value to which to set the property. - /// The backing field for the property. - /// A wrapper for the property-changed notification. - protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); - - /// - /// Sets the backing field for a direct avalonia property, raising the - /// event if the value has changed. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// A callback called to actually set the value to the backing field. - /// The value. - /// - /// True if the value changed, otherwise false. - /// - protected bool SetAndRaise( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - Contract.Requires(setterCallback != null); - return Values.Setter.SetAndNotify( - property, - ref field, - (object update, ref T backing, Action notify) => - { - setterCallback((T)update, ref backing, notify); - return true; - }, - value); - } - /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -561,32 +553,15 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - value); - } - /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) - { - var old = field; - field = value; + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetter setter = Values.GetDirectDeferredSetter(property); - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; + return setter.SetAndNotify(this, property, ref field, value); } /// diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 990a4b04f2..0ffd6a9539 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return AvaloniaProperty.UnsetValue; + return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 540b1bf19b..1d6e5e59ad 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -29,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Returns deferred setter for given non-direct property. + /// + /// Property. + /// Deferred setter for given property. + DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); + /// /// Logs a binding error. /// @@ -40,7 +47,5 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); - - DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 89a893577f..4996420fe7 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -30,7 +30,9 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; + private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; + private DeferredSetter _setter; /// /// Initializes a new instance of the class. @@ -50,6 +52,7 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; + _setAndNotifyCallback = SetAndNotify; } /// @@ -242,22 +245,22 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - Owner.Setter.SetAndNotify(Property, - ref _value, - UpdateCore, - (value, priority)); - } + var newValue = (value, priority); + + if (newValue == _value) + { + return; + } - private bool UpdateCore( - object update, - ref (object value, int priority) backing, - Action notify) - => UpdateCore(((object, int))update, ref backing, notify); + if (_setter == null) + { + _setter = Owner.GetNonDirectDeferredSetter(Property); + } + + _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + } - private bool UpdateCore( - (object value, int priority) update, - ref (object value, int priority) backing, - Action notify) + private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; @@ -286,7 +289,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); + Owner?.Changed(Property, ValuePriority, old, Value); } if (notification != null) @@ -305,7 +308,6 @@ namespace Avalonia val, val?.GetType()); } - return true; } } } diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs new file mode 100644 index 0000000000..ac128d83de --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -0,0 +1,150 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore + { + private Entry[] _entries; + + public AvaloniaPropertyValueStore() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValue(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValue(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,168 +1,122 @@ -using System; -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; namespace Avalonia.Utilities { + /// + /// Callback invoked when deferred setter wants to set a value. + /// + /// Value type. + /// Property being set. + /// Backing field reference. + /// New value. + internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); + /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. /// /// The type of value with which to track the delayed assignment. - class DeferredSetter + internal sealed class DeferredSetter { - private struct NotifyDisposable : IDisposable + private readonly SingleOrQueue _pendingValues; + private bool _isNotifying; + + public DeferredSetter() { - private readonly SettingStatus status; + _pendingValues = new SingleOrQueue(); + } - internal NotifyDisposable(SettingStatus status) - { - this.status = status; - status.Notifying = true; - } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; - public void Dispose() - { - status.Notifying = false; - } + backing = value; + + source.RaisePropertyChanged(property, old, value); } - /// - /// Information on current setting/notification status of a property. - /// - private class SettingStatus + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value) { - public bool Notifying { get; set; } - - private SingleOrQueue pendingValues; - - public SingleOrQueue PendingValues + if (!_isNotifying) { - get + using (new NotifyDisposable(this)) { - return pendingValues ?? (pendingValues = new SingleOrQueue()); + SetAndRaisePropertyChanged(source, property, ref backing, value); } - } - } - private Dictionary _setRecords; - private Dictionary SetRecords - => _setRecords ?? (_setRecords = new Dictionary()); + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } - private SettingStatus GetOrCreateStatus(AvaloniaProperty property) - { - if (!SetRecords.TryGetValue(property, out var status)) - { - status = new SettingStatus(); - SetRecords.Add(property, status); + return true; } - return status; + _pendingValues.Enqueue(value); + + return false; } - /// - /// Mark the property as currently notifying. - /// - /// The property to mark as notifying. - /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(AvaloniaProperty property) + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } - /// - /// Check if the property is currently notifying listeners. - /// - /// The property. - /// If the property is currently notifying listeners. - private bool IsNotifying(AvaloniaProperty property) - => SetRecords.TryGetValue(property, out var value) && value.Notifying; + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); + return true; + } - GetOrCreateStatus(property).PendingValues.Enqueue(value); - } + _pendingValues.Enqueue(value); - /// - /// Checks if there are any pending assignments for the property. - /// - /// The property to check. - /// If the property has any pending assignments. - private bool HasPendingSet(AvaloniaProperty property) - { - return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; + return false; } /// - /// Gets the first pending assignment for the property. + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. /// - /// The property to check. - /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(AvaloniaProperty property) + private readonly struct NotifyDisposable : IDisposable { - return GetOrCreateStatus(property).PendingValues.Dequeue(); - } - - public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + private readonly DeferredSetter _setter; - /// - /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 - /// - /// The property to set. - /// The backing field for the property - /// - /// A callback that actually sets the property. - /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. - /// - /// The value to try to set. - public bool SetAndNotify( - AvaloniaProperty property, - ref TValue backing, - SetterDelegate setterCallback, - TSetRecord value) - { - Contract.Requires(setterCallback != null); - if (!IsNotifying(property)) + internal NotifyDisposable(DeferredSetter setter) { - bool updated = false; - if (!object.Equals(value, backing)) - { - updated = setterCallback(value, ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - while (HasPendingSet(property)) - { - updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - - return updated; + _setter = setter; + _setter._isNotifying = true; } - else if(!object.Equals(value, backing)) + + public void Dispose() { - AddPendingSet(property, value); + _setter._isNotifying = false; } - return false; } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..1bdbd4ca7c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValue(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,28 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { - get + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); + return (DeferredSetter)deferredSetter; } - } - private bool TryGetValue(AvaloniaProperty property, out object value) - { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) - { - value = null; - return false; - } - - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetter(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValue(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) { - _entries[TryFindEntry(property.Id).Item1].Value = value; + return GetDeferredSetter(property); } - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else - { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); - } - - return (0, false); + return GetDeferredSetter(property); } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index bcd12fbfbb..490a724eda 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -24,6 +24,7 @@ using System.Linq; using Avalonia.Input.Platform; using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 2889a5c77c..eaa267ba66 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -195,7 +195,6 @@ - @@ -230,4 +229,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 473c4fe21b..1e2fc9f9d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -345,7 +345,6 @@ namespace Avalonia.Controls /// private IDisposable _collectionChangeSubscription; - private IMemberSelector _valueMemberSelector; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; @@ -541,12 +540,6 @@ namespace Avalonia.Controls o => o.Items, (o, v) => o.Items = v); - public static readonly DirectProperty ValueMemberSelectorProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueMemberSelector), - o => o.ValueMemberSelector, - (o, v) => o.ValueMemberSelector = v); - public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), @@ -958,20 +951,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the MemberSelector that is used to get values for - /// display in the text portion of the - /// control. - /// - /// The MemberSelector that is used to get values for display in - /// the text portion of the - /// control. - public IMemberSelector ValueMemberSelector - { - get { return _valueMemberSelector; } - set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } - } - /// /// Gets or sets the selected item in the drop-down. /// @@ -1841,11 +1820,6 @@ namespace Avalonia.Controls return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; } - if (_valueMemberSelector != null) - { - value = _valueMemberSelector.Select(value); - } - return value == null ? String.Empty : value.ToString(); } diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 8232697c18..395196d926 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -4,6 +4,7 @@ // All other rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using Avalonia.Data; @@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives { if (MonthView != null) { + var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth; + var children = new List(childCount); + for (int i = 0; i < Calendar.RowsPerMonth; i++) { if (_dayTitleTemplate != null) @@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); - MonthView.Children.Add(cell); + children.Add(cell); } } @@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives cell.PointerEnter += Cell_MouseEnter; cell.PointerLeave += Cell_MouseLeave; cell.Click += Cell_Click; - MonthView.Children.Add(cell); + children.Add(cell); } } + + MonthView.Children.AddRange(children); } if (YearView != null) { + var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear; + var children = new List(childCount); + CalendarButton month; for (int i = 0; i < Calendar.RowsPerYear; i++) { @@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; month.PointerEnter += Month_MouseEnter; month.PointerLeave += Month_MouseLeave; - YearView.Children.Add(month); + children.Add(month); } } + + YearView.Children.AddRange(children); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 5d427df5a6..f32b8fabc6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -333,8 +333,7 @@ namespace Avalonia.Controls } else { - var selector = MemberSelector; - SelectionBoxItem = selector != null ? selector.Select(item) : item; + SelectionBoxItem = item; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..d0804107b3 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,12 +1,13 @@ using System; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +91,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +113,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +128,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +144,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +172,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 653a4f5dcb..2d6757219d 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators /// The index of the item of data in the control's items. /// /// The item. - /// An optional member selector. /// The created controls. - ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector); + ItemContainerInfo Materialize(int index, object item); /// /// Removes a set of created containers. @@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); - bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector); + bool TryRecycle(int oldIndex, int newIndex, object item); /// /// Clears all created containers and returns the removed controls. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f1a1f94a01..4fd6f4135c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators public virtual Type ContainerType => null; /// - public ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector) + public ItemContainerInfo Materialize(int index, object item) { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainerInfo(CreateContainer(i), item, index); + var container = new ItemContainerInfo(CreateContainer(item), item, index); _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); @@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators } /// - public virtual bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) - { - return false; - } + public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; /// public virtual IEnumerable Clear() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 320d6c8faf..d1d1c2b172 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators } /// - public override bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) { var container = ContainerFromIndex(oldIndex); @@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators throw new IndexOutOfRangeException("Could not recycle container: not materialized."); } - var i = selector != null ? selector.Select(item) : item; - - container.SetValue(ContentProperty, i); + container.SetValue(ContentProperty, item); if (!(item is IControl)) { - container.DataContext = i; + container.DataContext = item; } - var info = MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, item); RaiseRecycled(new ItemContainerEventArgs(info)); return true; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 43d1108fb9..c06a64443c 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -117,10 +117,7 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } - public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) - { - return false; - } + public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; class WrapperTreeDataTemplate : ITreeDataTemplate { diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 304a760216..28b9b3a38f 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs new file mode 100644 index 0000000000..6b5cb2ee25 --- /dev/null +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls +{ + public interface IScrollAnchorProvider + { + IControl CurrentAnchor { get; } + void RegisterAnchorCandidate(IControl element); + void UnregisterAnchorCandidate(IControl element); + } +} diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index fa6f5787be..ff6cd482df 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls } } - return result.Constrain(availableSize); + return result; } /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a292ff7d0a..902e55bde6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,12 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - AvaloniaProperty.Register(nameof(MemberSelector)); - private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -144,15 +138,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the items presenter control. /// diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 07372eb714..1430c39c76 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls } /// - /// Utilize the for layout transforms. + /// Utilize the for layout transforms. /// public bool UseRenderTransform { diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3150b6be91..f26cd47bcb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -68,7 +68,13 @@ namespace Avalonia.Controls /// public new IList SelectedItems => base.SelectedItems; - /// + /// + /// Gets or sets the selection mode. + /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// public new SelectionMode SelectionMode { get { return base.SelectionMode; } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..6ec97aa04e 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -40,37 +41,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..8eed58bb4d 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -7,7 +7,6 @@ using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -31,13 +30,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 0f365fcb08..a4c674a03b 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); LogicalChildren.InsertRange(e.NewStartingIndex, controls); - VisualChildren.AddRange(e.NewItems.OfType()); + VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Move: diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index bb357453ff..cb1291410a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,94 +9,69 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer( - DispatcherPriority.Render, - new TimeSpan(0, 0, 0, 0, 66), - () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - private readonly AutoResetEvent _queued = new AutoResetEvent(false); - private readonly Queue _actions = new Queue(); public void RunLoop(CancellationToken cancellationToken) { - var handles = new[] {_signaled, _queued}; while (true) { - if (0 == WaitHandle.WaitAny(handles)) - Signaled?.Invoke(null); - else - { - while (true) - { - Action item; - lock (_actions) - if (_actions.Count == 0) - break; - else - item = _actions.Dequeue(); - item(); - } - } + Signaled?.Invoke(null); + _signaled.WaitOne(); } } - public void Send(Action cb) - { - lock (_actions) - { - _actions.Enqueue(cb); - _queued.Set(); - } - } - class WatTimer : IDisposable + class TimerImpl : IDisposable { - private readonly IDisposable _timer; + private readonly DispatcherPriority _priority; + private readonly TimeSpan _interval; + private readonly Action _tick; + private Timer _timer; private GCHandle _handle; - public WatTimer(IDisposable timer) + public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) { - _timer = timer; + _priority = priority; + _interval = interval; + _tick = tick; + _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1)); _handle = GCHandle.Alloc(_timer); } + private void OnTimer(object state) + { + if (_timer == null) + return; + Dispatcher.UIThread.Post(() => + { + + if (_timer == null) + return; + _tick(); + _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1)); + }); + } + + public void Dispose() { _handle.Free(); _timer.Dispose(); + _timer = null; } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - return new WatTimer(new System.Threading.Timer(delegate - { - var tcs = new TaskCompletionSource(); - Send(() => - { - try - { - tick(); - } - finally - { - tcs.SetResult(0); - } - }); - - - tcs.Task.Wait(); - }, null, TimeSpan.Zero, interval)); - - + return new TimerImpl(priority, interval, tick); } public void Signal(DispatcherPriority prio) diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a3123cf8c6..dedab3e43e 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters if (container == null && IsVirtualized) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + var materialized = ItemContainerGenerator.Materialize(index, item); Panel.Children.Add(materialized.ContainerControl); container = materialized.ContainerControl; } diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs index 035d404dec..6e72908e6b 100644 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 46da8fe3f8..ae52e733b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 413855bcc6..56f64779f6 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, Owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d11ce9a7ea..b8b8094582 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) { - var memberSelector = Owner.MemberSelector; var index = NextIndex; var step = 1; @@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters } } - var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + var materialized = generator.Materialize(index, Items.ElementAt(index)); if (step == 1) { @@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; var containers = generator.Containers.ToList(); var itemIndex = FirstIndex; @@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters if (!object.Equals(container.Item, item)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + if (!generator.TryRecycle(itemIndex, itemIndex, item)) { throw new NotImplementedException(); } @@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; //validate delta it should never overflow last index or generate index < 0 delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); @@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters var item = Items.ElementAt(newItemIndex); - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b4b792139d..ea56a0c6fc 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - ItemsControl.MemberSelectorProperty.AddOwner(); - private IEnumerable _items; private IDisposable _itemsSubscription; private bool _createdPanel; @@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the panel used to display the items. /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index b3345ec101..debbb81264 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters AffectsRender(PasswordCharProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, SelectionStartProperty, SelectionEndProperty); + + Observable.Merge( + SelectionStartProperty.Changed, + SelectionEndProperty.Changed, + PasswordCharProperty.Changed + ).AddClassHandler((x,_) => x.InvalidateFormattedText()); + + CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } public TextPresenter() @@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters _caretTimer = new DispatcherTimer(); _caretTimer.Interval = TimeSpan.FromMilliseconds(500); _caretTimer.Tick += CaretTimerTick; - - Observable.Merge( - this.GetObservable(SelectionStartProperty), - this.GetObservable(SelectionEndProperty)) - .Subscribe(_ => InvalidateFormattedText()); - - this.GetObservable(CaretIndexProperty) - .Subscribe(CaretIndexChanged); - - this.GetObservable(PasswordCharProperty) - .Subscribe(_ => InvalidateFormattedText()); } public int CaretIndex diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..058658357f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; @@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index e1b3061b54..c6119e89dc 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 91a9fa7e40..c8c15bc079 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the selection mode. /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } @@ -329,6 +333,11 @@ namespace Avalonia.Controls.Primitives case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: SelectedIndex = IndexOf(Items, SelectedItem); + + if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) + { + SelectedIndex = 0; + } break; } } @@ -338,24 +347,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var selectedIndex = SelectedIndex; - var selectedContainer = e.Containers - .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + var resetSelectedItems = false; - if (selectedContainer != null) + foreach (var container in e.Containers) { - SelectedIndex = selectedContainer.Index; - } - else if (selectedIndex >= e.StartingIndex && - selectedIndex < e.StartingIndex + e.Containers.Count) - { - var container = e.Containers[selectedIndex - e.StartingIndex]; + if ((container.ContainerControl as ISelectable)?.IsSelected == true) + { + if (SelectedIndex == -1) + { + SelectedIndex = container.Index; + } + else + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } - if (container.ContainerControl != null) + MarkContainerSelected(container.ContainerControl, true); + } + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// @@ -469,11 +490,6 @@ namespace Avalonia.Controls.Primitives /// protected void SelectAll() { - if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) - { - throw new NotSupportedException("Multiple selection is not enabled on this control."); - } - UpdateSelectedItems(() => { _selection.Clear(); @@ -523,7 +539,14 @@ namespace Avalonia.Controls.Primitives var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var range = multi && rangeModifier; - if (range) + if (rightButton) + { + if (!_selection.Contains(index)) + { + UpdateSelectedItem(index); + } + } + else if (range) { UpdateSelectedItems(() => { @@ -582,7 +605,7 @@ namespace Avalonia.Controls.Primitives } else { - UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); + UpdateSelectedItem(index); } if (Presenter?.Panel != null) diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..ec0dbd124c 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -12,11 +13,8 @@ namespace Avalonia.Controls.Primitives private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); - static TabStrip() { - MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -51,11 +49,5 @@ namespace Avalonia.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } - - private static object SelectHeader(object o) - { - var headered = o as IHeadered; - return (headered != null) ? (headered.Header ?? string.Empty) : o; - } } } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index c96fea6c25..21a7dd68f8 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Metadata; namespace Avalonia.Controls.Primitives diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a0f51099cd..29e3a17f74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -33,8 +34,8 @@ namespace Avalonia.Controls static ProgressBar() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.UpdateIndicatorWhenPropChanged); @@ -120,4 +121,4 @@ namespace Avalonia.Controls UpdateIndicator(Bounds.Size); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs new file mode 100644 index 0000000000..04d859c742 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -0,0 +1,54 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class ItemTemplateWrapper + { + private readonly IDataTemplate _dataTemplate; + + public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; + + public IControl GetElement(IControl parent, object data) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + IControl element = null; + + if (recyclePool != null) + { + // try to get an element from the recycle pool. + element = recyclePool.TryGetElement(string.Empty, parent); + } + + if (element == null) + { + // no element was found in recycle pool, create a new element + element = selectedTemplate.Build(data); + + // Associate template with element + element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate); + } + + return element; + } + + public void RecycleElement(IControl parent, IControl element) + { + var selectedTemplate = _dataTemplate; + var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); + if (recyclePool == null) + { + // No Recycle pool in the template, create one. + recyclePool = new RecyclePool(); + RecyclePool.SetPoolInstance(selectedTemplate, recyclePool); + } + + recyclePool.PutElement(element, "" /* key */, parent); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs new file mode 100644 index 0000000000..44783e2c97 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -0,0 +1,724 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Represents a data-driven collection control that incorporates a flexible layout system, + /// custom views, and virtualization. + /// + public class ItemsRepeater : Panel + { + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty HorizontalCacheLengthProperty = + AvaloniaProperty.Register(nameof(HorizontalCacheLength), 2.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemTemplateProperty = + ItemsControl.ItemTemplateProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty VerticalCacheLengthProperty = + AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); + + private static readonly AttachedProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + + internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); + internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); + + private readonly ViewManager _viewManager; + private readonly ViewportManager _viewportManager; + private IEnumerable _items; + private VirtualizingLayoutContext _layoutContext; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private bool _isLayoutInProgress; + private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; + private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; + private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; + + /// + /// Initializes a new instance of the class. + /// + public ItemsRepeater() + { + _viewManager = new ViewManager(this); + _viewportManager = new ViewportManager(this); + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); + OnLayoutChanged(null, Layout); + } + + static ItemsRepeater() + { + ClipToBoundsProperty.OverrideDefaultValue(true); + } + + /// + /// Gets or sets the layout used to size and position elements in the ItemsRepeater. + /// + /// + /// The layout used to size and position elements. The default is a StackLayout with + /// vertical orientation. + /// + public AttachedLayout Layout + { + get => GetValue(LayoutProperty); + set => SetValue(LayoutProperty, value); + } + + /// + /// Gets or sets an object source used to generate the content of the ItemsRepeater. + /// + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + /// + /// Gets or sets the template used to display each item. + /// + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling horizontally. + /// + public double HorizontalCacheLength + { + get => GetValue(HorizontalCacheLengthProperty); + set => SetValue(HorizontalCacheLengthProperty, value); + } + + /// + /// Gets or sets a value that indicates the size of the buffer used to realize items when + /// panning or scrolling vertically. + /// + public double VerticalCacheLength + { + get => GetValue(VerticalCacheLengthProperty); + set => SetValue(VerticalCacheLengthProperty, value); + } + + /// + /// Gets a standardized view of the supported interactions between a given Items object and + /// the ItemsRepeater control and its associated components. + /// + public ItemsSourceView ItemsSourceView { get; private set; } + + internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal Point LayoutOrigin { get; set; } + internal object LayoutState { get; set; } + internal IControl MadeAnchor => _viewportManager.MadeAnchor; + internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow(); + internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor; + + private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; + + private LayoutContext LayoutContext + { + get + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } + + /// + /// Occurs each time an element is cleared and made available to be re-used. + /// + /// + /// This event is raised immediately each time an element is cleared, such as when it falls + /// outside the range of realized items. Elements are cleared when they become available + /// for re-use. + /// + public event EventHandler ElementClearing; + + /// + /// Occurs for each realized when the index for the item it + /// represents has changed. + /// + /// + /// When you use ItemsRepeater to build a more complex control that supports specific + /// interactions on the child elements (such as selection or click), it is useful to be + /// able to keep an up-to-date identifier for the backing data item. + /// + /// This event is raised for each realized IControl where the index for the item it + /// represents has changed. For example, when another item is added or removed in the data + /// source, the index for items that come after in the ordering will be impacted. + /// + public event EventHandler ElementIndexChanged; + + /// + /// Occurs each time an element is prepared for use. + /// + /// + /// The prepared element might be newly created or an existing element that is being re- + /// used. + /// + public event EventHandler ElementPrepared; + + /// + /// Retrieves the index of the item from the data source that corresponds to the specified + /// . + /// + /// + /// The element that corresponds to the item to get the index of. + /// + /// + /// The index of the item from the data source that corresponds to the specified UIElement, + /// or -1 if the element is not supported. + /// + public int GetElementIndex(IControl element) => GetElementIndexImpl(element); + + /// + /// Retrieves the realized UIElement that corresponds to the item at the specified index in + /// the data source. + /// + /// The index of the item. + /// + /// he UIElement that corresponds to the item at the specified index if the item is + /// realized, or null if the item is not realized. + /// + public IControl TryGetElement(int index) => GetElementFromIndexImpl(index); + + internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true); + + internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); + + internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index); + + internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + { + var value = element.GetValue(VirtualizationInfoProperty); + return value; + } + + internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) + { + if (TryGetVirtualizationInfo(element) != null) + { + throw new InvalidOperationException("VirtualizationInfo already created."); + } + + var result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + return result; + } + + internal static VirtualizationInfo GetVirtualizationInfo(IControl element) + { + var result = element.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); + } + + return result; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _viewportManager.OnOwnerMeasuring(); + + _isLayoutInProgress = true; + + try + { + _viewManager.PrunePinnedElements(); + var extent = new Rect(); + var desiredSize = new Size(); + var layout = Layout; + + if (layout != null) + { + var layoutContext = GetLayoutContext(); + + desiredSize = layout.Measure(layoutContext, availableSize); + extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); + + // Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not + // call GetElementAt(index). + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + + if (virtInfo.Owner == ElementOwner.Layout && + virtInfo.AutoRecycleCandidate && + !virtInfo.KeepAlive) + { + ClearElementImpl(element); + } + } + } + + _viewportManager.SetLayoutExtent(extent); + return desiredSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Reentrancy detected during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new NotSupportedException("Cannot run layout in the middle of a collection change."); + } + + _isLayoutInProgress = true; + + try + { + var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + + // The view manager might clear elements during this call. + // That's why we call it before arranging cleared elements + // off screen. + _viewManager.OnOwnerArranged(); + + foreach (var element in Children) + { + var virtInfo = GetVirtualizationInfo(element); + virtInfo.KeepAlive = false; + + if (virtInfo.Owner == ElementOwner.ElementFactory || + virtInfo.Owner == ElementOwner.PinnedPool) + { + // Toss it away. And arrange it with size 0 so that XYFocus won't use it. + element.Arrange(new Rect( + ClearedElementsArrangePosition.X - element.DesiredSize.Width, + ClearedElementsArrangePosition.Y - element.DesiredSize.Height, + 0, + 0)); + } + else + { + var newBounds = element.Bounds; + virtInfo.ArrangeBounds = newBounds; + } + } + + _viewportManager.OnOwnerArranged(); + + return arrangeSize; + } + finally + { + _isLayoutInProgress = false; + } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + InvalidateMeasure(); + _viewportManager.ResetScrollers(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewportManager.ResetScrollers(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + var property = args.Property; + + if (property == ItemsProperty) + { + var newValue = (IEnumerable)args.NewValue; + var newDataSource = newValue as ItemsSourceView; + if (newValue != null && newDataSource == null) + { + newDataSource = new ItemsSourceView(newValue); + } + + OnDataSourcePropertyChanged(ItemsSourceView, newDataSource); + } + else if (property == ItemTemplateProperty) + { + OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue); + } + else if (property == LayoutProperty) + { + OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue); + } + else if (property == HorizontalCacheLengthProperty) + { + _viewportManager.HorizontalCacheLength = (double)args.NewValue; + } + else if (property == VerticalCacheLengthProperty) + { + _viewportManager.VerticalCacheLength = (double)args.NewValue; + } + else + { + base.OnPropertyChanged(args); + } + } + + internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + { + var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + return element; + } + + internal void ClearElementImpl(IControl element) + { + // Clearing an element due to a collection change + // is more strict in that pinned elements will be forcibly + // unpinned and sent back to the view generator. + var isClearedDueToCollectionChange = + _processingItemsSourceChange != null && + (_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace || + _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); + + _viewManager.ClearElement(element, isClearedDueToCollectionChange); + _viewportManager.OnElementCleared(element); + } + + private int GetElementIndexImpl(IControl element) + { + var virtInfo = TryGetVirtualizationInfo(element); + return _viewManager.GetElementIndex(virtInfo); + } + + private IControl GetElementFromIndexImpl(int index) + { + IControl result = null; + + var children = Children; + for (var i = 0; i < children.Count && result == null; ++i) + { + var element = children[i]; + var virtInfo = TryGetVirtualizationInfo(element); + if (virtInfo?.IsRealized == true && virtInfo.Index == index) + { + result = element; + } + } + + return result; + } + + private IControl GetOrCreateElementImpl(int index) + { + if (index >= 0 && index >= ItemsSourceView.Count) + { + throw new ArgumentException("Argument index is invalid.", "index"); + } + + if (_isLayoutInProgress) + { + throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout."); + } + + var element = GetElementFromIndexImpl(index); + bool isAnchorOutsideRealizedRange = element == null; + + if (isAnchorOutsideRealizedRange) + { + if (Layout == null) + { + throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); + } + + element = (IControl)GetLayoutContext().GetOrCreateElementAt(index); + element.Measure(Size.Infinity); + } + + _viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange); + InvalidateMeasure(); + + return element; + } + + internal void OnElementPrepared(IControl element, int index) + { + _viewportManager.OnElementPrepared(element); + if (ElementPrepared != null) + { + if (_elementPreparedArgs == null) + { + _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); + } + else + { + _elementPreparedArgs.Update(element, index); + } + + ElementPrepared(this, _elementPreparedArgs); + } + } + + internal void OnElementClearing(IControl element) + { + if (ElementClearing != null) + { + if (_elementClearingArgs == null) + { + _elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element); + } + else + { + _elementClearingArgs.Update(element); + } + + ElementClearing(this, _elementClearingArgs); + } + } + + internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) + { + if (ElementIndexChanged != null) + { + if (_elementIndexChangedArgs == null) + { + _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); + } + else + { + _elementIndexChangedArgs.Update(element, oldIndex, newIndex); + } + + ElementIndexChanged(this, _elementIndexChangedArgs); + } + } + + private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) + { + if (_isLayoutInProgress) + { + throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); + } + + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + + if (oldValue != null) + { + oldValue.CollectionChanged -= OnItemsSourceViewChanged; + } + + if (newValue != null) + { + newValue.CollectionChanged += OnItemsSourceViewChanged; + } + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + else if (Layout is NonVirtualizingLayout nonVirtualLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + + InvalidateMeasure(); + } + } + + private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue) + { + if (_isLayoutInProgress && oldValue != null) + { + throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout."); + } + + // Since the ItemTemplate has changed, we need to re-evaluate all the items that + // have already been created and are now in the tree. The easiest way to do that + // would be to do a reset.. Note that this has to be done before we change the template + // so that the cleared elements go back into the old template. + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } + } + else if (Layout is NonVirtualizingLayout) + { + // Walk through all the elements and make sure they are cleared for + // non-virtualizing layouts. + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + } + } + + ItemTemplateShim = new ItemTemplateWrapper(newValue); + + InvalidateMeasure(); + } + + private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue) + { + if (_isLayoutInProgress) + { + throw new InvalidOperationException("Layout cannot be changed during layout."); + } + + _viewManager.OnLayoutChanging(); + + if (oldValue != null) + { + oldValue.UninitializeForContext(LayoutContext); + oldValue.MeasureInvalidated -= InvalidateMeasureForLayout; + oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; + + // Walk through all the elements and make sure they are cleared + foreach (var element in Children) + { + if (GetVirtualizationInfo(element).IsRealized) + { + ClearElementImpl(element); + } + } + + LayoutState = null; + } + + if (newValue != null) + { + newValue.InitializeForContext(LayoutContext); + newValue.MeasureInvalidated += InvalidateMeasureForLayout; + newValue.ArrangeInvalidated += InvalidateArrangeForLayout; + } + + bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + _viewportManager.OnLayoutChanged(isVirtualizingLayout); + InvalidateMeasure(); + } + + private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_isLayoutInProgress) + { + // Bad things will follow if the data changes while we are in the middle of a layout pass. + throw new InvalidOperationException("Changes in data source are not allowed during layout."); + } + + if (IsProcessingCollectionChange) + { + throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source."); + } + + _processingItemsSourceChange = args; + + try + { + _viewManager.OnItemsSourceChanged(sender, args); + + if (Layout != null) + { + if (Layout is VirtualizingLayout virtualLayout) + { + virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); + } + else + { + // NonVirtualizingLayout + InvalidateMeasure(); + } + } + } + finally + { + _processingItemsSourceChange = null; + } + } + + private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure(); + + private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange(); + + private VirtualizingLayoutContext GetLayoutContext() + { + if (_layoutContext == null) + { + _layoutContext = new RepeaterLayoutContext(this); + } + + return _layoutContext; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs new file mode 100644 index 0000000000..75d50e52a6 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs @@ -0,0 +1,24 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementClearingEventArgs : EventArgs + { + internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element; + + /// + /// Gets the element that is being cleared for re-use. + /// + public IControl Element { get; private set; } + + internal void Update(IControl element) => Element = element; + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs new file mode 100644 index 0000000000..7ca68140b2 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs @@ -0,0 +1,44 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs + { + internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + + /// + /// Get the element for which the index changed. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the element after the change. + /// + public int NewIndex { get; private set; } + + /// + /// Gets the index of the element before the change. + /// + public int OldIndex { get; private set; } + + internal void Update(IControl element, int newIndex, int oldIndex) + { + Element = element; + NewIndex = newIndex; + OldIndex = oldIndex; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs new file mode 100644 index 0000000000..5a30dbcf2a --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs @@ -0,0 +1,35 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +namespace Avalonia.Controls +{ + /// + /// Provides data for the event. + /// + public class ItemsRepeaterElementPreparedEventArgs + { + internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index) + { + Element = element; + Index = index; + } + + /// + /// Gets the prepared element. + /// + public IControl Element { get; private set; } + + /// + /// Gets the index of the item the element was prepared for. + /// + public int Index { get; private set; } + + internal void Update(IControl element, int index) + { + Element = element; + Index = index; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemsSourceView.cs b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs new file mode 100644 index 0000000000..732ba8501c --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ItemsSourceView.cs @@ -0,0 +1,143 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls +{ + /// + /// Represents a standardized view of the supported interactions between a given ItemsSource + /// object and an control. + /// + /// + /// Components written to work with ItemsRepeater should consume the + /// via ItemsSourceView since this provides a normalized + /// view of the Items. That way, each component does not need to know if the source is an + /// IEnumerable, an IList, or something else. + /// + public class ItemsSourceView : INotifyCollectionChanged, IDisposable + { + private readonly IList _inner; + private INotifyCollectionChanged _notifyCollectionChanged; + private int _cachedSize = -1; + + /// + /// Initializes a new instance of the ItemsSourceView class for the specified data source. + /// + /// The data source. + public ItemsSourceView(IEnumerable source) + { + Contract.Requires(source != null); + + _inner = source as IList; + + if (_inner == null && source is IEnumerable objectEnumerable) + { + _inner = new List(objectEnumerable); + } + else + { + _inner = new List(source.Cast()); + } + + ListenToCollectionChanges(); + } + + /// + /// Gets the number of items in the collection. + /// + public int Count + { + get + { + if (_cachedSize == -1) + { + _cachedSize = _inner.Count; + } + + return _cachedSize; + } + } + + /// + /// Gets a value that indicates whether the items source can provide a unique key for each item. + /// + /// + /// TODO: Not yet implemented in Avalonia. + /// + public bool HasKeyIndexMapping => false; + + /// + /// Occurs when the collection has changed to indicate the reason for the change and which items changed. + /// + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// + public void Dispose() + { + if (_notifyCollectionChanged != null) + { + _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + } + } + + /// + /// Retrieves the item at the specified index. + /// + /// The index. + /// the item. + public object GetAt(int index) => _inner[index]; + + /// + /// Retrieves the index of the item that has the specified unique identifier (key). + /// + /// The index. + /// The key + /// + /// TODO: Not yet implemented in Avalonia. + /// + public string KeyFromIndex(int index) + { + throw new NotImplementedException(); + } + + /// + /// Retrieves the unique identifier (key) for the item at the specified index. + /// + /// The key. + /// The index. + /// + /// TODO: Not yet implemented in Avalonia. + /// + public int IndexFromKey(string key) + { + throw new NotImplementedException(); + } + + protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) + { + _cachedSize = _inner.Count; + CollectionChanged?.Invoke(this, args); + } + + private void ListenToCollectionChanges() + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + _notifyCollectionChanged = incc; + } + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnItemsSourceChanged(e); + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs new file mode 100644 index 0000000000..4e5950bdc5 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -0,0 +1,106 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + internal class RecyclePool + { + public static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + + private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); + private readonly Dictionary> _elements = new Dictionary>(); + + public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate) + { + s_pools.TryGetValue(dataTemplate, out var result); + return result; + } + + public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value); + + public void PutElement(IControl element, string key, IControl owner) + { + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + var elementInfo = new ElementInfo(element, ownerAsPanel); + + if (!_elements.TryGetValue(key, out var pool)) + { + pool = new List(); + _elements.Add(key, pool); + } + + pool.Add(elementInfo); + } + + public IControl TryGetElement(string key, IControl owner) + { + if (_elements.TryGetValue(key, out var elements)) + { + if (elements.Count > 0) + { + // Prefer an element from the same owner or with no owner so that we don't incur + // the enter/leave cost during recycling. + // TODO: prioritize elements with the same owner to those without an owner. + var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault(); + elements.Remove(elementInfo); + + var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner); + if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel) + { + // Element is still under its parent. remove it from its parent. + var panel = elementInfo.Owner; + if (panel != null) + { + int childIndex = panel.Children.IndexOf(elementInfo.Element); + if (childIndex == -1) + { + throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection."); + } + + panel.Children.RemoveAt(childIndex); + } + } + + return elementInfo.Element; + } + } + + return null; + } + + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) + { + if (owner is IPanel panel) + { + return panel; + } + else if (owner != null) + { + throw new InvalidOperationException("Owner must be IPanel or null."); + } + + return null; + } + + private class ElementInfo + { + public ElementInfo(IControl element, IPanel owner) + { + Element = element; + Owner = owner; + } + + public IControl Element { get; } + public IPanel Owner { get;} + } + } +} diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs new file mode 100644 index 0000000000..977d9d794c --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs @@ -0,0 +1,65 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + internal class RepeaterLayoutContext : VirtualizingLayoutContext + { + private readonly ItemsRepeater _owner; + + public RepeaterLayoutContext(ItemsRepeater owner) + { + _owner = owner; + } + + protected override Point LayoutOriginCore + { + get => _owner.LayoutOrigin; + set => _owner.LayoutOrigin = value; + } + + protected override object LayoutStateCore + { + get => _owner.LayoutState; + set => _owner.LayoutState = value; + } + + protected override int RecommendedAnchorIndexCore + { + get + { + int anchorIndex = -1; + var anchor = _owner.SuggestedAnchor; + if (anchor != null) + { + anchorIndex = _owner.GetElementIndex(anchor); + } + + return anchorIndex; + } + } + + protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0; + + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) + { + return _owner.GetElementImpl( + index, + (options & ElementRealizationOptions.ForceCreate) != 0, + (options & ElementRealizationOptions.SuppressAutoRecycle) != 0); + } + + protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); + + protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); + + protected override Rect RealizationRectCore() => _owner.RealizationWindow; + } +} diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs new file mode 100644 index 0000000000..775aa3f113 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs @@ -0,0 +1,54 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + internal class UniqueIdElementPool : IEnumerable> + { + private readonly Dictionary _elementMap = new Dictionary(); + private readonly ItemsRepeater _owner; + + public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner; + + public void Add(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var key = virtInfo.UniqueId; + + if (_elementMap.ContainsKey(key)) + { + throw new InvalidOperationException($"The unique id provided ({key}) is not unique."); + } + + _elementMap.Add(key, element); + } + + public IControl Remove(int index) + { + // Check if there is already a element in the mapping and if so, use it. + string key = _owner.ItemsSourceView.KeyFromIndex(index); + + if (_elementMap.TryGetValue(key, out var element)) + { + _elementMap.Remove(key); + } + + return element; + } + + public void Clear() + { + _elementMap.Clear(); + } + + public IEnumerator> GetEnumerator() => _elementMap.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs new file mode 100644 index 0000000000..833e708e9e --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -0,0 +1,682 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal sealed class ViewManager + { + private const int FirstRealizedElementIndexDefault = int.MaxValue; + private const int LastRealizedElementIndexDefault = int.MinValue; + + private readonly ItemsRepeater _owner; + private readonly List _pinnedPool = new List(); + private readonly UniqueIdElementPool _resetPool; + private IControl _lastFocusedElement; + private bool _isDataSourceStableResetPending; + private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + private bool _eventsSubscribed; + + public ViewManager(ItemsRepeater owner) + { + _owner = owner; + _resetPool = new UniqueIdElementPool(owner); + } + + public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle) + { + var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index); + if (element == null) + { + // check if this is the anchor made through repeater in preparation + // for a bring into view. + var madeAnchor = _owner.MadeAnchor; + if (madeAnchor != null) + { + var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); + if (anchorVirtInfo.Index == index) + { + element = madeAnchor; + } + } + } + if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; + if (element == null) { element = GetElementFromPinnedElements(index); } + if (element == null) { element = GetElementFromElementFactory(index); } + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (suppressAutoRecycle) + { + virtInfo.AutoRecycleCandidate = false; + } + else + { + virtInfo.AutoRecycleCandidate = true; + virtInfo.KeepAlive = true; + } + + return element; + } + + public void ClearElement(IControl element, bool isClearedDueToCollectionChange) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var index = virtInfo.Index; + bool cleared = + ClearElementToUniqueIdResetPool(element, virtInfo) || + ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange); + + if (!cleared) + { + ClearElementToElementFactory(element); + } + + // Both First and Last indices need to be valid or default. + if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout) + { + // First and last were pointing to the same element and that is going away. + InvalidateRealizedIndicesHeldByLayout(); + } + else if (index == _firstRealizedElementIndexHeldByLayout) + { + // The FirstElement is going away, shrink the range by one. + ++_firstRealizedElementIndexHeldByLayout; + } + else if (index == _lastRealizedElementIndexHeldByLayout) + { + // Last element is going away, shrink the range by one at the end. + --_lastRealizedElementIndexHeldByLayout; + } + else + { + // Index is either outside the range we are keeping track of or inside the range. + // In both these cases, we just keep the range we have. If this clear was due to + // a collection change, then in the CollectionChanged event, we will invalidate these guys. + } + } + + public void ClearElementToElementFactory(IControl element) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var clearedIndex = virtInfo.Index; + _owner.OnElementClearing(element); + _owner.ItemTemplateShim.RecycleElement(_owner, element); + + virtInfo.MoveOwnershipToElementFactory(); + + if (_lastFocusedElement == element) + { + // Focused element is going away. Remove the tracked last focused element + // and pick a reasonable next focus if we can find one within the layout + // realized elements. + MoveFocusFromClearedIndex(clearedIndex); + } + + } + + private void MoveFocusFromClearedIndex(int clearedIndex) + { + IControl focusedChild = null; + var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild); + if (focusCandidate != null) + { + focusCandidate.Focus(); + _lastFocusedElement = focusedChild; + + // Add pin to hold the focused element. + UpdatePin(focusedChild, true /* addPin */); + } + else + { + // We could not find a candiate. + _lastFocusedElement = null; + } + } + + IControl FindFocusCandidate(int clearedIndex, IControl focusedChild) + { + // Walk through all the children and find elements with index before and after the cleared index. + // Note that during a delete the next element would now have the same index. + int previousIndex = int.MinValue; + int nextIndex = int.MaxValue; + IControl nextElement = null; + IControl previousElement = null; + + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + int currentIndex = virtInfo.Index; + if (currentIndex < clearedIndex) + { + if (currentIndex > previousIndex) + { + previousIndex = currentIndex; + previousElement = child; + } + } + else if (currentIndex >= clearedIndex) + { + // Note that we use >= above because if we deleted the focused element, + // the next element would have the same index now. + if (currentIndex < nextIndex) + { + nextIndex = currentIndex; + nextElement = child; + } + } + } + } + + // TODO: Find the next element if one exists, if not use the previous element. + // If the container itself is not focusable, find a descendent that is. + + return nextElement; + } + + public int GetElementIndex(VirtualizationInfo virtInfo) + { + if (virtInfo == null) + { + throw new ArgumentException("Element is not a child of this ItemsRepeater."); + } + + return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1; + } + + public void PrunePinnedElements() + { + EnsureEventSubscriptions(); + + // Go through pinned elements and make sure they still have + // a reason to be pinned. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (!virtInfo.IsPinned) + { + _pinnedPool.RemoveAt(i); + --i; + + // Pinning was the only thing keeping this element alive. + ClearElementToElementFactory(elementInfo.PinnedElement); + } + } + } + + public void UpdatePin(IControl element, bool addPin) + { + var parent = element.VisualParent; + var child = (IVisual)element; + + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child); + if (virtInfo.IsRealized) + { + if (addPin) + { + virtInfo.AddPin(); + } + else if (virtInfo.IsPinned) + { + if (virtInfo.RemovePin() == 0) + { + // ElementFactory is invoked during the measure pass. + // We will clear the element then. + repeater.InvalidateMeasure(); + } + } + } + } + + child = parent; + parent = child.VisualParent; + } + } + + public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args) + { + // Note: For items that have been removed, the index will not be touched. It will hold + // the old index before it was removed. It is not valid anymore. + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + { + var newIndex = args.NewStartingIndex; + var newCount = args.NewItems.Count; + EnsureFirstLastRealizedIndices(); + if (newIndex <= _lastRealizedElementIndexHeldByLayout) + { + _lastRealizedElementIndexHeldByLayout += newCount; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + else + { + // Indices held by layout are not affected + // We could still have items in the pinned elements that need updates. This is usually a very small vector. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized && dataIndex >= newIndex) + { + var element = elementInfo.PinnedElement; + UpdateElementIndex(element, virtInfo, dataIndex + newCount); + } + } + } + break; + } + + case NotifyCollectionChangedAction.Replace: + { + // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true. + // Two cases here + // case 1: oldCount == newCount + // indices are not affected. nothing to do here. + // case 2: oldCount != newCount + // Replaced with less or more items. This is like an insert or remove + // depending on the counts. + var oldStartIndex = args.OldStartingIndex; + var newStartingIndex = args.NewStartingIndex; + var oldCount = args.OldItems.Count; + var newCount = args.NewItems.Count; + if (oldStartIndex != newStartingIndex) + { + throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); + } + + if (oldCount == 0) + { + throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); + } + + if (newCount == 0) + { + throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); + } + + int countChange = newCount - oldCount; + if (countChange != 0) + { + // countChange > 0 : countChange items were added + // countChange < 0 : -countChange items were removed + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (dataIndex >= oldStartIndex + oldCount) + { + UpdateElementIndex(element, virtInfo, dataIndex + countChange); + } + } + } + + EnsureFirstLastRealizedIndices(); + _lastRealizedElementIndexHeldByLayout += countChange; + } + break; + } + + case NotifyCollectionChangedAction.Remove: + { + var oldStartIndex = args.OldStartingIndex; + var oldCount = args.OldItems.Count; + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + var dataIndex = virtInfo.Index; + + if (virtInfo.IsRealized) + { + if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) + { + // If we are doing the mapping, remove the element who's data was removed. + _owner.ClearElementImpl(element); + } + else if (dataIndex >= (oldStartIndex + oldCount)) + { + UpdateElementIndex(element, virtInfo, dataIndex - oldCount); + } + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + + case NotifyCollectionChangedAction.Reset: + if (_owner.ItemsSourceView.HasKeyIndexMapping) + { + _isDataSourceStableResetPending = true; + } + + // Walk through all the elements and make sure they are cleared, they will go into + // the stable id reset pool. + foreach (var element in _owner.Children) + { + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) + { + _owner.ClearElementImpl(element); + } + } + + InvalidateRealizedIndicesHeldByLayout(); + break; + } + } + + private void EnsureFirstLastRealizedIndices() + { + if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault) + { + // This will ensure that the indexes are updated. + GetElementIfAlreadyHeldByLayout(0); + } + } + + public void OnLayoutChanging() + { + if (_owner.ItemsSourceView?.HasKeyIndexMapping == true) + { + _isDataSourceStableResetPending = true; + } + } + + public void OnOwnerArranged() + { + if (_isDataSourceStableResetPending) + { + _isDataSourceStableResetPending = false; + + foreach (var entry in _resetPool) + { + // TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource. + // Focused element is getting cleared. Need to figure out semantics on where + // focus should go when the focused element is removed from the data collection. + ClearElement(entry.Value, true /* isClearedDueToCollectionChange */); + } + + _resetPool.Clear(); + } + } + + // We optimize for the case where index is not realized to return null as quickly as we can. + // Flow layouts manage containers on their own and will never ask for an index that is already realized. + // If an index that is realized is requested by the layout, we unfortunately have to walk the + // children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing + // and non-virtualizing hosts. + private IControl GetElementIfAlreadyHeldByLayout(int index) + { + IControl element = null; + + bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault; + bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout); + + if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange) + { + foreach (var child in _owner.Children) + { + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); + if (virtInfo?.IsHeldByLayout == true) + { + // Only give back elements held by layout. If someone else is holding it, they will be served by other methods. + int childIndex = virtInfo.Index; + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex); + if (virtInfo.Index == index) + { + element = child; + // If we have valid first/last indices, we don't have to walk the rest, but if we + // do not, then we keep walking through the entire children collection to get accurate + // indices once. + if (!cachedFirstLastIndicesInvalid) + { + break; + } + } + } + } + } + + return element; + } + + private IControl GetElementFromUniqueIdResetPool(int index) + { + IControl element = null; + // See if you can get it from the reset pool. + if (_isDataSourceStableResetPending) + { + element = _resetPool.Remove(index); + if (element != null) + { + // Make sure that the index is updated to the current one + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); + virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool(); + UpdateElementIndex(element, virtInfo, index); + } + } + + return element; + } + + private IControl GetElementFromPinnedElements(int index) + { + IControl element = null; + + // See if you can find something among the pinned elements. + for (var i = 0; i < _pinnedPool.Count; ++i) + { + var elementInfo = _pinnedPool[i]; + var virtInfo = elementInfo.VirtualizationInfo; + + if (virtInfo.Index == index) + { + _pinnedPool.RemoveAt(i); + element = elementInfo.PinnedElement; + elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool(); + break; + } + } + + return element; + } + + private IControl GetElementFromElementFactory(int index) + { + // The view generator is the provider of last resort. + + var itemTemplateFactory = _owner.ItemTemplateShim; + if (itemTemplateFactory == null) + { + // If no ItemTemplate was provided, use a default + var factory = FuncDataTemplate.Default; + _owner.ItemTemplate = factory; + itemTemplateFactory = _owner.ItemTemplateShim; + } + + var data = _owner.ItemsSourceView.GetAt(index); + var element = itemTemplateFactory.GetElement(_owner, data); + + var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); + if (virtInfo == null) + { + virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); + } + + // Prepare the element + element.DataContext = data; + + virtInfo.MoveOwnershipToLayoutFromElementFactory( + index, + /* uniqueId: */ + _owner.ItemsSourceView.HasKeyIndexMapping ? + _owner.ItemsSourceView.KeyFromIndex(index) : + string.Empty); + + // The view generator is the only provider that prepares the element. + var repeater = _owner; + + // Add the element to the children collection here before raising OnElementPrepared so + // that handlers can walk up the tree in case they want to find their IndexPath in the + // nested case. + var children = repeater.Children; + if (element.VisualParent != repeater) + { + children.Add(element); + } + + repeater.OnElementPrepared(element, index); + + // Update realized indices + _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); + _lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index); + + return element; + } + + private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange) + { + if (_isDataSourceStableResetPending) + { + _resetPool.Add(element); + virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout(); + } + + return _isDataSourceStableResetPending; + } + + private void UpdateFocusedElement() + { + IControl focusedElement = null; + + var child = FocusManager.Instance.Current; + + if (child != null) + { + var parent = child.VisualParent; + var owner = _owner; + + // Find out if the focused element belongs to one of our direct + // children. + while (parent != null) + { + if (parent is ItemsRepeater repeater) + { + var element = child as IControl; + if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) + { + focusedElement = element; + } + + break; + } + + child = parent as IInputElement; + parent = child.VisualParent; + } + } + + // If the focused element has changed, + // we need to unpin the old one and pin the new one. + if (_lastFocusedElement != focusedElement) + { + if (_lastFocusedElement != null) + { + UpdatePin(_lastFocusedElement, false /* addPin */); + } + + if (focusedElement != null) + { + UpdatePin(focusedElement, true /* addPin */); + } + + _lastFocusedElement = focusedElement; + } + } + + private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement(); + + private void EnsureEventSubscriptions() + { + if (!_eventsSubscribed) + { + _owner.GotFocus += OnFocusChanged; + _owner.LostFocus += OnFocusChanged; + } + } + + private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index) + { + var oldIndex = virtInfo.Index; + if (oldIndex != index) + { + virtInfo.UpdateIndex(index); + _owner.OnElementIndexChanged(element, oldIndex, index); + } + } + + private void InvalidateRealizedIndicesHeldByLayout() + { + _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; + _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; + } + + private struct PinnedElementInfo + { + public PinnedElementInfo(IControl element) + { + PinnedElement = element; + VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); + } + + public IControl PinnedElement { get; } + public VirtualizationInfo VirtualizationInfo { get; } + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs new file mode 100644 index 0000000000..10c11889d0 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -0,0 +1,501 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + internal class ViewportManager + { + private const double CacheBufferPerSideInflationPixelDelta = 40.0; + private readonly ItemsRepeater _owner; + private bool _ensuredScroller; + private IScrollAnchorProvider _scroller; + private IControl _makeAnchorElement; + private bool _isAnchorOutsideRealizedRange; + private Task _cacheBuildAction; + private Rect _visibleWindow; + private Rect _layoutExtent; + // This is the expected shift by the layout. + private Point _expectedViewportShift; + // This is what is pending and not been accounted for. + // Sometimes the scrolling surface cannot service a shift (for example + // it is already at the top and cannot shift anymore.) + private Point _pendingViewportShift; + // Unshiftable shift amount that this view manager can + // handle on its own to fake it to the layout as if the shift + // actually happened. This can happen in cases where no scrollviewer + // in the parent chain can scroll in the shift direction. + private Point _unshiftableShift; + private double _maximumHorizontalCacheLength = 0.0; + private double _maximumVerticalCacheLength = 0.0; + private double _horizontalCacheBufferPerSide; + private double _verticalCacheBufferPerSide; + private bool _isBringIntoViewInProgress; + // For non-virtualizing layouts, we do not need to keep + // updating viewports and invalidating measure often. So when + // a non virtualizing layout is used, we stop doing all that work. + bool _managingViewportDisabled; + private IDisposable _effectiveViewportChangedRevoker; + private bool _layoutUpdatedSubscribed; + + public ViewportManager(ItemsRepeater owner) + { + _owner = owner; + } + + public IControl SuggestedAnchor + { + get + { + // The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick. + var suggestedAnchor = _makeAnchorElement; + var owner = _owner; + + if (suggestedAnchor == null) + { + var anchorElement = _scroller?.CurrentAnchor; + + if (anchorElement != null) + { + // We can't simply return anchorElement because, in case of nested Repeaters, it may not + // be a direct child of ours, or even an indirect child. We need to walk up the tree starting + // from anchorElement to figure out what child of ours (if any) to use as the suggested element. + var child = anchorElement; + var parent = child.VisualParent as IControl; + + while (parent != null) + { + if (parent == owner) + { + suggestedAnchor = child; + break; + } + + child = parent; + parent = parent.VisualParent as IControl; + } + } + } + + return suggestedAnchor; + } + } + + public bool HasScroller => _scroller != null; + + public IControl MadeAnchor => _makeAnchorElement; + + public double HorizontalCacheLength + { + get => _maximumHorizontalCacheLength; + set + { + if (_maximumHorizontalCacheLength != value) + { + ValidateCacheLength(value); + _maximumHorizontalCacheLength = value; + } + } + } + + public double VerticalCacheLength + { + get => _maximumVerticalCacheLength; + set + { + if (_maximumVerticalCacheLength != value) + { + ValidateCacheLength(value); + _maximumVerticalCacheLength = value; + } + } + } + + private Rect GetLayoutVisibleWindowDiscardAnchor() + { + var visibleWindow = _visibleWindow; + + if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutVisibleWindow() + { + var visibleWindow = _visibleWindow; + + if (_makeAnchorElement != null) + { + // The anchor is not necessarily laid out yet. Its position should default + // to zero and the layout origin is expected to change once layout is done. + // Until then, we need a window that's going to protect the anchor from + // getting recycled. + visibleWindow = visibleWindow.WithX(0).WithY(0); + } + else if (HasScroller) + { + visibleWindow = new Rect( + visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X, + visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y, + visibleWindow.Width, + visibleWindow.Height); + } + + return visibleWindow; + } + + public Rect GetLayoutRealizationWindow() + { + var realizationWindow = GetLayoutVisibleWindow(); + if (HasScroller) + { + realizationWindow = new Rect( + realizationWindow.X - _horizontalCacheBufferPerSide, + realizationWindow.Y - _verticalCacheBufferPerSide, + realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0, + realizationWindow.Height + _verticalCacheBufferPerSide * 2.0); + } + + return realizationWindow; + } + + public void SetLayoutExtent(Rect extent) + { + _expectedViewportShift = new Point( + _expectedViewportShift.X + _layoutExtent.X - extent.X, + _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); + + // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. + if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) + { + // There are cases where we might be expecting a shift but not get it. We will + // be waiting for the effective viewport event but if the scroll viewer is not able + // to perform the shift (perhaps because it cannot scroll in negative offset), + // then we will end up not realizing elements in the visible + // window. To avoid this, we register to layout updated for this layout pass. If we + // get an effective viewport, we know we have a new viewport and we unregister from + // layout updated. If we get the layout updated handler, then we know that the + // scroller was unable to perform the shift and we invalidate measure and unregister + // from the layout updated event. + if (!_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated += OnLayoutUpdated; + _layoutUpdatedSubscribed = true; + } + } + + _layoutExtent = extent; + _pendingViewportShift = _expectedViewportShift; + + // We just finished a measure pass and have a new extent. + // Let's make sure the scrollers will run its arrange so that they track the anchor. + ((IControl)_scroller)?.InvalidateArrange(); + } + + public Point GetOrigin() => _layoutExtent.TopLeft; + + public void OnLayoutChanged(bool isVirtualizing) + { + _managingViewportDisabled = !isVirtualizing; + + _layoutExtent = default; + _expectedViewportShift = default; + _pendingViewportShift = default; + _unshiftableShift = default; + + _effectiveViewportChangedRevoker?.Dispose(); + + if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + } + + public void OnElementPrepared(IControl element) + { + // If we have an anchor element, we do not want the + // scroll anchor provider to start anchoring some other element. + ////element.CanBeScrollAnchor(true); + } + + public void OnElementCleared(ILayoutable element) + { + ////element.CanBeScrollAnchor(false); + } + + public void OnOwnerMeasuring() + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + EnsureScroller(); + } + + public void OnOwnerArranged() + { + _expectedViewportShift = default; + + if (!_managingViewportDisabled) + { + // This is because of a bug that causes effective viewport to not + // fire if you register during arrange. + // Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport + // EnsureScroller(); + + if (HasScroller) + { + double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0; + double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0; + + bool continueBuildingCache = + _horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide || + _verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide; + + if (continueBuildingCache) + { + _horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + _verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta; + + _horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide); + _verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide); + } + } + } + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + if (_managingViewportDisabled) + { + return; + } + + // We were expecting a viewport shift but we never got one and we are not going to in this + // layout pass. We likely will never get this shift, so lets assume that we are never going to get it and + // adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer + // that can scroll in the direction where the shift is expected. + if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) + { + // Assume this is never going to come. + _unshiftableShift = new Point( + _unshiftableShift.X + _pendingViewportShift.X, + _unshiftableShift.Y + _pendingViewportShift.Y); + _pendingViewportShift = default; + _expectedViewportShift = default; + + TryInvalidateMeasure(); + } + } + + public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) + { + _makeAnchorElement = anchor; + _isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; + } + + public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) + { + if (!_managingViewportDisabled) + { + // During the time between a bring into view request and the element coming into view we do not + // want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the + // element that is being brought into view. We can do this by making just that element as a potential + // anchor candidate and ensure no other element of this repeater is an anchor candidate. + // Once the layout pass is done and we render the frame, the element will be in frame and we can + // switch back to letting the anchor provider pick a suitable anchor. + + // get the targetChild - i.e the immediate child of this repeater that is being brought into view. + // Note that the element being brought into view could be a descendant. + var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + + // Make sure that only the target child can be the anchor during the bring into view operation. + foreach (var child in _owner.Children) + { + ////if (child.CanBeScrollAnchor && child != targetChild) + ////{ + //// child.CanBeScrollAnchor = false; + ////} + } + + // Register to rendering event to go back to how things were before where any child can be the anchor. + _isBringIntoViewInProgress = true; + ////if (!m_renderingToken) + ////{ + //// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; + //// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); + ////} + } + } + + private IControl GetImmediateChildOfRepeater(IControl descendant) + { + var targetChild = descendant; + var parent = descendant.Parent; + while (parent != null && parent != _owner) + { + targetChild = parent; + parent = (IControl)parent.VisualParent; + } + + if (parent == null) + { + throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + } + + return targetChild; + } + + public void ResetScrollers() + { + _scroller = null; + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = null; + _ensuredScroller = false; + } + + private void OnEffectiveViewportChanged(TransformedBounds? bounds) + { + if (!bounds.HasValue) + { + return; + } + + var globalClip = bounds.Value.Clip; + var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; + var clip = globalClip.TransformToAABB(transform); + var effectiveViewport = clip.Intersect(bounds.Value.Bounds); + + UpdateViewport(effectiveViewport); + + _pendingViewportShift = default; + _unshiftableShift = default; + if (_visibleWindow.IsEmpty) + { + // We got cleared. + _layoutExtent = default; + } + + // We got a new viewport, we dont need to wait for layout updated anymore to + // see if our request for a pending shift was handled. + if (_layoutUpdatedSubscribed) + { + _owner.LayoutUpdated -= OnLayoutUpdated; + } + } + + private void EnsureScroller() + { + if (!_ensuredScroller) + { + ResetScrollers(); + + var parent = _owner.GetVisualParent(); + while (parent != null) + { + if (parent is IScrollAnchorProvider scroller) + { + _scroller = scroller; + break; + } + + parent = parent.VisualParent; + } + + if (_scroller == null) + { + // We usually update the viewport in the post arrange handler. But, since we don't have + // a scroller, let's do it now. + UpdateViewport(Rect.Empty); + } + else if (!_managingViewportDisabled) + { + _effectiveViewportChangedRevoker?.Dispose(); + _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + } + + _ensuredScroller = true; + } + } + + private void UpdateViewport(Rect viewport) + { + var currentVisibleWindow = viewport; + + if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && + -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) + { + // We got cleared. + _visibleWindow = default; + } + else + { + _visibleWindow = currentVisibleWindow; + } + + TryInvalidateMeasure(); + } + + private static void ValidateCacheLength(double cacheLength) + { + if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength)) + { + throw new ArgumentException("The maximum cache length must be equal or superior to zero."); + } + } + + private void TryInvalidateMeasure() + { + // Don't invalidate measure if we have an invalid window. + if (!_visibleWindow.IsEmpty) + { + // We invalidate measure instead of just invalidating arrange because + // we don't invalidate measure in UpdateViewport if the view is changing to + // avoid layout cycles. + _owner.InvalidateMeasure(); + } + } + + private IDisposable SubscribeToEffectiveViewportChanged(IControl control) + { + // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - + // we can get this from TransformedBounds, but this property is updated after layout has + // run, resulting in the UI being updated too late when scrolling quickly. We can + // partially remedey this by triggering also on Bounds changes, but this won't work so + // well for nested ItemsRepeaters. + // + // UWP uses the EffectiveBoundsChanged event (which I think was implemented specially + // for this case): we need to implement that in Avalonia. + return control.GetObservable(Visual.TransformedBoundsProperty) + .Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) + .Skip(1) + .Subscribe(OnEffectiveViewportChanged); + } + + private class ScrollerInfo + { + public ScrollerInfo(ScrollViewer scroller) + { + Scroller = scroller; + } + + public ScrollViewer Scroller { get; } + } + }; +} diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs new file mode 100644 index 0000000000..eb30c1b7cf --- /dev/null +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -0,0 +1,118 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Controls +{ + internal enum ElementOwner + { + // All elements are originally owned by the view generator. + ElementFactory, + // Ownership is transferred to the layout when it calls GetElement. + Layout, + // Ownership is transferred to the pinned pool if the element is cleared (outside of + // a 'remove' collection change of course). + PinnedPool, + // Ownership is transfered to the reset pool if the element is cleared by a reset and + // the data source supports unique ids. + UniqueIdResetPool, + // Ownership is transfered to the animator if the element is cleared due to a + // 'remove'-like collection change. + Animator + } + + internal class VirtualizationInfo + { + private int _pinCounter; + private object _data; + + public Rect ArrangeBounds { get; set; } + public bool AutoRecycleCandidate { get; set; } + public int Index { get; private set; } + public bool IsPinned => _pinCounter > 0; + public bool IsHeldByLayout => Owner == ElementOwner.Layout; + public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; + public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; + public bool KeepAlive { get; set; } + public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; + public string UniqueId { get; private set; } + + public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId) + { + Owner = ElementOwner.Layout; + Index = index; + UniqueId = uniqueId; + } + + public void MoveOwnershipToLayoutFromUniqueIdResetPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToLayoutFromPinnedPool() + { + Owner = ElementOwner.Layout; + } + + public void MoveOwnershipToElementFactory() + { + Owner = ElementOwner.ElementFactory; + _pinCounter = 0; + Index = -1; + UniqueId = string.Empty; + ArrangeBounds = ItemsRepeater.InvalidRect; + } + + public void MoveOwnershipToUniqueIdResetPoolFromLayout() + { + Owner = ElementOwner.UniqueIdResetPool; + // Keep the pinCounter the same. If the container survives the reset + // it can go on being pinned as if nothing happened. + } + + public void MoveOwnershipToAnimator() + { + // During a unique id reset, some elements might get removed. + // Their ownership will go from the UniqueIdResetPool to the Animator. + // The common path though is for ownership to go from Layout to Animator. + Owner = ElementOwner.Animator; + Index = -1; + _pinCounter = 0; + } + + public void MoveOwnershipToPinnedPool() + { + Owner = ElementOwner.PinnedPool; + } + + public int AddPin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't pin an unrealized element."); + } + + return ++_pinCounter; + } + + public int RemovePin() + { + if (!IsRealized) + { + throw new InvalidOperationException("You can't unpin an unrealized element."); + } + + if (!IsPinned) + { + throw new InvalidOperationException("UnpinElement was called more often than PinElement."); + } + + return --_pinCounter; + } + + public void UpdateIndex(int newIndex) => Index = newIndex; + } +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 264b1fd2ce..c9b5cbb75b 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -11,7 +11,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl, IScrollable + public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { /// /// Defines the property. @@ -333,6 +333,9 @@ namespace Avalonia.Controls get { return _viewport.Height; } } + /// + IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -373,6 +376,16 @@ namespace Avalonia.Controls control.SetValue(VerticalScrollBarVisibilityProperty, value); } + void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + + void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) + { + // TODO: Implement + } + internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index bc4733296b..9eaa246434 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c29faa1b4d..bd3441078d 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -1,8 +1,9 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// This source file is adapted from the Windows Presentation Foundation project. +// (https://github.com/dotnet/wpf/) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Linq; using Avalonia.Input; using Avalonia.Layout; @@ -17,13 +18,13 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackLayout.SpacingProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackLayout.OrientationProperty.AddOwner(); /// /// Initializes static members of the class. @@ -155,106 +156,122 @@ namespace Avalonia.Controls } /// - /// Measures the control. + /// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content). + /// Children in this dimension are encouraged to be as large as they like. In the other dimension, + /// StackPanel will assume the maximum size of its children. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size availableSize) { - double childAvailableWidth = double.PositiveInfinity; - double childAvailableHeight = double.PositiveInfinity; + Size stackDesiredSize = new Size(); + var children = Children; + Size layoutSlotSize = availableSize; + bool fHorizontal = (Orientation == Orientation.Horizontal); + double spacing = Spacing; + bool hasVisibleChild = false; - if (Orientation == Orientation.Vertical) + // + // Initialize child sizing and iterator data + // Allow children as much size as they want along the stack. + // + if (fHorizontal) { - childAvailableWidth = availableSize.Width; - - if (!double.IsNaN(Width)) - { - childAvailableWidth = Width; - } - - childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth); - childAvailableWidth = Math.Max(childAvailableWidth, MinWidth); + layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity); } else { - childAvailableHeight = availableSize.Height; + layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity); + } - if (!double.IsNaN(Height)) - { - childAvailableHeight = Height; - } + // + // Iterate through children. + // While we still supported virtualization, this was hidden in a child iterator (see source history). + // + for (int i = 0, count = children.Count; i < count; ++i) + { + // Get next child. + var child = children[i]; - childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight); - childAvailableHeight = Math.Max(childAvailableHeight, MinHeight); - } + if (child == null) + { continue; } - double measuredWidth = 0; - double measuredHeight = 0; - double spacing = Spacing; - bool hasVisibleChild = Children.Any(c => c.IsVisible); + bool isVisible = child.IsVisible; - foreach (Control child in Children) - { - child.Measure(new Size(childAvailableWidth, childAvailableHeight)); - Size size = child.DesiredSize; + if (isVisible && !hasVisibleChild) + { + hasVisibleChild = true; + } - if (Orientation == Orientation.Vertical) + // Measure the child. + child.Measure(layoutSlotSize); + Size childDesiredSize = child.DesiredSize; + + // Accumulate child size. + if (fHorizontal) { - measuredHeight += size.Height + (child.IsVisible ? spacing : 0); - measuredWidth = Math.Max(measuredWidth, size.Width); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width); + stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height)); } else { - measuredWidth += size.Width + (child.IsVisible ? spacing : 0); - measuredHeight = Math.Max(measuredHeight, size.Height); + stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width)); + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height); } } - if (Orientation == Orientation.Vertical) + if (fHorizontal) { - measuredHeight -= (hasVisibleChild ? spacing : 0); + stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0)); } else - { - measuredWidth -= (hasVisibleChild ? spacing : 0); + { + stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0)); } - return new Size(measuredWidth, measuredHeight).Constrain(availableSize); + return stackDesiredSize; } - /// + /// + /// Content arrangement. + /// + /// Arrange size protected override Size ArrangeOverride(Size finalSize) { - var orientation = Orientation; + var children = Children; + bool fHorizontal = (Orientation == Orientation.Horizontal); + Rect rcChild = new Rect(finalSize); + double previousChildSize = 0.0; var spacing = Spacing; - var finalRect = new Rect(finalSize); - var pos = 0.0; - foreach (Control child in Children) + // + // Arrange and Position Children. + // + for (int i = 0, count = children.Count; i < count; ++i) { - if (!child.IsVisible) - { - continue; - } + var child = children[i]; - double childWidth = child.DesiredSize.Width; - double childHeight = child.DesiredSize.Height; + if (child == null) + { continue; } - if (orientation == Orientation.Vertical) + if (fHorizontal) { - var rect = new Rect(0, pos, childWidth, childHeight) - .Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top); - ArrangeChild(child, rect, finalSize, orientation); - pos += childHeight + spacing; + rcChild = rcChild.WithX(rcChild.X + previousChildSize); + previousChildSize = child.DesiredSize.Width; + rcChild = rcChild.WithWidth(previousChildSize); + rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height)); + previousChildSize += spacing; } else { - var rect = new Rect(pos, 0, childWidth, childHeight) - .Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment); - ArrangeChild(child, rect, finalSize, orientation); - pos += childWidth + spacing; + rcChild = rcChild.WithY(rcChild.Y + previousChildSize); + previousChildSize = child.DesiredSize.Height; + rcChild = rcChild.WithHeight(previousChildSize); + rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width)); + previousChildSize += spacing; } + + ArrangeChild(child, rcChild, finalSize, Orientation); } return finalSize; diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs deleted file mode 100644 index 5ab186261e..0000000000 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object using a . - /// - public class FuncMemberSelector : IMemberSelector - { - private readonly Func _selector; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The selector. - public FuncMemberSelector(Func selector) - { - this._selector = selector; - } - - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - public object Select(object o) - { - return (o is TObject) ? _selector((TObject)o) : default(TMember); - } - } -} diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs deleted file mode 100644 index e1ec42a849..0000000000 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object. - /// - public interface IMemberSelector - { - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - object Select(object o); - } -} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 6b0c48b97b..b9603b91ed 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,12 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive; using System.Reactive.Linq; using Avalonia.LogicalTree; using Avalonia.Media; -using Avalonia.Media.Immutable; using Avalonia.Metadata; namespace Avalonia.Controls @@ -106,6 +103,14 @@ namespace Avalonia.Controls FontWeightProperty, FontSizeProperty, FontStyleProperty); + + Observable.Merge( + TextProperty.Changed, + TextAlignmentProperty.Changed, + FontSizeProperty.Changed, + FontStyleProperty.Changed, + FontWeightProperty.Changed + ).AddClassHandler((x,_) => x.OnTextPropertiesChanged()); } /// @@ -114,18 +119,6 @@ namespace Avalonia.Controls public TextBlock() { _text = string.Empty; - - Observable.Merge( - this.GetObservable(TextProperty).Select(_ => Unit.Default), - this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), - this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), - this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), - this.GetObservable(FontWeightProperty).Select(_ => Unit.Default)) - .Subscribe(_ => - { - InvalidateFormattedText(); - InvalidateMeasure(); - }); } /// @@ -408,5 +401,11 @@ namespace Avalonia.Controls InvalidateFormattedText(); InvalidateMeasure(); } + + private void OnTextPropertiesChanged() + { + InvalidateFormattedText(); + InvalidateMeasure(); + } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 888f4a2013..4514109e12 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,32 +105,21 @@ namespace Avalonia.Controls get => _selectedItem; set { - SetAndRaise(SelectedItemProperty, ref _selectedItem, - (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - backing = val; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - val)); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); - if (val != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != val) - { - _syncingSelectedItems = true; - SelectSingleItem(val); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - }, value); + if (value != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } } } @@ -164,6 +153,48 @@ namespace Avalonia.Controls } } + /// + /// Expands the specified all descendent s. + /// + /// The item to expand. + public void ExpandSubTree(TreeViewItem item) + { + item.IsExpanded = true; + + var panel = item.Presenter.Panel; + + if (panel != null) + { + foreach (var child in panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + ExpandSubTree(treeViewItem); + } + } + } + } + + /// + /// Selects all items in the . + /// + /// + /// Note that this method only selects nodes currently visible due to their parent nodes + /// being expanded: it does not expand nodes. + /// + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } + + /// + /// Deselects all items in the . + /// + public void UnselectAll() + { + SelectedItems.Clear(); + } + /// /// Subscribes to the CollectionChanged event, if any. /// @@ -409,7 +440,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SelectAll(); e.Handled = true; } } @@ -479,7 +510,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } @@ -490,11 +522,13 @@ namespace Avalonia.Controls /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var item = ItemContainerGenerator.Index.ItemFromContainer(container); @@ -515,7 +549,14 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!toggle && !range) + if (rightButton) + { + if (!SelectedItems.Contains(item)) + { + SelectSingleItem(item); + } + } + else if (!toggle && !range) { SelectSingleItem(item); } @@ -684,6 +725,7 @@ namespace Avalonia.Controls /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -692,13 +734,14 @@ namespace Avalonia.Controls IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 6f53d853c7..3acf341c35 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -4,6 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; using static System.Math; diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 3b6d071583..a7a94130ea 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(instance) .Bind().ToConstant(threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 0f55d42e33..1df0f3a097 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,23 +1,24 @@ - - - - - - + - - - + + + + + + + + + Hold Ctrl+Shift over a control to inspect. - + Focused: - - + + Pointer Over: - + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index e0bacf326b..cc3c545d84 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -1,10 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,22 +17,22 @@ using Avalonia.VisualTree; namespace Avalonia { - public static class DevToolsExtensions - { - public static void AttachDevTools(this TopLevel control) - { - Avalonia.Diagnostics.DevTools.Attach(control); - } - } + public static class DevToolsExtensions + { + public static void AttachDevTools(this TopLevel control) + { + Diagnostics.DevTools.Attach(control); + } + } } namespace Avalonia.Diagnostics { - public class DevTools : UserControl + public class DevTools : UserControl { - private static Dictionary s_open = new Dictionary(); - private static HashSet s_visualTreeRoots = new HashSet(); - private IDisposable _keySubscription; + private static readonly Dictionary s_open = new Dictionary(); + private static readonly HashSet s_visualTreeRoots = new HashSet(); + private readonly IDisposable _keySubscription; public DevTools(IControl root) { @@ -46,7 +48,6 @@ namespace Avalonia.Diagnostics // HACK: needed for XAMLIL, will fix that later public DevTools() { - } public IControl Root { get; } @@ -64,9 +65,8 @@ namespace Avalonia.Diagnostics if (e.Key == Key.F12) { var control = (TopLevel)sender; - var devToolsWindow = default(Window); - if (s_open.TryGetValue(control, out devToolsWindow)) + if (s_open.TryGetValue(control, out var devToolsWindow)) { devToolsWindow.Activate(); } @@ -79,10 +79,8 @@ namespace Avalonia.Diagnostics Width = 1024, Height = 512, Content = devTools, - DataTemplates = - { - new ViewLocator(), - } + DataTemplates = { new ViewLocator() }, + Title = "Avalonia DevTools" }; devToolsWindow.Closed += devTools.DevToolsClosed; @@ -114,7 +112,6 @@ namespace Avalonia.Diagnostics if ((e.Modifiers) == modifiers) { - var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) .FirstOrDefault(); diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Models/EventChainLink.cs index aab50a13dd..464187a048 100644 --- a/src/Avalonia.Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Models/EventChainLink.cs @@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models { Contract.Requires(handler != null); - this.Handler = handler; - this.Handled = handled; - this.Route = route; + Handler = handler; + Handled = handled; + Route = route; } public object Handler { get; } @@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models { return named.Name + " (" + Handler.GetType().Name + ")"; } + return Handler.GetType().Name; } } diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index b107338aec..a66703301d 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -7,7 +7,7 @@ using Avalonia.Controls.Templates; namespace Avalonia.Diagnostics { - public class ViewLocator : IDataTemplate + internal class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; @@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics return data is TViewModel; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs index d723890196..4b832f7ce6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IEnumerable Classes - { - get; - private set; - } - - public IEnumerable Properties - { - get; - private set; - } + public IEnumerable Properties { get; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index c6d3f02e8b..9f524a21eb 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Collections.ObjectModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; @@ -10,21 +11,23 @@ namespace Avalonia.Diagnostics.ViewModels { internal class DevToolsViewModel : ViewModelBase { - private ViewModelBase _content; - private int _selectedTab; - private TreePageViewModel _logicalTree; - private TreePageViewModel _visualTree; - private EventsViewModel _eventsView; + private IDevToolViewModel _selectedTool; private string _focusedControl; private string _pointerOverElement; public DevToolsViewModel(IControl root) { - _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); - _eventsView = new EventsViewModel(root); + Tools = new ObservableCollection + { + new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), + new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), + new EventsViewModel(root) + }; + + SelectedTool = Tools.First(); UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) @@ -33,58 +36,33 @@ namespace Avalonia.Diagnostics.ViewModels } }; - SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } - public ViewModelBase Content + public IDevToolViewModel SelectedTool { - get { return _content; } - private set { RaiseAndSetIfChanged(ref _content, value); } + get => _selectedTool; + set => RaiseAndSetIfChanged(ref _selectedTool, value); } - public int SelectedTab - { - get { return _selectedTab; } - set - { - _selectedTab = value; - - switch (value) - { - case 0: - Content = _logicalTree; - break; - case 1: - Content = _visualTree; - break; - case 2: - Content = _eventsView; - break; - } - - RaisePropertyChanged(); - } - } + public ObservableCollection Tools { get; } public string FocusedControl { - get { return _focusedControl; } - private set { RaiseAndSetIfChanged(ref _focusedControl, value); } + get => _focusedControl; + private set => RaiseAndSetIfChanged(ref _focusedControl, value); } public string PointerOverElement { - get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + get => _pointerOverElement; + private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); } public void SelectControl(IControl control) { - var tree = Content as TreePageViewModel; - - if (tree != null) + if (SelectedTool is TreePageViewModel tree) { tree.SelectControl(control); } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs index 0674918400..7e38749a6f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventOwnerTreeNode : EventTreeNodeBase { - private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[] + private static readonly RoutedEvent[] s_defaultEvents = { - Button.ClickEvent, - InputElement.KeyDownEvent, - InputElement.KeyUpEvent, - InputElement.TextInputEvent, - InputElement.PointerReleasedEvent, - InputElement.PointerPressedEvent, + Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent, + InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent }; public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) : base(null, type.Name) { - this.Children = new AvaloniaList(events.OrderBy(e => e.Name) + Children = new AvaloniaList(events.OrderBy(e => e.Name) .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) })); - this.IsExpanded = true; + IsExpanded = true; } public override bool? IsEnabled @@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels if (base.IsEnabled != value) { base.IsEnabled = value; + if (_updateChildren && value != null) { foreach (var child in Children) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs index 7ece790310..36f1904253 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; using Avalonia.Threading; @@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels { internal class EventTreeNode : EventTreeNodeBase { - private RoutedEvent _event; - private EventsViewModel _parentViewModel; + private readonly RoutedEvent _event; + private readonly EventsViewModel _parentViewModel; private bool _isRegistered; private FiredEvent _currentEvent; @@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(@event != null); Contract.Requires(vm != null); - this._event = @event; - this._parentViewModel = vm; + _event = @event; + _parentViewModel = vm; } public override bool? IsEnabled diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs index 146a8cea8e..4be4d8f74e 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs @@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels private bool _isExpanded; private bool? _isEnabled = false; - public EventTreeNodeBase(EventTreeNodeBase parent, string text) + protected EventTreeNodeBase(EventTreeNodeBase parent, string text) { - this.Parent = parent; - this.Text = text; + Parent = parent; + Text = text; } public IAvaloniaReadOnlyList Children @@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public virtual bool? IsEnabled { - get { return _isEnabled; } - set { RaiseAndSetIfChanged(ref _isEnabled, value); } + get => _isEnabled; + set => RaiseAndSetIfChanged(ref _isEnabled, value); } public EventTreeNodeBase Parent @@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Text { get; - private set; } internal void UpdateChecked() @@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels { if (Children == null) return false; + bool? value = false; + for (int i = 0; i < Children.Count; i++) { if (i == 0) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs index a23677afc8..1c868148ce 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -5,8 +5,6 @@ using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; -using System.Windows.Input; - using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Interactivity; @@ -14,21 +12,24 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { - internal class EventsViewModel : ViewModelBase + internal class EventsViewModel : ViewModelBase, IDevToolViewModel { private readonly IControl _root; private FiredEvent _selectedEvent; public EventsViewModel(IControl root) { - this._root = root; - this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + _root = root; + + Nodes = RoutedEventRegistry.Instance.GetAllRegistered() .GroupBy(e => e.OwnerType) .OrderBy(e => e.Key.Name) .Select(g => new EventOwnerTreeNode(g.Key, g, this)) .ToArray(); } + public string Name => "Events"; + public EventTreeNodeBase[] Nodes { get; } public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); @@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + return (bool)value ? Brushes.Green : Brushes.Transparent; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs index 049280c390..daf8ebd0f6 100644 --- a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs @@ -3,7 +3,6 @@ using System; using System.Collections.ObjectModel; - using Avalonia.Diagnostics.Models; using Avalonia.Interactivity; @@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels { internal class FiredEvent : ViewModelBase { - private RoutedEventArgs _eventArgs; + private readonly RoutedEventArgs _eventArgs; private EventChainLink _handledBy; public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator) @@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels Contract.Requires(eventArgs != null); Contract.Requires(originator != null); - this._eventArgs = eventArgs; - this.Originator = originator; + _eventArgs = eventArgs; + Originator = originator; AddToChain(originator); } @@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels if (IsHandled) { return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine + - $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; + $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}"; } + return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}"; } } @@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels public EventChainLink HandledBy { - get { return _handledBy; } + get => _handledBy; set { if (_handledBy != value) diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs new file mode 100644 index 0000000000..0434230a63 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Diagnostics.ViewModels +{ + /// + /// View model interface for tool showing up in DevTools + /// + public interface IDevToolViewModel + { + /// + /// Name of a tool. + /// + string Name { get; } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs index 638cf6c88f..0b9bd85b4f 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels public static LogicalTreeNode[] Create(object control) { - var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; + return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs index 2609b74ce0..523be406c8 100644 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs @@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels Value = diagnostic.Value ?? "(null)"; Priority = (diagnostic.Priority != BindingPriority.Unset) ? diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? "Inherited" : "Unset"; + diagnostic.Property.Inherits ? + "Inherited" : + "Unset"; Diagnostic = diagnostic.Diagnostic; }); } @@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels public string Priority { - get { return _priority; } - private set { RaiseAndSetIfChanged(ref _priority, value); } + get => _priority; + private set => RaiseAndSetIfChanged(ref _priority, value); } public string Diagnostic { - get { return _diagnostic; } - private set { RaiseAndSetIfChanged(ref _diagnostic, value); } + get => _diagnostic; + private set => RaiseAndSetIfChanged(ref _diagnostic, value); } public object Value { - get { return _value; } - private set { RaiseAndSetIfChanged(ref _value, value); } + get => _value; + private set => RaiseAndSetIfChanged(ref _value, value); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..902eb81bd9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs @@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => styleable.Classes.CollectionChanged += x, + x => styleable.Classes.CollectionChanged -= x) + .TakeUntil(styleable.StyleDetach); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) @@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { - get { return _classes; } - private set { RaiseAndSetIfChanged(ref _classes, value); } + get => _classes; + private set => RaiseAndSetIfChanged(ref _classes, value); } public IVisual Visual @@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get { return _isExpanded; } - set { RaiseAndSetIfChanged(ref _isExpanded, value); } + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public TreeNode Parent @@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels public string Type { get; - private set; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index dba44c5d0c..b2b1aaa723 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,21 +6,24 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase + internal class TreePageViewModel : ViewModelBase, IDevToolViewModel { private TreeNode _selected; private ControlDetailsViewModel _details; - public TreePageViewModel(TreeNode[] nodes) + public TreePageViewModel(TreeNode[] nodes, string name) { Nodes = nodes; + Name = name; } + public string Name { get; } + public TreeNode[] Nodes { get; protected set; } public TreeNode SelectedNode { - get { return _selected; } + get => _selected; set { if (RaiseAndSetIfChanged(ref _selected, value)) @@ -32,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels public ControlDetailsViewModel Details { - get { return _details; } - private set { RaiseAndSetIfChanged(ref _details, value); } + get => _details; + private set => RaiseAndSetIfChanged(ref _details, value); } public TreeNode FindNode(IControl control) @@ -63,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { control = control.GetVisualParent(); } - } + } if (node != null) { @@ -87,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels { return node; } - else + + foreach (var child in node.Children) { - foreach (var child in node.Children) - { - var result = FindNode(child, control); + var result = FindNode(child, control); - if (result != null) - { - return result; - } + if (result != null) + { + return result; } } diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs index 00660754c0..a6ff4dd853 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using JetBrains.Annotations; namespace Avalonia.Diagnostics.ViewModels { - public class ViewModelBase : INotifyPropertyChanged + internal class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs index 8c070261d9..47ef91507a 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs @@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels } } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 381b2e04b4..fb867ab55e 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -7,7 +7,6 @@ using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Media; -using Avalonia.Styling; namespace Avalonia.Diagnostics.Views { @@ -15,6 +14,7 @@ namespace Avalonia.Diagnostics.Views { private static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); + private SimpleGrid _grid; public ControlDetailsView() @@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views public ControlDetailsViewModel ViewModel { - get { return GetValue(ViewModelProperty); } + get => GetValue(ViewModelProperty); private set { SetValue(ViewModelProperty, value); @@ -38,48 +38,37 @@ namespace Avalonia.Diagnostics.Views { Func> pt = PropertyTemplate; - Content = new ScrollViewer - { - Content = _grid = new SimpleGrid - { - Styles = - { - new Style(x => x.Is()) - { - Setters = new[] - { - new Setter(MarginProperty, new Thickness(2)), - } - }, - }, - [GridRepeater.TemplateProperty] = pt, - } - }; + Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; } private IEnumerable PropertyTemplate(object i) { var property = (PropertyDetails)i; + var margin = new Thickness(2); + yield return new TextBlock { + Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), + [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() }; yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) - .ToBinding(), + .ToBinding() }; yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), + [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() }; } } diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Views/EventsView.xaml index 8d4d37f7b3..406dd433a2 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Views/EventsView.xaml @@ -2,53 +2,57 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.EventsView"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -