Browse Source

Merge pull request #1435 from DmitryZhelnin/ButtonSpinner-Control

ButtonSpinner control is ported from ExtendedWFPToolkit
Steven Kirk 7 năm trước cách đây
mục cha
commit
80a0faf164

+ 6 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -35,6 +35,9 @@
     <EmbeddedResource Include="DecoratedWindow.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="Pages\ButtonSpinnerPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <EmbeddedResource Include="Pages\DialogsPage.xaml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
@@ -164,6 +167,9 @@
     <Compile Include="Pages\ToolTipPage.xaml.cs">
       <DependentUpon>ToolTipPage.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
+      <DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\ScreenPage.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>

+ 1 - 0
samples/ControlCatalog/MainView.xaml

@@ -7,6 +7,7 @@
     </TabControl.Transition>
     <TabItem Header="Border"><pages:BorderPage/></TabItem>
     <TabItem Header="Button"><pages:ButtonPage/></TabItem>
+    <TabItem Header="ButtonSpinner"><pages:ButtonSpinnerPage/></TabItem>
     <TabItem Header="Calendar"><pages:CalendarPage/></TabItem> 
     <TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
     <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>

+ 24 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml

@@ -0,0 +1,24 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+  <StackPanel Orientation="Vertical" Gap="4">
+    <TextBlock Classes="h1">ButtonSpinner</TextBlock>
+    <TextBlock Classes="h2">The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element.</TextBlock>
+
+    <StackPanel Orientation="Vertical" Gap="8" Width="200" Margin="0,20,0,0">
+      <CheckBox Name="allowSpinCheck" IsChecked="True">AllowSpin</CheckBox>
+      <CheckBox Name="showSpinCheck" IsChecked="True">ShowButtonSpinner</CheckBox>
+      <ButtonSpinner Spin="OnSpin" Height="30"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+      <ButtonSpinner Spin="OnSpin" Height="30" ButtonSpinnerLocation="Left"
+                     AllowSpin="{Binding #allowSpinCheck.IsChecked}"
+                     ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Everest"/>
+      </ButtonSpinner>
+    </StackPanel>
+  </StackPanel>
+
+</UserControl>

+ 54 - 0
samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs

@@ -0,0 +1,54 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class ButtonSpinnerPage : UserControl
+    {
+        public ButtonSpinnerPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void OnSpin(object sender, SpinEventArgs e)
+        {
+            var spinner = (ButtonSpinner)sender;
+            var txtBox = (TextBlock)spinner.Content;
+
+            int value = Array.IndexOf(_mountains, txtBox.Text);
+            if (e.Direction == SpinDirection.Increase)
+                value++;
+            else
+                value--;
+
+            if (value < 0)
+                value = _mountains.Length - 1;
+            else if (value >= _mountains.Length)
+                value = 0;
+
+            txtBox.Text = _mountains[value];
+        }
+
+        private readonly string[] _mountains = new[]
+        {
+            "Everest",
+            "K2 (Mount Godwin Austen)",
+            "Kangchenjunga",
+            "Lhotse",
+            "Makalu",
+            "Cho Oyu",
+            "Dhaulagiri",
+            "Manaslu",
+            "Nanga Parbat",
+            "Annapurna"
+        };
+    }
+}

+ 258 - 0
src/Avalonia.Controls/ButtonSpinner.cs

@@ -0,0 +1,258 @@
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    public enum Location
+    {
+        Left,
+        Right
+    }
+
+    /// <summary>
+    /// Represents a spinner control that includes two Buttons.
+    /// </summary>
+    public class ButtonSpinner : Spinner
+    {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(AllowSpin), true);
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            AvaloniaProperty.Register<ButtonSpinner, bool>(nameof(ShowButtonSpinner), true);
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
+
+        private Button _decreaseButton;
+        /// <summary>
+        /// Gets or sets the DecreaseButton template part.
+        /// </summary>
+        private Button DecreaseButton
+        {
+            get { return _decreaseButton; }
+            set
+            {
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click  -= OnButtonClick;
+                }
+                _decreaseButton = value;
+                if (_decreaseButton != null)
+                {
+                    _decreaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        private Button _increaseButton;
+        /// <summary>
+        /// Gets or sets the IncreaseButton template part.
+        /// </summary>
+        private Button IncreaseButton
+        {
+            get
+            {
+                return _increaseButton;
+            }
+            set
+            {
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click -= OnButtonClick;
+                }
+                _increaseButton = value;
+                if (_increaseButton != null)
+                {
+                    _increaseButton.Click += OnButtonClick;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ButtonSpinner"/> class.
+        /// </summary>
+        static ButtonSpinner()
+        {
+            AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
+            PseudoClass(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the <see cref="ButtonSpinner"/> should allow to spin.
+        /// </summary>
+        public bool AllowSpin
+        {
+            get { return GetValue(AllowSpinProperty); }
+            set { SetValue(AllowSpinProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the spin buttons should be shown.
+        /// </summary>
+        public bool ShowButtonSpinner
+        {
+            get { return GetValue(ShowButtonSpinnerProperty); }
+            set { SetValue(ShowButtonSpinnerProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets current location of the <see cref="ButtonSpinner"/>.
+        /// </summary>
+        public Location ButtonSpinnerLocation
+        {
+            get { return GetValue(ButtonSpinnerLocationProperty); }
+            set { SetValue(ButtonSpinnerLocationProperty, value); }
+        }
+
+        /// <inheritdoc />
+        protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+        {
+            IncreaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
+            DecreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
+            SetButtonUsage();
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+            Point mousePosition;
+            if (IncreaseButton != null && IncreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(IncreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < IncreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < IncreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+
+            if (DecreaseButton != null && DecreaseButton.IsEnabled == false)
+            {
+                mousePosition = e.GetPosition(DecreaseButton);
+                if (mousePosition.X > 0 && mousePosition.X < DecreaseButton.Width &&
+                    mousePosition.Y > 0 && mousePosition.Y < DecreaseButton.Height)
+                {
+                    e.Handled = true;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            switch (e.Key)
+            {
+                case Key.Up:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Increase));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Down:
+                {
+                    if (AllowSpin)
+                    {
+                        OnSpin(new SpinEventArgs(SpinEvent, SpinDirection.Decrease));
+                        e.Handled = true;
+                    }
+                    break;
+                }
+                case Key.Enter:
+                {
+                    //Do not Spin on enter Key when spinners have focus
+                    if (((IncreaseButton != null) && (IncreaseButton.IsFocused))
+                        || ((DecreaseButton != null) && DecreaseButton.IsFocused))
+                    {
+                        e.Handled = true;
+                    }
+                    break;
+                }
+            }
+        }
+
+        /// <inheritdoc />
+        protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+        {
+            base.OnPointerWheelChanged(e);
+            if (!e.Handled && AllowSpin)
+            {
+                if (e.Delta.Y != 0)
+                {
+                    var spinnerEventArgs = new SpinEventArgs(SpinEvent, (e.Delta.Y < 0) ? SpinDirection.Decrease : SpinDirection.Increase, true);
+                    OnSpin(spinnerEventArgs);
+                    e.Handled = spinnerEventArgs.Handled;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnAllowSpinChanged(bool oldValue, bool newValue)
+        {
+            SetButtonUsage();
+        }
+
+        /// <summary>
+        /// Called when the <see cref="AllowSpin"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void AllowSpinChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is ButtonSpinner spinner)
+            {
+                var oldValue = (bool)e.OldValue;
+                var newValue = (bool)e.NewValue;
+                spinner.OnAllowSpinChanged(oldValue, newValue);
+            }
+        }
+        
+        /// <summary>
+        /// Disables or enables the buttons based on the valid spin direction.
+        /// </summary>
+        private void SetButtonUsage()
+        {
+            if (IncreaseButton != null)
+            {
+                IncreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase);
+            }
+
+            if (DecreaseButton != null)
+            {
+                DecreaseButton.IsEnabled = AllowSpin && ((ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease);
+            }
+        }
+
+        /// <summary>
+        /// Called when user clicks one of the spin buttons.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void OnButtonClick(object sender, RoutedEventArgs e)
+        {
+            if (AllowSpin)
+            {
+                var direction = sender == IncreaseButton ? SpinDirection.Increase : SpinDirection.Decrease;
+                OnSpin(new SpinEventArgs(SpinEvent, direction));
+            }
+        }
+    }
+}

+ 174 - 0
src/Avalonia.Controls/Spinner.cs

@@ -0,0 +1,174 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents spin directions that are valid.
+    /// </summary>
+    [Flags]
+    public enum ValidSpinDirections
+    {
+        /// <summary>
+        /// Can not increase nor decrease.
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Can increase.
+        /// </summary>
+        Increase = 1,
+
+        /// <summary>
+        /// Can decrease.
+        /// </summary>
+        Decrease = 2
+    }
+
+    /// <summary>
+    /// Represents spin directions that could be initiated by the end-user.
+    /// </summary>
+    public enum SpinDirection
+    {
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Increase a value.
+        /// </summary>
+        Increase = 0,
+
+        /// <summary>
+        /// Represents a spin initiated by the end-user in order to Decrease a value.
+        /// </summary>
+        Decrease = 1
+    }
+
+    /// <summary>
+    /// Provides data for the Spinner.Spin event.
+    /// </summary>
+    public class SpinEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Gets the SpinDirection for the spin that has been initiated by the end-user.
+        /// </summary>
+        public SpinDirection Direction { get; }
+
+        /// <summary>
+        /// Get or set whheter the spin event originated from a mouse wheel event.
+        /// </summary>
+        public bool UsingMouseWheel{ get; }
+
+        /// <summary>
+        /// Initializes a new instance of the SpinEventArgs class.
+        /// </summary>
+        /// <param name="direction">Spin direction.</param>
+        public SpinEventArgs(SpinDirection direction)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction)
+            : base(routedEvent)
+        {
+            Direction = direction;
+        }
+
+        public SpinEventArgs(SpinDirection direction, bool usingMouseWheel)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+
+        public SpinEventArgs(RoutedEvent routedEvent, SpinDirection direction, bool usingMouseWheel)
+            : base(routedEvent)
+        {
+            Direction = direction;
+            UsingMouseWheel = usingMouseWheel;
+        }
+    }
+
+    /// <summary>
+    /// Base class for controls that represents controls that can spin.
+    /// </summary>
+    public abstract class Spinner : ContentControl
+    {
+        /// <summary>
+        /// Defines the <see cref="ValidSpinDirection"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ValidSpinDirections> ValidSpinDirectionProperty =
+            AvaloniaProperty.Register<Spinner, ValidSpinDirections>(nameof(ValidSpinDirection),
+                ValidSpinDirections.Increase | ValidSpinDirections.Decrease);
+
+        /// <summary>
+        /// Defines the <see cref="Spin"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<SpinEventArgs> SpinEvent =
+            RoutedEvent.Register<Spinner, SpinEventArgs>(nameof(Spin), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Initializes static members of the <see cref="Spinner"/> class.
+        /// </summary>
+        static Spinner()
+        {
+            ValidSpinDirectionProperty.Changed.Subscribe(OnValidSpinDirectionPropertyChanged);
+        }
+
+        /// <summary>
+        /// Occurs when spinning is initiated by the end-user.
+        /// </summary>
+        public event EventHandler<SpinEventArgs> Spin
+        {
+            add { AddHandler(SpinEvent, value); }
+            remove { RemoveHandler(SpinEvent, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets <see cref="ValidSpinDirections"/> allowed for this control.
+        /// </summary>
+        public ValidSpinDirections ValidSpinDirection
+        {
+            get { return GetValue(ValidSpinDirectionProperty); }
+            set { SetValue(ValidSpinDirectionProperty, value); }
+        }
+
+        /// <summary>
+        /// Called when valid spin direction changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+        {
+        }
+
+        /// <summary>
+        /// Raises the OnSpin event when spinning is initiated by the end-user.
+        /// </summary>
+        /// <param name="e">Spin event args.</param>
+        protected virtual void OnSpin(SpinEventArgs e)
+        {
+            var valid = e.Direction == SpinDirection.Increase
+                ? ValidSpinDirections.Increase
+                : ValidSpinDirections.Decrease;
+
+            //Only raise the event if spin is allowed.
+            if ((ValidSpinDirection & valid) == valid)
+            {
+                RaiseEvent(e);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="ValidSpinDirection"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnValidSpinDirectionPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is Spinner spinner)
+            {
+                var oldValue = (ValidSpinDirections)e.OldValue;
+                var newValue = (ValidSpinDirections)e.NewValue;
+                spinner.OnValidSpinDirectionChanged(oldValue, newValue);
+            }
+        }
+    }
+}

+ 86 - 0
src/Avalonia.Themes.Default/ButtonSpinner.xaml

@@ -0,0 +1,86 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="ButtonSpinner">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
+    <Setter Property="VerticalContentAlignment" Value="Center"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton">
+    <Setter Property="RepeatButton.Background" Value="Transparent"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="Transparent"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton:pointerover">
+    <Setter Property="RepeatButton.Background" Value="{DynamicResource ThemeControlMidBrush}"/>
+    <Setter Property="RepeatButton.BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,5 L4.5,.5 9,5 6,5 4.5,3.5 3,5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner /template/ RepeatButton#PART_DecreaseButton">
+    <Setter Property="Content">
+      <Template>
+        <Path Fill="{DynamicResource ThemeForegroundBrush}"
+              Width="8"
+              Height="4"
+              Stretch="Uniform"
+              HorizontalAlignment="Center"
+              VerticalAlignment="Center"
+              Data="M0,0 L3,0 4.5,1.5 6,0 9,0 4.5,4.5 z"/>
+      </Template>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:right">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="*,Auto">
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="0"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+            <Grid Grid.Column="1" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  <Style Selector="ButtonSpinner:left">
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Margin="{TemplateBinding Padding}"
+                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
+          <Grid ColumnDefinitions="Auto,*">
+            <Grid Grid.Column="0" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
+              <RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
+              <RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
+            </Grid>
+            <ContentPresenter Name="PART_ContentPresenter" Grid.Column="1"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Content="{TemplateBinding Content}"/>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -42,4 +42,5 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.CalendarItem.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.Calendar.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.ButtonSpinner.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>