diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 1a9347e317..f27bb5fac6 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Primitives; +using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -14,7 +15,7 @@ namespace Avalonia.Controls.Notifications /// /// An that displays notifications in a . /// - public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager + public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest { private IList _items; @@ -153,5 +154,7 @@ namespace Avalonia.Controls.Notifications adornerLayer?.Children.Add(this); } + + public bool HitTest(Point point) => VisualChildren.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index ebe5e0a93e..9a2f0310d7 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -138,10 +138,7 @@ namespace Avalonia.Controls.Primitives } } - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + public bool HitTest(Point point) => Children.HitTestCustom(point); private class AdornedElementInfo { diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 487a5e91e4..5150033a53 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -21,11 +21,8 @@ namespace Avalonia.Controls.Primitives return null; } - - public bool HitTest(Point point) - { - return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); - } + + public bool HitTest(Point point) => Children.HitTestCustom(point); protected override Size ArrangeOverride(Size finalSize) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a5bbcec186..69da211aa4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -240,17 +241,14 @@ namespace Avalonia.Controls.Primitives public override void BeginInit() { base.BeginInit(); - ++_updateCount; - _updateSelectedIndex = int.MinValue; + + InternalBeginInit(); } /// public override void EndInit() { - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); base.EndInit(); } @@ -437,7 +435,8 @@ namespace Avalonia.Controls.Primitives protected override void OnDataContextBeginUpdate() { base.OnDataContextBeginUpdate(); - ++_updateCount; + + InternalBeginInit(); } /// @@ -445,10 +444,7 @@ namespace Avalonia.Controls.Primitives { base.OnDataContextEndUpdate(); - if (--_updateCount == 0) - { - UpdateFinished(); - } + InternalEndInit(); } protected override void OnKeyDown(KeyEventArgs e) @@ -1118,6 +1114,26 @@ namespace Avalonia.Controls.Primitives } } + private void InternalBeginInit() + { + if (_updateCount == 0) + { + _updateSelectedIndex = int.MinValue; + } + + ++_updateCount; + } + + private void InternalEndInit() + { + Debug.Assert(_updateCount > 0); + + if (--_updateCount == 0) + { + UpdateFinished(); + } + } + private class Selection : IEnumerable { private readonly List _list = new List(); diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index e7cd0697a0..4b3e8e2110 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -7,8 +7,14 @@ using Avalonia.Data; namespace Avalonia.Controls.Primitives { + /// + /// Represents a control that a user can select (check) or clear (uncheck). Base class for controls that can switch states. + /// public class ToggleButton : Button { + /// + /// Defines the property. + /// public static readonly DirectProperty IsCheckedProperty = AvaloniaProperty.RegisterDirect( nameof(IsChecked), @@ -17,9 +23,30 @@ namespace Avalonia.Controls.Primitives unsetValue: null, defaultBindingMode: BindingMode.TwoWay); + /// + /// Defines the property. + /// public static readonly StyledProperty IsThreeStateProperty = AvaloniaProperty.Register(nameof(IsThreeState)); + /// + /// Defines the event. + /// + public static readonly RoutedEvent CheckedEvent = + RoutedEvent.Register(nameof(Checked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent UncheckedEvent = + RoutedEvent.Register(nameof(Unchecked), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IndeterminateEvent = + RoutedEvent.Register(nameof(Indeterminate), RoutingStrategies.Bubble); + private bool? _isChecked = false; static ToggleButton() @@ -27,14 +54,49 @@ namespace Avalonia.Controls.Primitives PseudoClass(IsCheckedProperty, c => c == true, ":checked"); PseudoClass(IsCheckedProperty, c => c == false, ":unchecked"); PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate"); + + IsCheckedProperty.Changed.AddClassHandler((x, e) => x.OnIsCheckedChanged(e)); + } + + /// + /// Raised when a is checked. + /// + public event EventHandler Checked + { + add => AddHandler(CheckedEvent, value); + remove => RemoveHandler(CheckedEvent, value); + } + + /// + /// Raised when a is unchecked. + /// + public event EventHandler Unchecked + { + add => AddHandler(UncheckedEvent, value); + remove => RemoveHandler(UncheckedEvent, value); + } + + /// + /// Raised when a is neither checked nor unchecked. + /// + public event EventHandler Indeterminate + { + add => AddHandler(IndeterminateEvent, value); + remove => RemoveHandler(IndeterminateEvent, value); } + /// + /// Gets or sets whether the is checked. + /// public bool? IsChecked { - get { return _isChecked; } - set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + get => _isChecked; + set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + /// + /// Gets or sets a value that indicates whether the control supports three states. + /// public bool IsThreeState { get => GetValue(IsThreeStateProperty); @@ -47,18 +109,78 @@ namespace Avalonia.Controls.Primitives base.OnClick(); } + /// + /// Toggles the property. + /// protected virtual void Toggle() { if (IsChecked.HasValue) + { if (IsChecked.Value) + { if (IsThreeState) + { IsChecked = null; + } else + { IsChecked = false; + } + } else + { IsChecked = true; + } + } else + { IsChecked = false; + } + } + + /// + /// Called when becomes true. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnChecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes false. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnUnchecked(RoutedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when becomes null. + /// + /// Event arguments for the routed event that is raised by the default implementation of this method. + protected virtual void OnIndeterminate(RoutedEventArgs e) + { + RaiseEvent(e); + } + + private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool?)e.NewValue; + + switch (newValue) + { + case true: + OnChecked(new RoutedEventArgs(CheckedEvent)); + break; + case false: + OnUnchecked(new RoutedEventArgs(UncheckedEvent)); + break; + default: + OnIndeterminate(new RoutedEventArgs(IndeterminateEvent)); + break; + } } } } diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs index 7199053b08..4c15de0312 100644 --- a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs +++ b/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + namespace Avalonia.Rendering { /// @@ -9,4 +13,13 @@ namespace Avalonia.Rendering { bool HitTest(Point point); } + + public static class CustomSimpleHitTestExtensions + { + public static bool HitTestCustom(this IVisual visual, Point point) + => (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; + + public static bool HitTestCustom(this IEnumerable children, Point point) + => children.Any(ctrl => ctrl.HitTestCustom(point)); + } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index e5bbebdec2..8c16dd0f70 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -129,6 +129,23 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void SelectedIndex_Should_Be_Minus_1_Without_Initialize() + { + var items = new[] + { + new Item(), + new Item(), + }; + + var target = new ListBox(); + target.Items = items; + target.Template = Template(); + target.DataContext = new object(); + + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void SelectedIndex_Should_Be_0_After_Initialize_With_AlwaysSelected() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs index 4f4ab47b0a..9acd42aba6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -1,5 +1,4 @@ using Avalonia.Data; -using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -63,6 +62,54 @@ namespace Avalonia.Controls.Primitives.UnitTests Assert.Null(threeStateButton.IsChecked); } + [Fact] + public void ToggleButton_Events_Are_Raised_On_Is_Checked_Changes() + { + var threeStateButton = new ToggleButton(); + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.IsChecked = true; + Assert.True(checkedRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.IsChecked = false; + Assert.True(uncheckedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.IsChecked = null; + Assert.True(indeterminateRaised); + } + + [Fact] + public void ToggleButton_Events_Are_Raised_When_Toggling() + { + var threeStateButton = new TestToggleButton { IsThreeState = true }; + + bool checkedRaised = false; + threeStateButton.Checked += (_, __) => checkedRaised = true; + + threeStateButton.Toggle(); + Assert.True(checkedRaised); + + bool indeterminateRaised = false; + threeStateButton.Indeterminate += (_, __) => indeterminateRaised = true; + + threeStateButton.Toggle(); + Assert.True(indeterminateRaised); + + bool uncheckedRaised = false; + threeStateButton.Unchecked += (_, __) => uncheckedRaised = true; + + threeStateButton.Toggle(); + Assert.True(uncheckedRaised); + } + private class Class1 : NotifyingBase { private bool _foo; @@ -80,5 +127,10 @@ namespace Avalonia.Controls.Primitives.UnitTests set { nullableFoo = value; RaisePropertyChanged(); } } } + + private class TestToggleButton : ToggleButton + { + public new void Toggle() => base.Toggle(); + } } }