Browse Source

Merge pull request #1265 from jp2masa/togglebutton-three-states

Implemented three states on ToggleButton, CheckBox and RadioButton
Jeremy Koritzinsky 8 years ago
parent
commit
5054012cc1

+ 19 - 6
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@@ -1,15 +1,28 @@
-<UserControl xmlns="https://github.com/avaloniaui">
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <StackPanel Orientation="Vertical" Gap="4">
     <TextBlock Classes="h1">CheckBox</TextBlock>
     <TextBlock Classes="h2">A check box control</TextBlock>
 
-    <StackPanel Orientation="Vertical"
+    <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
                 Gap="16">
-      <CheckBox>Unchecked</CheckBox>
-      <CheckBox IsChecked="True">Checked</CheckBox>
-      <CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
-    </StackPanel>    
+      <StackPanel Orientation="Vertical"
+                  Gap="16">
+        <CheckBox>Unchecked</CheckBox>
+        <CheckBox IsChecked="True">Checked</CheckBox>
+        <CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
+        <CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
+      </StackPanel>
+      <StackPanel Orientation="Vertical"
+                  HorizontalAlignment="Center"
+                  Gap="16">
+        <CheckBox IsChecked="False" IsThreeState="True">Three State: Unchecked</CheckBox>
+        <CheckBox IsChecked="True" IsThreeState="True">Three State: Checked</CheckBox>
+        <CheckBox IsChecked="{x:Null}" IsThreeState="True">Three State: Indeterminate</CheckBox>
+        <CheckBox IsChecked="{x:Null}" IsThreeState="True" IsEnabled="False">Three State: Disabled</CheckBox>
+      </StackPanel>
+    </StackPanel>
   </StackPanel>
 </UserControl>

+ 1 - 1
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -36,7 +36,7 @@ namespace ControlCatalog.Pages
             };
         }
 
-        Window GetWindow() => this.FindControl<CheckBox>("IsModal").IsChecked ? (Window)this.VisualRoot : null;
+        Window GetWindow() => this.FindControl<CheckBox>("IsModal").IsChecked.Value ? (Window)this.VisualRoot : null;
 
         private void InitializeComponent()
         {

+ 18 - 6
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@@ -1,15 +1,27 @@
-<UserControl xmlns="https://github.com/avaloniaui">
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <StackPanel Orientation="Vertical" Gap="4">
     <TextBlock Classes="h1">RadioButton</TextBlock>
     <TextBlock Classes="h2">Allows the selection of a single option of many</TextBlock>
 
-    <StackPanel Orientation="Vertical"
+    <StackPanel Orientation="Horizontal"
                 Margin="0,16,0,0"
                 HorizontalAlignment="Center"
                 Gap="16">
-      <RadioButton IsChecked="True">Option 1</RadioButton>
-      <RadioButton>Option 2</RadioButton>
-      <RadioButton IsEnabled="False">Disabled</RadioButton>
-    </StackPanel>    
+      <StackPanel Orientation="Vertical"
+                  Gap="16">
+        <RadioButton IsChecked="True">Option 1</RadioButton>
+        <RadioButton>Option 2</RadioButton>
+        <RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
+        <RadioButton IsEnabled="False">Disabled</RadioButton>
+      </StackPanel>
+      <StackPanel Orientation="Vertical"
+                  Gap="16">
+        <RadioButton IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton>
+        <RadioButton IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton>
+        <RadioButton IsChecked="{x:Null}" IsThreeState="True">Three States: Option 3</RadioButton>
+        <RadioButton IsChecked="{x:Null}" IsThreeState="True" IsEnabled="False">Disabled</RadioButton>
+      </StackPanel>
+    </StackPanel>
   </StackPanel>
 </UserControl>

+ 28 - 8
src/Avalonia.Controls/Primitives/ToggleButton.cs

@@ -9,26 +9,37 @@ namespace Avalonia.Controls.Primitives
 {
     public class ToggleButton : Button
     {
-        public static readonly DirectProperty<ToggleButton, bool> IsCheckedProperty =
-            AvaloniaProperty.RegisterDirect<ToggleButton, bool>(
-                "IsChecked",
+        public static readonly DirectProperty<ToggleButton, bool?> IsCheckedProperty =
+            AvaloniaProperty.RegisterDirect<ToggleButton, bool?>(
+                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<bool> IsThreeStateProperty =
+            AvaloniaProperty.Register<ToggleButton, bool>(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,16 @@ namespace Avalonia.Controls.Primitives
 
         protected virtual void Toggle()
         {
-            IsChecked = !IsChecked;
+            if (IsChecked.HasValue)
+                if (IsChecked.Value)
+                    if (IsThreeState)
+                        IsChecked = null;
+                    else
+                        IsChecked = false;
+                else
+                    IsChecked = true;
+            else
+                IsChecked = false;
         }
     }
 }

+ 5 - 4
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;
                 }
             }
         }

+ 23 - 8
src/Avalonia.Themes.Default/CheckBox.xaml

@@ -12,14 +12,23 @@
                   Width="18"
                   Height="18"
                   VerticalAlignment="Center">
-            <Path Name="checkMark"
-                  Fill="{DynamicResource HighlightBrush}"
-                  Width="11"
-                  Height="10"
-                  Stretch="Uniform"
-                  HorizontalAlignment="Center"
-                  VerticalAlignment="Center"
-                  Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
+            <Panel>
+              <Path Name="checkMark"
+                    Fill="{DynamicResource HighlightBrush}"
+                    Width="11"
+                    Height="10"
+                    Stretch="Uniform"
+                    HorizontalAlignment="Center"
+                    VerticalAlignment="Center"
+                    Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
+              <Rectangle Name="indeterminateMark"
+                         Fill="{DynamicResource HighlightBrush}"
+                         Width="10"
+                         Height="10"
+                         Stretch="Uniform"
+                         HorizontalAlignment="Center"
+                         VerticalAlignment="Center"/>
+            </Panel>
           </Border>
           <ContentPresenter Name="PART_ContentPresenter"
                             Content="{TemplateBinding Content}"
@@ -37,9 +46,15 @@
   <Style Selector="CheckBox /template/ Path#checkMark">
     <Setter Property="IsVisible" Value="False"/>
   </Style>
+  <Style Selector="CheckBox /template/ Rectangle#indeterminateMark">
+    <Setter Property="IsVisible" Value="False"/>
+  </Style>
   <Style Selector="CheckBox:checked /template/ Path#checkMark">
     <Setter Property="IsVisible" Value="True"/>
   </Style>
+  <Style Selector="CheckBox:indeterminate /template/ Rectangle#indeterminateMark">
+    <Setter Property="IsVisible" Value="True"/>
+  </Style>
   <Style Selector="CheckBox:disabled /template/ Border#border">
     <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
   </Style>

+ 14 - 0
src/Avalonia.Themes.Default/RadioButton.xaml

@@ -20,6 +20,14 @@
                    UseLayoutRounding="False"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"/>
+          <Ellipse Name="indeterminateMark"
+                   Fill="{DynamicResource ThemeAccentBrush}"
+                   Width="10"
+                   Height="10"
+                   Stretch="Uniform"
+                   UseLayoutRounding="False"
+                   HorizontalAlignment="Center"
+                   VerticalAlignment="Center"/>
           <ContentPresenter Name="PART_ContentPresenter"
                             Content="{TemplateBinding Content}"
                             ContentTemplate="{TemplateBinding ContentTemplate}"
@@ -36,9 +44,15 @@
   <Style Selector="RadioButton /template/ Ellipse#checkMark">
     <Setter Property="IsVisible" Value="False"/>
   </Style>
+  <Style Selector="RadioButton /template/ Ellipse#indeterminateMark">
+    <Setter Property="IsVisible" Value="False"/>
+  </Style>
   <Style Selector="RadioButton:checked /template/ Ellipse#checkMark">
     <Setter Property="IsVisible" Value="True"/>
   </Style>
+  <Style Selector="RadioButton:indeterminate /template/ Ellipse#indeterminateMark">
+    <Setter Property="IsVisible" Value="True"/>
+  </Style>
   <Style Selector="RadioButton:disabled /template/ Ellipse#border">
     <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
   </Style>

+ 57 - 0
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(); }
+            }
+        }
+    }
+}

+ 36 - 0
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);
+        }
+    }
+}