diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 76abcf6912..44d5c239ef 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -4,11 +4,16 @@ Avalonia 0.8.999 Copyright 2019 © The AvaloniaUI Project - https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md - https://github.com/AvaloniaUI/Avalonia/ + https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true CS1591 latest + MIT + https://avatars2.githubusercontent.com/u/14075148?s=200 + Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS. + avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin + https://github.com/AvaloniaUI/Avalonia/releases + git diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 49e9aafc4a..b1b3112e60 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,12 +10,14 @@ HorizontalAlignment="Center" Spacing="16"> - + + + Single Multiple diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index 8a67766c76..cdbf8fd2b6 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; @@ -27,7 +28,7 @@ namespace ControlCatalog.Pages public PageViewModel() { - Items = new ObservableCollection(Enumerable.Range(1, 10).Select(i => GenerateItem())); + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); SelectedItems = new ObservableCollection(); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); @@ -39,16 +40,34 @@ namespace ControlCatalog.Pages Items.Remove(SelectedItems[0]); } }); + + SelectRandomItemCommand = ReactiveCommand.Create(() => + { + var random = new Random(); + + SelectedItem = Items[random.Next(Items.Count - 1)]; + }); } public ObservableCollection Items { get; } + private string _selectedItem; + + public string SelectedItem + { + get { return _selectedItem; } + set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } + } + + public ObservableCollection SelectedItems { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } + public SelectionMode SelectionMode { get => _selectionMode; diff --git a/scripts/ReplaceNugetCache.sh b/scripts/ReplaceNugetCache.sh index 4cc11edd60..e1c0487d60 100755 --- a/scripts/ReplaceNugetCache.sh +++ b/scripts/ReplaceNugetCache.sh @@ -2,7 +2,6 @@ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/ diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 3a3d00b94a..2c321b8b28 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -45,16 +45,17 @@ namespace Avalonia.Animation { get { - if (_transitions == null) + if (_transitions is null) _transitions = new Transitions(); - if (_previousTransitions == null) + if (_previousTransitions is null) _previousTransitions = new Dictionary(); return _transitions; } set { + SetAndRaise(TransitionsProperty, ref _transitions, value); } } @@ -66,18 +67,20 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null) - { - var match = Transitions.FirstOrDefault(x => x.Property == e.Property); + if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; - if (match != null) + // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). + foreach (var transition in Transitions) + { + if (transition.Property == e.Property) { if (_previousTransitions.TryGetValue(e.Property, out var dispose)) dispose.Dispose(); - var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); _previousTransitions[e.Property] = instance; + return; } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 94558c4367..8cc512d132 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -82,6 +82,7 @@ namespace Avalonia set { + VerifyAccess(); if (_inheritanceParent != value) { if (_inheritanceParent != null) @@ -89,25 +90,33 @@ namespace Avalonia _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } - var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); - var inherited = (from property in properties - where property.Inherits - select new - { - Property = property, - Value = GetValue(property), - }).ToList(); - + var oldInheritanceParent = _inheritanceParent; _inheritanceParent = value; + var valuestore = _values; - foreach (var i in inherited) + foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) { - object newValue = GetValue(i.Property); + if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) + { + // if local value set there can be no change + continue; + } + // get the value as it would have been with the previous InheritanceParent + object oldValue; + if (oldInheritanceParent is AvaloniaObject aobj) + { + oldValue = aobj.GetValueOrDefaultUnchecked(property); + } + else + { + oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + + object newValue = GetDefaultValue(property); - if (!Equals(i.Value, newValue)) + if (!Equals(oldValue, newValue)) { - RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 88b0201fcb..d718f5917c 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -26,6 +26,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _initializedCache = new Dictionary>(); + private readonly Dictionary> _inheritedCache = + new Dictionary>(); /// /// Gets the instance @@ -103,6 +105,46 @@ namespace Avalonia return result; } + /// + /// Gets all inherited s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegisteredInherited(Type type) + { + Contract.Requires(type != null); + + if (_inheritedCache.TryGetValue(type, out var result)) + { + return result; + } + + result = new List(); + var visited = new HashSet(); + + foreach (var property in GetRegistered(type)) + { + if (property.Inherits) + { + result.Add(property); + visited.Add(property); + } + } + foreach (var property in GetRegisteredAttached(type)) + { + if (property.Inherits) + { + if (!visited.Contains(property)) + { + result.Add(property); + } + } + } + + _inheritedCache.Add(type, result); + return result; + } + /// /// Gets all s registered on a object. /// @@ -230,6 +272,7 @@ namespace Avalonia _registeredCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } /// @@ -266,6 +309,7 @@ namespace Avalonia _attachedCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } internal void NotifyInitialized(AvaloniaObject o) diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs new file mode 100644 index 0000000000..5fc515f299 --- /dev/null +++ b/src/Avalonia.Base/BoxedValue.cs @@ -0,0 +1,28 @@ +// 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 +{ + /// + /// Represents boxed value of type . + /// + /// Type of stored value. + internal readonly struct BoxedValue + { + public BoxedValue(T value) + { + Boxed = value; + Typed = value; + } + + /// + /// Boxed value. + /// + public object Boxed { get; } + + /// + /// Typed value. + /// + public T Typed { get; } + } +} diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index d4a47306a7..95add0dfac 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using Avalonia.Data; using Avalonia.Threading; @@ -10,9 +11,9 @@ namespace Avalonia /// /// A registered binding in a . /// - internal class PriorityBindingEntry : IDisposable + internal class PriorityBindingEntry : IDisposable, IObserver { - private PriorityLevel _owner; + private readonly PriorityLevel _owner; private IDisposable _subscription; /// @@ -85,7 +86,7 @@ namespace Avalonia Description = ((IDescription)binding).Description; } - _subscription = binding.Subscribe(ValueChanged, Completed); + _subscription = binding.Subscribe(this); } /// @@ -96,7 +97,7 @@ namespace Avalonia _subscription?.Dispose(); } - private void ValueChanged(object value) + void IObserver.OnNext(object value) { void Signal() { @@ -132,7 +133,7 @@ namespace Avalonia } } - private void Completed() + void IObserver.OnCompleted() { HasCompleted = true; @@ -145,5 +146,10 @@ namespace Avalonia Dispatcher.UIThread.Post(() => _owner.Completed(this)); } } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 4996420fe7..2871271062 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -24,13 +24,11 @@ namespace Avalonia /// method on the /// owner object is fired with the old and new values. /// - internal class PriorityValue + internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)> { private readonly Type _valueType; 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; @@ -52,7 +50,6 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; - _setAndNotifyCallback = SetAndNotify; } /// @@ -257,10 +254,15 @@ namespace Avalonia _setter = Owner.GetNonDirectDeferredSetter(Property); } - _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + _setter.SetAndNotifyCallback(Property, this, ref _value, newValue); + } + + void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value) + { + SetAndNotify(ref backing, value); } - private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) + private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index eb112e753a..27a502246a 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -68,7 +68,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue; + return GetMetadata(type).DefaultValue.Typed; } /// @@ -164,7 +164,14 @@ namespace Avalonia } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type); + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + private object GetDefaultBoxedValue(Type type) + { + Contract.Requires(type != null); + + return GetMetadata(type).DefaultValue.Boxed; + } [DebuggerHidden] private Func Cast(Func validate) diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index ed01f1bc70..d1a0e2dc53 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -19,26 +19,26 @@ namespace Avalonia /// A validation function. /// The default binding mode. public StyledPropertyMetadata( - TValue defaultValue = default(TValue), + TValue defaultValue = default, Func validate = null, BindingMode defaultBindingMode = BindingMode.Default) : base(defaultBindingMode) { - DefaultValue = defaultValue; + DefaultValue = new BoxedValue(defaultValue); Validate = validate; } /// /// Gets the default value for the property. /// - public TValue DefaultValue { get; private set; } + internal BoxedValue DefaultValue { get; private set; } /// /// Gets the validation callback. /// public Func Validate { get; private set; } - object IStyledPropertyMetadata.DefaultValue => DefaultValue; + object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; Func IStyledPropertyMetadata.Validate => Cast(Validate); @@ -47,11 +47,9 @@ namespace Avalonia { base.Merge(baseMetadata, property); - var src = baseMetadata as StyledPropertyMetadata; - - if (src != null) + if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue == null) + if (DefaultValue.Boxed == null) { DefaultValue = src.DefaultValue; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index fd7a66fb52..fe9b0e58a0 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -5,15 +5,6 @@ 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. @@ -70,14 +61,14 @@ namespace Avalonia.Utilities return false; } - public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + public bool SetAndNotifyCallback(AvaloniaProperty property, ISetAndNotifyHandler setAndNotifyHandler, ref TValue backing, TValue value) where TValue : TSetRecord { if (!_isNotifying) { using (new NotifyDisposable(this)) { - setAndNotifyCallback(property, ref backing, value); + setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value); } if (!_pendingValues.Empty) @@ -86,7 +77,7 @@ namespace Avalonia.Utilities { while (!_pendingValues.Empty) { - setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue()); } } } @@ -119,4 +110,19 @@ namespace Avalonia.Utilities } } } + + /// + /// Handler for set and notify requests. + /// + /// Value type. + internal interface ISetAndNotifyHandler + { + /// + /// Handles deferred setter requests to set a value. + /// + /// Property being set. + /// Backing field reference. + /// New value. + void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value); + } } diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 7b76457049..d85eb4cd76 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities /// public static class TypeUtilities { - private static int[] Conversions = + private static readonly int[] Conversions = { 0b101111111111101, // Boolean 0b100001111111110, // Char @@ -32,7 +32,7 @@ namespace Avalonia.Utilities 0b111111111111111, // String }; - private static int[] ImplicitConversions = + private static readonly int[] ImplicitConversions = { 0b000000000000001, // Boolean 0b001110111100010, // Char @@ -51,7 +51,7 @@ namespace Avalonia.Utilities 0b100000000000000, // String }; - private static Type[] InbuiltTypes = + private static readonly Type[] InbuiltTypes = { typeof(Boolean), typeof(Char), @@ -70,7 +70,7 @@ namespace Avalonia.Utilities typeof(String), }; - private static readonly Type[] NumericTypes = new[] + private static readonly Type[] NumericTypes = { typeof(Byte), typeof(Decimal), @@ -188,8 +188,7 @@ namespace Avalonia.Utilities } } - var cast = from.GetRuntimeMethods() - .FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to); + var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit); if (cast != null) { @@ -253,8 +252,7 @@ namespace Avalonia.Utilities } } - var cast = from.GetRuntimeMethods() - .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to); + var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit); if (cast != null) { @@ -335,5 +333,41 @@ namespace Avalonia.Utilities return NumericTypes.Contains(type); } } + + [Flags] + private enum OperatorType + { + Implicit = 1, + Explicit = 2 + } + + private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType) + { + const string implicitName = "op_Implicit"; + const string explicitName = "op_Explicit"; + + bool allowImplicit = (operatorType & OperatorType.Implicit) != 0; + bool allowExplicit = (operatorType & OperatorType.Explicit) != 0; + + foreach (MethodInfo method in fromType.GetMethods()) + { + if (!method.IsSpecialName || method.ReturnType != toType) + { + continue; + } + + if (allowImplicit && method.Name == implicitName) + { + return method; + } + + if (allowExplicit && method.Name == explicitName) + { + return method; + } + } + + return null; + } } } diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 16f17ae1bd..02d7890404 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -1,11 +1,13 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Metadata; namespace Avalonia.Controls @@ -39,12 +41,9 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalContentAlignment)); - /// - /// Initializes static members of the class. - /// static ContentControl() { - ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); + ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); } /// @@ -95,20 +94,39 @@ namespace Avalonia.Controls } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_ContentPresenter") { Presenter = presenter; + return true; + } + + return false; + } + + private void ContentChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 902e55bde6..0fe7291835 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -302,19 +302,6 @@ namespace Avalonia.Controls /// The details of the containers. protected virtual void OnContainersRecycled(ItemContainerEventArgs e) { - var toRemove = new List(); - - foreach (var container in e.Containers) - { - // If the item is its own container, then it will be removed from the logical tree - // when it is removed from the Items collection. - if (container?.ContainerControl != container?.Item) - { - toRemove.Add(container.ContainerControl); - } - } - - LogicalChildren.RemoveAll(toRemove); } /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f26cd47bcb..449ca18465 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -66,7 +66,11 @@ namespace Avalonia.Controls } /// - public new IList SelectedItems => base.SelectedItems; + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } /// /// Gets or sets the selection mode. diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs deleted file mode 100644 index b826fb982e..0000000000 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Linq; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using Avalonia.Collections; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using Avalonia.LogicalTree; - -namespace Avalonia.Controls.Mixins -{ - /// - /// Adds content control functionality to control classes. - /// - /// - /// The adds behavior to a control which acts as a content - /// control such as and . It - /// keeps the control's logical children in sync with the content being displayed by the - /// control. - /// - public class ContentControlMixin - { - private static Lazy> subscriptions = - new Lazy>(() => - new ConditionalWeakTable()); - - /// - /// Initializes a new instance of the class. - /// - /// The control type. - /// The content property. - /// - /// Given an control of should return the control's - /// logical children collection. - /// - /// - /// The name of the content presenter in the control's template. - /// - public static void Attach( - AvaloniaProperty content, - Func> logicalChildrenSelector, - string presenterName = "PART_ContentPresenter") - where TControl : TemplatedControl - { - Contract.Requires(content != null); - Contract.Requires(logicalChildrenSelector != null); - - void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e) - { - if (s is IControl sender && sender?.TemplatedParent is TControl parent) - { - UpdateLogicalChild( - sender, - logicalChildrenSelector(parent), - e.OldValue, - null); - } - } - - void TemplateApplied(object s, RoutedEventArgs ev) - { - if (s is TControl sender) - { - var e = (TemplateAppliedEventArgs)ev; - var presenter = e.NameScope.Find(presenterName) as IContentPresenter; - - if (presenter != null) - { - presenter.ApplyTemplate(); - - var logicalChildren = logicalChildrenSelector(sender); - var subscription = new CompositeDisposable(); - - presenter.ChildChanging += ChildChanging; - subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging)); - - subscription.Add(presenter - .GetPropertyChangedObservable(ContentPresenter.ChildProperty) - .Subscribe(c => UpdateLogicalChild( - sender, - logicalChildren, - null, - c.NewValue))); - - UpdateLogicalChild( - sender, - logicalChildren, - null, - presenter.GetValue(ContentPresenter.ChildProperty)); - - if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription)) - { - subscription = new CompositeDisposable(previousSubscription, subscription); - subscriptions.Value.Remove(sender); - } - - subscriptions.Value.Add(sender, subscription); - } - } - } - - TemplatedControl.TemplateAppliedEvent.AddClassHandler( - typeof(TControl), - TemplateApplied, - RoutingStrategies.Direct); - - content.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - var logicalChildren = logicalChildrenSelector(sender); - UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue); - } - }); - - Control.TemplatedParentProperty.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl; - logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent); - } - }); - - TemplatedControl.TemplateProperty.Changed.Subscribe(e => - { - if (e.Sender is TControl sender) - { - if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription)) - { - subscription.Dispose(); - subscriptions.Value.Remove(sender); - } - } - }); - } - - private static void UpdateLogicalChild( - IControl control, - IAvaloniaList logicalChildren, - object oldValue, - object newValue) - { - if (oldValue != newValue) - { - if (oldValue is IControl child) - { - logicalChildren.Remove(child); - ((ISetInheritanceParent)child).SetParent(child.Parent); - } - - child = newValue as IControl; - - if (child != null && !logicalChildren.Contains(child)) - { - child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent); - logicalChildren.Add(child); - } - } - } - } -} diff --git a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs b/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs index 768d87c63e..9edf9848fd 100644 --- a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs +++ b/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs @@ -39,6 +39,11 @@ namespace Avalonia.Controls foreach (Control child in children) { + if (!child.IsVisible) + { + continue; + } + double childWidth = child.DesiredSize.Width; double childHeight = child.DesiredSize.Height; diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index b0dfa4185e..98f925cd0c 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -377,7 +377,7 @@ namespace Avalonia.Controls.Platform if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - Menu.Close(); + Menu?.Close(); } } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 1072b21b1b..a5374e7c5a 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -83,7 +84,6 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; - EventHandler _childChanging; private IDataTemplate _dataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); @@ -190,12 +190,10 @@ namespace Avalonia.Controls.Presenters set { SetValue(PaddingProperty, value); } } - /// - event EventHandler IContentPresenter.ChildChanging - { - add => _childChanging += value; - remove => _childChanging -= value; - } + /// + /// Gets the host content control. + /// + internal IContentPresenterHost Host { get; private set; } /// public sealed override void ApplyTemplate() @@ -222,34 +220,16 @@ namespace Avalonia.Controls.Presenters var content = Content; var oldChild = Child; var newChild = CreateChild(); + var logicalChildren = Host?.LogicalChildren ?? LogicalChildren; // Remove the old child if we're not recycling it. if (newChild != oldChild) { + if (oldChild != null) { VisualChildren.Remove(oldChild); - } - - if (oldChild?.Parent == this) - { - // If we're the child's parent then the presenter isn't in a ContentControl's - // template. - LogicalChildren.Remove(oldChild); - } - else if (TemplatedParent != null) - { - // If we're in a ContentControl's template then invoke ChildChanging to let - // ContentControlMixin handle removing the logical child. - _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs( - this, - ChildProperty, - oldChild, - newChild, - BindingPriority.LocalValue)); - } - else if (oldChild != null) - { + logicalChildren.Remove(oldChild); ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent); } } @@ -272,15 +252,11 @@ namespace Avalonia.Controls.Presenters else if (newChild != oldChild) { ((ISetInheritanceParent)newChild).SetParent(this); - Child = newChild; - // If we're in a ContentControl's template then the child's parent will have been - // set by ContentControlMixin in response to Child changing. If not, then we're - // standalone and should make the control our own logical child. - if (newChild.Parent == null && TemplatedParent == null) + if (!logicalChildren.Contains(newChild)) { - LogicalChildren.Add(newChild); + logicalChildren.Add(newChild); } VisualChildren.Add(newChild); @@ -459,7 +435,8 @@ namespace Avalonia.Controls.Presenters private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e) { - (e.NewValue as IContentPresenterHost)?.RegisterContentPresenter(this); + var host = e.NewValue as IContentPresenterHost; + Host = host?.RegisterContentPresenter(this) == true ? host : null; } } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index 78bffec93b..31ab3a21a6 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Presenters @@ -22,16 +20,5 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the content to be displayed by the presenter. /// object Content { get; set; } - - /// - /// Raised when property is about to change. - /// - /// - /// This event should be raised after the child has been removed from the visual tree, - /// but before the property has changed. It is intended for consumption - /// by in order to update the host control's logical - /// children. - /// - event EventHandler ChildChanging; } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs index 3aa7e625ed..4acfba2c71 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; +using Avalonia.LogicalTree; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -18,10 +20,19 @@ namespace Avalonia.Controls.Presenters /// public interface IContentPresenterHost : ITemplatedControl { + /// + /// Gets a collection describing the logical children of the host control. + /// + IAvaloniaList LogicalChildren { get; } + /// /// Registers an with a host control. /// /// The content presenter. - void RegisterContentPresenter(IContentPresenter presenter); + /// + /// True if the content presenter should add its child to the logical children of the + /// host; otherwise false. + /// + bool RegisterContentPresenter(IContentPresenter presenter); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b8b8094582..cd14211075 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -295,11 +295,14 @@ namespace Avalonia.Controls.Presenters /// public override void ScrollIntoView(object item) { - var index = Items.IndexOf(item); - - if (index != -1) + if (Items != null) { - ScrollIntoView(index); + var index = Items.IndexOf(item); + + if (index != -1) + { + ScrollIntoView(index); + } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 98476c9c94..3cf50a7b80 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -29,10 +30,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedContentControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + ContentProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -63,13 +61,29 @@ namespace Avalonia.Controls.Primitives } /// - protected override void RegisterContentPresenter(IContentPresenter presenter) + protected override bool RegisterContentPresenter(IContentPresenter presenter) { - base.RegisterContentPresenter(presenter); + var result = base.RegisterContentPresenter(presenter); if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + result = true; + } + + return result; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index bda426c23b..e0eb0b005f 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + return true; + } + + return false; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index d59be66b2b..533b643ea6 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { @@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives /// static HeaderedSelectingItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - protected virtual void RegisterContentPresenter(IContentPresenter presenter) + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) { if (presenter.Name == "PART_HeaderPresenter") { HeaderPresenter = presenter; + return true; + } + + return false; + } + + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (e.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); } } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index d428952bb9..07348cdf78 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -100,6 +100,12 @@ namespace Avalonia.Controls.Primitives.PopupPositioning ?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); + + if (targetScreen != null && targetScreen.WorkingArea.IsEmpty) + { + return targetScreen.Bounds; + } + return targetScreen?.WorkingArea ?? new Rect(0, 0, double.MaxValue, double.MaxValue); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 44ae89fdbc..cc0c5f52be 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives private bool _syncingSelectedItems; private int _updateCount; private int _updateSelectedIndex; - private IList _updateSelectedItems; + private object _updateSelectedItem; /// /// Initializes static members of the class. @@ -160,7 +160,7 @@ namespace Avalonia.Controls.Primitives else { _updateSelectedIndex = value; - _updateSelectedItems = null; + _updateSelectedItem = null; } } } @@ -183,7 +183,7 @@ namespace Avalonia.Controls.Primitives } else { - _updateSelectedItems = new AvaloniaList(value); + _updateSelectedItem = value; _updateSelectedIndex = int.MinValue; } } @@ -855,11 +855,6 @@ namespace Avalonia.Controls.Primitives _selectedItem = ElementAt(Items, _selectedIndex); RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); - - if (AutoScrollToSelectedItem) - { - ScrollIntoView(_selectedIndex); - } } } @@ -1046,6 +1041,11 @@ namespace Avalonia.Controls.Primitives removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); RaiseEvent(e); } + + if (AutoScrollToSelectedItem && _selectedIndex != -1) + { + ScrollIntoView(_selectedItem); + } } private void UpdateSelectedItems(Action action) @@ -1075,9 +1075,9 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = _updateSelectedIndex; } - else if (_updateSelectedItems != null) + else if (_updateSelectedItem != null) { - SelectedItems = _updateSelectedItems; + SelectedItem = _updateSelectedItem; } } diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index a1d353f135..54afde7da2 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls public static readonly RadioButtonGroupManager Default = new RadioButtonGroupManager(); static readonly ConditionalWeakTable s_registeredVisualRoots = new ConditionalWeakTable(); - + readonly Dictionary>> s_registeredGroups = new Dictionary>>(); @@ -127,13 +127,11 @@ namespace Avalonia.Controls { if (!string.IsNullOrEmpty(GroupName)) { - var manager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root); - if (manager != _groupManager) - { - _groupManager.Remove(this, _groupName); - _groupManager = manager; - manager.Add(this); - } + _groupManager?.Remove(this, _groupName); + + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root); + + _groupManager.Add(this); } base.OnAttachedToVisualTree(e); } @@ -141,9 +139,10 @@ namespace Avalonia.Controls protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - if (!string.IsNullOrEmpty(GroupName) && _groupManager != null) + + if (!string.IsNullOrEmpty(GroupName)) { - _groupManager.Remove(this, _groupName); + _groupManager?.Remove(this, _groupName); } } @@ -152,9 +151,9 @@ namespace Avalonia.Controls string oldGroupName = GroupName; if (newGroupName != oldGroupName) { - if (!string.IsNullOrEmpty(oldGroupName) && _groupManager != null) + if (!string.IsNullOrEmpty(oldGroupName)) { - _groupManager.Remove(this, oldGroupName); + _groupManager?.Remove(this, oldGroupName); } _groupName = newGroupName; if (!string.IsNullOrEmpty(newGroupName)) @@ -181,7 +180,7 @@ namespace Avalonia.Controls .GetVisualChildren() .OfType() .Where(x => x != this); - + foreach (var sibling in siblings) { if (sibling.IsChecked.GetValueOrDefault()) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index bd3441078d..9e087b7fd3 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -251,7 +251,7 @@ namespace Avalonia.Controls { var child = children[i]; - if (child == null) + if (child == null || !child.IsVisible) { continue; } if (fHorizontal) diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index fc2c118132..50bcb034ac 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -9,6 +10,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -16,7 +18,7 @@ namespace Avalonia.Controls /// /// A tab control that displays a tab strip along with the content of the selected tab. /// - public class TabControl : SelectingItemsControl + public class TabControl : SelectingItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -68,10 +70,6 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - ContentControlMixin.Attach( - SelectedContentProperty, - x => x.LogicalChildren, - "PART_SelectedContentHost"); } /// @@ -136,7 +134,31 @@ namespace Avalonia.Controls internal ItemsPresenter ItemsPresenterPart { get; private set; } - internal ContentPresenter ContentPart { get; private set; } + internal IContentPresenter ContentPart { get; private set; } + + /// + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + { + return RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual bool RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_SelectedContentHost") + { + ContentPart = presenter; + return true; + } + + return false; + } protected override IItemContainerGenerator CreateItemContainerGenerator() { @@ -148,8 +170,6 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); ItemsPresenterPart = e.NameScope.Get("PART_ItemsPresenter"); - - ContentPart = e.NameScope.Get("PART_SelectedContentHost"); } /// diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index 1fcfb525cb..037e80e372 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -28,6 +28,11 @@ namespace Avalonia { Diagnostics.DevTools.Attach(control, gesture); } + + public static void OpenDevTools(this TopLevel control) + { + Diagnostics.DevTools.OpenDevTools(control); + } } } @@ -73,7 +78,7 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - private static void OpenDevTools(TopLevel control) + internal static void OpenDevTools(TopLevel control) { if (s_open.TryGetValue(control, out var devToolsWindow)) { diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs index ddf1473b45..1326f718de 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs @@ -70,7 +70,10 @@ namespace Avalonia.Diagnostics.Views private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e) { var item = (TreeViewItem)sender; - var header = item.HeaderPresenter.Child; + var headerPresenter = item.HeaderPresenter; + headerPresenter.ApplyTemplate(); + + var header = headerPresenter.Child; header.PointerEnter += AddAdorner; header.PointerLeave += RemoveAdorner; item.TemplateApplied -= TreeViewItemTemplateApplied; diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 29768513f3..9e4b2b84e0 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -231,15 +231,6 @@ namespace Avalonia.Input } break; - - case Key.F10: - _owner.ShowAccessKeys = _showingAccessKeys = true; - if (MainMenu != null) - { - MainMenu.Open(); - e.Handled = true; - } - break; } } diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index ea4892ebfc..6b06151773 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -39,6 +39,36 @@ namespace Avalonia.Input InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); } + public static void AddTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(TappedEvent, handler); + } + + public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(DoubleTappedEvent, handler); + } + + public static void AddRightTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(RightTappedEvent, handler); + } + + public static void RemoveTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(TappedEvent, handler); + } + + public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(DoubleTappedEvent, handler); + } + + public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(RightTappedEvent, handler); + } + private static void PointerPressed(RoutedEventArgs ev) { if (ev.Route == RoutingStrategies.Bubble) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 4b4ab177b8..47e85416cf 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -565,9 +565,17 @@ namespace Avalonia.Input { IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); - foreach (var child in this.GetVisualChildren().OfType()) + // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ + // will cause extra allocations and overhead. + + var children = VisualChildren; + + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < children.Count; ++i) { - child.UpdateIsEffectivelyEnabled(this); + var child = children[i] as InputElement; + + child?.UpdateIsEffectivelyEnabled(this); } } } diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs index 6dd5337b27..ce2b03e355 100644 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs @@ -1,5 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Reactive.Disposables; +using System.Threading; using Avalonia.Native.Interop; using Avalonia.Rendering; @@ -13,11 +16,27 @@ namespace Avalonia.Native { _window = window; } + public IDisposable TryLock() { if (_window.TryLock()) - return Disposable.Create(() => _window.Unlock()); + return new UnlockDisposable(_window); return null; } + + private sealed class UnlockDisposable : IDisposable + { + private IAvnWindowBase _window; + + public UnlockDisposable(IAvnWindowBase window) + { + _window = window; + } + + public void Dispose() + { + Interlocked.Exchange(ref _window, null)?.Unlock(); + } + } } } diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs index eea695d77e..92b2915e2e 100644 --- a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -8,16 +8,16 @@ using Avalonia.Controls.Platform; namespace Avalonia.Native { - internal class WindowsMountedVolumeInfoListener : IDisposable + internal class MacOSMountedVolumeInfoListener : IDisposable { private readonly CompositeDisposable _disposables; - private readonly ObservableCollection _targetObs; private bool _beenDisposed = false; private ObservableCollection mountedDrives; - public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + public MacOSMountedVolumeInfoListener(ObservableCollection mountedDrives) { this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) @@ -30,7 +30,8 @@ namespace Avalonia.Native private void Poll(long _) { - var mountVolInfos = Directory.GetDirectories("/Volumes") + var mountVolInfos = Directory.GetDirectories("/Volumes/") + .Where(p=> p != null) .Select(p => new MountedVolumeInfo() { VolumeLabel = Path.GetFileName(p), @@ -38,15 +39,15 @@ namespace Avalonia.Native VolumeSizeBytes = 0 }) .ToArray(); - - if (_targetObs.SequenceEqual(mountVolInfos)) + + if (mountedDrives.SequenceEqual(mountVolInfos)) return; else { - _targetObs.Clear(); + mountedDrives.Clear(); foreach (var i in mountVolInfos) - _targetObs.Add(i); + mountedDrives.Add(i); } } @@ -72,7 +73,7 @@ namespace Avalonia.Native public IDisposable Listen(ObservableCollection mountedDrives) { Contract.Requires(mountedDrives != null); - return new WindowsMountedVolumeInfoListener(mountedDrives); + return new MacOSMountedVolumeInfoListener(mountedDrives); } } } diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 2d4a39e026..259874423e 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -1,5 +1,7 @@ +// 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.Disposables; using System.Threading; namespace Avalonia.Rendering @@ -7,7 +9,7 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); - + /// /// Tries to lock the target surface or window /// @@ -15,7 +17,7 @@ namespace Avalonia.Rendering public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) - return Disposable.Create(() => Monitor.Exit(_lock)); + return new UnlockDisposable(_lock); return null; } @@ -25,7 +27,27 @@ namespace Avalonia.Rendering public IDisposable Lock() { Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); + return new UnlockDisposable(_lock); + } + + private sealed class UnlockDisposable : IDisposable + { + private object _lock; + + public UnlockDisposable(object @lock) + { + _lock = @lock; + } + + public void Dispose() + { + object @lock = Interlocked.Exchange(ref _lock, null); + + if (@lock != null) + { + Monitor.Exit(@lock); + } + } } } } diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2f1690184d..11bda8b00e 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -65,9 +65,7 @@ namespace Avalonia /// Second vector /// The dot product public static double operator *(Vector a, Vector b) - { - return a.X * b.X + a.Y * b.Y; - } + => Dot(a, b); /// /// Scales a vector. @@ -76,9 +74,7 @@ namespace Avalonia /// The scaling factor. /// The scaled vector. public static Vector operator *(Vector vector, double scale) - { - return new Vector(vector._x * scale, vector._y * scale); - } + => Multiply(vector, scale); /// /// Scales a vector. @@ -87,14 +83,17 @@ namespace Avalonia /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) - { - return new Vector(vector._x / scale, vector._y / scale); - } + => Divide(vector, scale); /// /// Length of the vector /// - public double Length => Math.Sqrt(X * X + Y * Y); + public double Length => Math.Sqrt(SquaredLength); + + /// + /// Squared Length of the vector + /// + public double SquaredLength => _x * _x + _y * _y; /// /// Negates a vector. @@ -102,9 +101,7 @@ namespace Avalonia /// The vector. /// The negated vector. public static Vector operator -(Vector a) - { - return new Vector(-a._x, -a._y); - } + => Negate(a); /// /// Adds two vectors. @@ -113,9 +110,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the addition. public static Vector operator +(Vector a, Vector b) - { - return new Vector(a._x + b._x, a._y + b._y); - } + => Add(a, b); /// /// Subtracts two vectors. @@ -124,9 +119,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the subtraction. public static Vector operator -(Vector a, Vector b) - { - return new Vector(a._x - b._x, a._y - b._y); - } + => Subtract(a, b); /// /// Check if two vectors are equal (bitwise). @@ -155,7 +148,8 @@ namespace Avalonia public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(null, obj)) + return false; return obj is Vector vector && Equals(vector); } @@ -206,5 +200,131 @@ namespace Avalonia { return new Vector(_x, y); } + + /// + /// Returns a normalized version of this vector. + /// + /// The normalized vector. + public Vector Normalize() + => Normalize(this); + + /// + /// Returns a negated version of this vector. + /// + /// The negated vector. + public Vector Negate() + => Negate(this); + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector a, Vector b) + => a._x * b._x + a._y * b._y; + + /// + /// Returns the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + public static double Cross(Vector a, Vector b) + => a._x * b._y - a._y * b._x; + + /// + /// Normalizes the given vector. + /// + /// The vector + /// The normalized vector. + public static Vector Normalize(Vector vector) + => Divide(vector, vector.Length); + + /// + /// Divides the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Divide(Vector a, Vector b) + => new Vector(a._x / b._x, a._y / b._y); + + /// + /// Divides the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Divide(Vector vector, double scalar) + => new Vector(vector._x / scalar, vector._y / scalar); + + /// + /// Multiplies the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Multiply(Vector a, Vector b) + => new Vector(a._x * b._x, a._y * b._y); + + /// + /// Multiplies the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Multiply(Vector vector, double scalar) + => new Vector(vector._x * scalar, vector._y * scalar); + + /// + /// Adds the second to the first vector + /// + /// The first vector. + /// The second vector. + /// The summed vector. + public static Vector Add(Vector a, Vector b) + => new Vector(a._x + b._x, a._y + b._y); + + /// + /// Subtracts the second from the first vector + /// + /// The first vector. + /// The second vector. + /// The difference vector. + public static Vector Subtract(Vector a, Vector b) + => new Vector(a._x - b._x, a._y - b._y); + + /// + /// Negates the vector + /// + /// The vector to negate. + /// The scaled vector. + public static Vector Negate(Vector vector) + => new Vector(-vector._x, -vector._y); + + /// + /// Returnes the vector (0.0, 0.0) + /// + public static Vector Zero + => new Vector(0, 0); + + /// + /// Returnes the vector (1.0, 1.0) + /// + public static Vector One + => new Vector(1, 1); + + /// + /// Returnes the vector (1.0, 0.0) + /// + public static Vector UnitX + => new Vector(1, 0); + + /// + /// Returnes the vector (0.0, 1.0) + /// + public static Vector UnitY + => new Vector(0, 1); } } diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index 102e027584..a17e6b8b51 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -10,8 +10,7 @@ namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; - private readonly ObservableCollection _targetObs = new ObservableCollection(); + private readonly CompositeDisposable _disposables; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -41,14 +40,14 @@ namespace Avalonia.Win32 }) .ToArray(); - if (_targetObs.SequenceEqual(mountVolInfos)) + if (mountedDrives.SequenceEqual(mountVolInfos)) return; else { - _targetObs.Clear(); + mountedDrives.Clear(); foreach (var i in mountVolInfos) - _targetObs.Add(i); + mountedDrives.Add(i); } } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index ef7dc33f76..03d061e04f 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -984,6 +984,7 @@ namespace Avalonia.Controls.UnitTests TextBox textBox = GetTextBox(control); var window = new Window {Content = control}; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index 93355a22f2..f7332415ac 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -50,6 +50,7 @@ namespace Avalonia.Controls.UnitTests root.Child = target; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); @@ -331,6 +332,45 @@ namespace Avalonia.Controls.UnitTests Assert.Null(textBlock.GetLogicalParent()); } + [Fact] + public void Should_Set_Child_LogicalParent_After_Removing_And_Adding_Back_To_Logical_Tree() + { + using (UnitTestApplication.Start(TestServices.RealStyler)) + { + var target = new ContentControl(); + var root = new TestRoot + { + Styles = + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ContentControl.TemplateProperty, GetTemplate()), + } + } + }, + Child = target + }; + + target.Content = "Foo"; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Equal(target, target.Presenter.Child.LogicalParent); + + root.Child = null; + + Assert.Null(target.Template); + + target.Content = null; + root.Child = target; + target.Content = "Bar"; + + Assert.Equal(target, target.Presenter.Child.LogicalParent); + } + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate((parent, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 522afc9546..ac80fc6c7a 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }.ApplyTemplate(); + var window = new Window { Content = target }; + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); int openedCount = 0; @@ -53,7 +55,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }.ApplyTemplate(); + var window = new Window { Content = target }; + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); sut.Open(target); @@ -86,6 +90,7 @@ namespace Avalonia.Controls.UnitTests var window = new Window {Content = target}; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); _mouse.Click(target, MouseButton.Right); @@ -115,7 +120,8 @@ namespace Avalonia.Controls.UnitTests var window = new Window {Content = target}; window.ApplyTemplate(); - + window.Presenter.ApplyTemplate(); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs deleted file mode 100644 index 638443e17f..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using System.Linq; -using Avalonia.Collections; -using Avalonia.Controls.Mixins; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.LogicalTree; -using Moq; -using Xunit; - -namespace Avalonia.Controls.UnitTests.Mixins -{ - public class ContentControlMixinTests - { - [Fact] - public void Multiple_Mixin_Usages_Should_Not_Throw() - { - var target = new TestControl() - { - Template = new FuncControlTemplate((_, scope) => new Panel - { - Children = - { - new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope), - new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope) - } - }) - }; - - - var ex = Record.Exception(() => target.ApplyTemplate()); - - Assert.Null(ex); - } - - [Fact] - public void Replacing_Template_Releases_Events() - { - var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; - var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - var target = new TestControl - { - Template = new FuncControlTemplate((_, scope) => new Panel - { - Children = - { - p1.RegisterInNameScope(scope), - p2.RegisterInNameScope(scope) - } - }) - }; - target.ApplyTemplate(); - - Control tc; - - p1.Content = tc = new Control(); - p1.UpdateChild(); - Assert.Contains(tc, target.GetLogicalChildren()); - - p2.Content = tc = new Control(); - p2.UpdateChild(); - Assert.Contains(tc, target.GetLogicalChildren()); - - target.Template = null; - - p1.Content = tc = new Control(); - p1.UpdateChild(); - Assert.DoesNotContain(tc, target.GetLogicalChildren()); - - p2.Content = tc = new Control(); - p2.UpdateChild(); - Assert.DoesNotContain(tc, target.GetLogicalChildren()); - - } - - private class TestControl : TemplatedControl - { - public static readonly StyledProperty Content1Property = - AvaloniaProperty.Register(nameof(Content1)); - - public static readonly StyledProperty Content2Property = - AvaloniaProperty.Register(nameof(Content2)); - - static TestControl() - { - ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); - ContentControlMixin.Attach(Content2Property, x => x.LogicalChildren, "Content_2_Presenter"); - } - - public object Content1 - { - get { return GetValue(Content1Property); } - set { SetValue(Content1Property, value); } - } - - public object Content2 - { - get { return GetValue(Content2Property); } - set { SetValue(Content2Property, value); } - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 6ab9c345d4..6d6dfc9230 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -343,6 +343,19 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); } + [Fact] + public void Should_Clear_Host_When_Host_Template_Cleared() + { + var (target, host) = CreateTarget(); + + Assert.Same(host, target.Host); + + host.Template = null; + host.ApplyTemplate(); + + Assert.Null(target.Host); + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 0ebe6833d3..f75f6fcf91 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -51,6 +51,7 @@ namespace Avalonia.Controls.UnitTests.Primitives window.Content = target; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); @@ -167,6 +168,7 @@ namespace Avalonia.Controls.UnitTests.Primitives window.Content = target; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bc002174ec..4e4d92afdc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -894,6 +894,53 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Qux", target.SelectedItem); } + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var raised = false; + target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); + + target.SelectedIndex = 2; + + Assert.True(raised); + } + + [Fact] + public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() + { + // Issue #2969. + var target = new ListBox(); + var selectedItems = new List(); + + target.BeginInit(); + target.Template = Template(); + target.Items = new[] { "Foo", "Bar", "Baz" }; + target.SelectedItems = selectedItems; + target.SelectedItem = "Bar"; + target.EndInit(); + + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(selectedItems, target.SelectedItems); + Assert.Equal(new[] { "Bar" }, selectedItems); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index db113f0569..984734d414 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -332,6 +332,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren); } + [Theory] + [InlineData(Orientation.Horizontal)] + [InlineData(Orientation.Vertical)] + public void Only_Arrange_Visible_Children(Orientation orientation) + { + + var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false }; + var panel = new Panel { Width = 10, Height = 10 }; + + var target = new StackPanel + { + Spacing = 40, + Orientation = orientation, + Children = + { + hiddenPanel, + panel + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.Equal(new Rect(0, 0, 10, 10), panel.Bounds); + } + private class TestControl : Control { public Size MeasureConstraint { get; private set; } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ee8d9cc62e..ddf7e7a0fa 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -183,27 +183,27 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); var dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((Button)target.ContentPart.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = ((TextBlock)target.ContentPart.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); dataContext = target.ContentPart.DataContext; Assert.Equal("Base", dataContext); } @@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - target.ContentPart.UpdateChild(); + ((ContentPresenter)target.ContentPart).UpdateChild(); var content = Assert.IsType(target.ContentPart.Child); Assert.Equal("bar", content.Tag); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 901d780f16..645f87163a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -202,8 +202,9 @@ namespace Avalonia.Controls.UnitTests target.Template = CreateTemplate(); target.Content = child; + target.ApplyTemplate(); - Assert.Throws(() => target.ApplyTemplate()); + Assert.Throws(() => target.Presenter.ApplyTemplate()); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index b1abc9ea54..77bb215ad5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -142,6 +142,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.DataContext = new { Foo = "foo" }; window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); Assert.Equal("foo", border.DataContext); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 86b874f75c..f8678ee22e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -73,6 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var button = window.FindControl