diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index 4a6db5bed9..a00b3a7bef 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -1,15 +1,28 @@ - + CheckBox A check box control - - Unchecked - Checked - Disabled - + + Unchecked + Checked + Indeterminate + Disabled + + + Three State: Unchecked + Three State: Checked + Three State: Indeterminate + Three State: Disabled + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 94364da698..8b3e810f0a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -36,7 +36,7 @@ namespace ControlCatalog.Pages }; } - Window GetWindow() => this.FindControl("IsModal").IsChecked ? (Window)this.VisualRoot : null; + Window GetWindow() => this.FindControl("IsModal").IsChecked.Value ? (Window)this.VisualRoot : null; private void InitializeComponent() { diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index a40477a679..29bac2c2c5 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -1,15 +1,26 @@ - + RadioButton Allows the selection of a single option of many - - Option 1 - Option 2 - Disabled - + + Option 1 + Option 2 + Disabled + + + Three States: Option 1 + Three States: Option 2 + Three States: Option 3 + Disabled + + \ No newline at end of file diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index 4e9e326997..b9a517db4c 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -9,26 +9,37 @@ namespace Avalonia.Controls.Primitives { public class ToggleButton : Button { - public static readonly DirectProperty IsCheckedProperty = - AvaloniaProperty.RegisterDirect( - "IsChecked", + public static readonly DirectProperty IsCheckedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsChecked), o => o.IsChecked, - (o,v) => o.IsChecked = v, + (o, v) => o.IsChecked = v, defaultBindingMode: BindingMode.TwoWay); - private bool _isChecked; + public static readonly StyledProperty IsThreeStateProperty = + AvaloniaProperty.Register(nameof(IsThreeState)); + + private bool? _isChecked = false; static ToggleButton() { - PseudoClass(IsCheckedProperty, ":checked"); + PseudoClass(IsCheckedProperty, c => c == true, ":checked"); + PseudoClass(IsCheckedProperty, c => c == false, ":unchecked"); + PseudoClass(IsCheckedProperty, c => c == null, ":indeterminate"); } - public bool IsChecked + public bool? IsChecked { get { return _isChecked; } set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } } + public bool IsThreeState + { + get => GetValue(IsThreeStateProperty); + set => SetValue(IsThreeStateProperty, value); + } + protected override void OnClick() { Toggle(); @@ -37,7 +48,7 @@ namespace Avalonia.Controls.Primitives protected virtual void Toggle() { - IsChecked = !IsChecked; + IsChecked = IsChecked.HasValue ? (IsChecked.Value ? (IsThreeState ? (bool?)null : false) : true) : false; } } } diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 25ab0f9c12..945335b8f7 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -17,17 +17,17 @@ namespace Avalonia.Controls protected override void Toggle() { - if (!IsChecked) + if (!IsChecked.GetValueOrDefault()) { IsChecked = true; } } - private void IsCheckedChanged(bool value) + private void IsCheckedChanged(bool? value) { var parent = this.GetVisualParent(); - if (value && parent != null) + if (value.GetValueOrDefault() && parent != null) { var siblings = parent .GetVisualChildren() @@ -36,7 +36,8 @@ namespace Avalonia.Controls foreach (var sibling in siblings) { - sibling.IsChecked = false; + if (sibling.IsChecked.GetValueOrDefault()) + sibling.IsChecked = false; } } } diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/CheckBox.xaml index 195ec098c9..ef96e7ed4f 100644 --- a/src/Avalonia.Themes.Default/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/CheckBox.xaml @@ -12,14 +12,23 @@ Width="18" Height="18" VerticalAlignment="Center"> - + + + + + + diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index 996f46023e..0a0eb48564 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -20,6 +20,14 @@ UseLayoutRounding="False" HorizontalAlignment="Center" VerticalAlignment="Center"/> + + + diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs new file mode 100644 index 0000000000..db60d05a34 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ToggleButtonTests.cs @@ -0,0 +1,57 @@ +using Avalonia.Markup.Xaml.Data; +using Avalonia.UnitTests; + +using Xunit; + +namespace Avalonia.Controls.Primitives.UnitTests +{ + public class ToggleButtonTests + { + private const string uncheckedClass = ":unchecked"; + private const string checkedClass = ":checked"; + private const string indeterminateClass = ":indeterminate"; + + [Theory] + [InlineData(false, uncheckedClass, false)] + [InlineData(false, uncheckedClass, true)] + [InlineData(true, checkedClass, false)] + [InlineData(true, checkedClass, true)] + [InlineData(null, indeterminateClass, false)] + [InlineData(null, indeterminateClass, true)] + public void ToggleButton_Has_Correct_Class_According_To_Is_Checked(bool? isChecked, string expectedClass, bool isThreeState) + { + var toggleButton = new ToggleButton(); + toggleButton.IsThreeState = isThreeState; + toggleButton.IsChecked = isChecked; + + Assert.Contains(expectedClass, toggleButton.Classes); + } + + [Fact] + public void ToggleButton_Is_Checked_Binds_To_Bool() + { + var toggleButton = new ToggleButton(); + var source = new Class1(); + + toggleButton.DataContext = source; + toggleButton.Bind(ToggleButton.IsCheckedProperty, new Binding("Foo")); + + source.Foo = true; + Assert.True(toggleButton.IsChecked); + + source.Foo = false; + Assert.False(toggleButton.IsChecked); + } + + private class Class1 : NotifyingBase + { + private bool _foo; + + public bool Foo + { + get { return _foo; } + set { _foo = value; RaisePropertyChanged(); } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs b/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs new file mode 100644 index 0000000000..37fb52beeb --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs @@ -0,0 +1,36 @@ +using Avalonia.Markup.Xaml.Data; +using Avalonia.UnitTests; + +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class RadioButtonTests + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Indeterminate_RadioButton_Is_Not_Unchecked_After_Checking_Other_Radio_Button(bool isThreeState) + { + var panel = new Panel(); + + var radioButton1 = new RadioButton(); + radioButton1.IsThreeState = false; + radioButton1.IsChecked = false; + + var radioButton2 = new RadioButton(); + radioButton2.IsThreeState = isThreeState; + radioButton2.IsChecked = null; + + panel.Children.Add(radioButton1); + panel.Children.Add(radioButton2); + + Assert.Null(radioButton2.IsChecked); + + radioButton1.IsChecked = true; + + Assert.True(radioButton1.IsChecked); + Assert.Null(radioButton2.IsChecked); + } + } +}