From 29865672e24cd265ae8a53a2f2e2f0fad010286d Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 23:46:23 -0500 Subject: [PATCH] Add ToggleSplitButton --- .../ControlCatalog/Pages/SplitButtonPage.xaml | 57 ++--- .../SplitButton/SplitButton.cs | 145 ++++++++---- .../SplitButton/SplitButtonClickEventArgs.cs | 25 -- .../SplitButton/ToggleSplitButton.cs | 147 ++++++++++++ .../Controls/SplitButton.xaml | 215 +++++++++--------- 5 files changed, 390 insertions(+), 199 deletions(-) delete mode 100644 src/Avalonia.Controls/SplitButton/SplitButtonClickEventArgs.cs create mode 100644 src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs diff --git a/samples/ControlCatalog/Pages/SplitButtonPage.xaml b/samples/ControlCatalog/Pages/SplitButtonPage.xaml index 2ada3bf3f4..e530920dab 100644 --- a/samples/ControlCatalog/Pages/SplitButtonPage.xaml +++ b/samples/ControlCatalog/Pages/SplitButtonPage.xaml @@ -1,37 +1,40 @@ + + + + + + + + + + + + + - + - - - - - - - - - - - - + - - - - - - - - - - - + + + + + + + + + diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index a6a3112d05..d30217ae4c 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using System.Windows.Input; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -9,16 +10,32 @@ using Avalonia.LogicalTree; namespace Avalonia.Controls { /// - /// A button with primary and secondary parts that can each be invoked separately. - /// The primary part behaves like a button and the secondary part opens a flyout. + /// A button with primary and secondary parts that can each be pressed separately. + /// The primary part behaves like a and the secondary part opens a flyout. /// - //[PseudoClasses(":pressed")] + [PseudoClasses( + ":disabled", + ":secondaryButtonRight", + ":secondaryButtonSpan", + ":checkedFlyoutOpen", + ":flyoutOpen", + ":checkedTouchPressed", + ":checked", + ":checkedPrimaryPressed", + ":checkedPrimaryPointerOver", + ":checkedSecondaryPressed", + ":checkedSecondaryPointerOver", + ":touchPressed", + ":primaryPressed", + ":primaryPointerOver", + ":secondaryPressed", + ":secondaryPointerOver")] public class SplitButton : ContentControl, ICommandSource { /// /// Raised when the user presses the primary part of the . /// - public event EventHandler Click + public event EventHandler Click { add => AddHandler(ClickEvent, value); remove => RemoveHandler(ClickEvent, value); @@ -27,8 +44,8 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent ClickEvent = - RoutedEvent.Register( + public static readonly RoutedEvent ClickEvent = + RoutedEvent.Register( nameof(Click), RoutingStrategies.Bubble); @@ -61,11 +78,12 @@ namespace Avalonia.Controls private Button _primaryButton = null; private Button _secondaryButton = null; - private bool _commandCanExecute = true; - protected bool _hasLoaded = false; - private bool _isFlyoutOpen = false; - private bool _isKeyDown = false; - private PointerType _lastPointerType = PointerType.Mouse; + private bool _commandCanExecute = true; + protected bool _hasLoaded = false; + private bool _isAttachedToLogicalTree = false; + private bool _isFlyoutOpen = false; + private bool _isKeyDown = false; + private PointerType _lastPointerType = PointerType.Mouse; private CompositeDisposable _buttonPropertyChangedDisposable; private IDisposable _flyoutPropertyChangedDisposable; @@ -116,6 +134,19 @@ namespace Avalonia.Controls set => SetValue(FlyoutProperty, value); } + /// + /// Gets a value indicating whether the button is currently checked. + /// + /// + /// This property exists only for the derived and is + /// unused (set to false) within . Doing this allows the + /// two controls to share a default style. + /// + internal virtual bool InternalIsChecked => false; + + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + //////////////////////////////////////////////////////////////////////// // // Methods @@ -134,17 +165,12 @@ namespace Avalonia.Controls } } - /// - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; - /// /// Updates the visual state of the control by applying latest PseudoClasses. /// - private void UpdatePseudoClasses() + protected void UpdatePseudoClasses() { - bool internalIsChecked = false; - - string pcNormal = ":normal"; + string pcNormal = ":normal"; // Not supported in XAML style string pcDisabled = ":disabled"; string pcSecondaryButtonRight = ":secondaryButtonRight"; @@ -189,7 +215,7 @@ namespace Avalonia.Controls { if (_isFlyoutOpen) { - if (internalIsChecked) + if (InternalIsChecked) { SetExclusivePseudoClass(pcCheckedFlyoutOpen); } @@ -199,7 +225,7 @@ namespace Avalonia.Controls } } // SplitButton and ToggleSplitButton share a template -- this section is driving the checked states for ToggleSplitButton. - else if (internalIsChecked) + else if (InternalIsChecked) { if (_lastPointerType == PointerType.Touch || _isKeyDown) { @@ -273,23 +299,23 @@ namespace Avalonia.Controls // This more closely matches the VisualStateManager of WinUI where the default style originated void SetExclusivePseudoClass(string pseudoClass = "") { - PseudoClasses.Set(pcNormal, pseudoClass == pcNormal); + PseudoClasses.Set(pcNormal, pseudoClass == pcNormal); PseudoClasses.Set(pcDisabled, pseudoClass == pcDisabled); PseudoClasses.Set(pcCheckedFlyoutOpen, pseudoClass == pcCheckedFlyoutOpen); - PseudoClasses.Set(pcFlyoutOpen, pseudoClass == pcFlyoutOpen); + PseudoClasses.Set(pcFlyoutOpen, pseudoClass == pcFlyoutOpen); - PseudoClasses.Set(pcCheckedTouchPressed, pseudoClass == pcCheckedTouchPressed); - PseudoClasses.Set(pcChecked, pseudoClass == pcChecked); - PseudoClasses.Set(pcCheckedPrimaryPressed, pseudoClass == pcCheckedPrimaryPressed); - PseudoClasses.Set(pcCheckedPrimaryPointerOver, pseudoClass == pcCheckedPrimaryPointerOver); - PseudoClasses.Set(pcCheckedSecondaryPressed, pseudoClass == pcCheckedSecondaryPressed); + PseudoClasses.Set(pcCheckedTouchPressed, pseudoClass == pcCheckedTouchPressed); + PseudoClasses.Set(pcChecked, pseudoClass == pcChecked); + PseudoClasses.Set(pcCheckedPrimaryPressed, pseudoClass == pcCheckedPrimaryPressed); + PseudoClasses.Set(pcCheckedPrimaryPointerOver, pseudoClass == pcCheckedPrimaryPointerOver); + PseudoClasses.Set(pcCheckedSecondaryPressed, pseudoClass == pcCheckedSecondaryPressed); PseudoClasses.Set(pcCheckedSecondaryPointerOver, pseudoClass == pcCheckedSecondaryPointerOver); - PseudoClasses.Set(pcTouchPressed, pseudoClass == pcTouchPressed); - PseudoClasses.Set(pcPrimaryPressed, pseudoClass == pcPrimaryPressed); - PseudoClasses.Set(pcPrimaryPointerOver, pseudoClass == pcPrimaryPointerOver); - PseudoClasses.Set(pcSecondaryPressed, pseudoClass == pcSecondaryPressed); + PseudoClasses.Set(pcTouchPressed, pseudoClass == pcTouchPressed); + PseudoClasses.Set(pcPrimaryPressed, pseudoClass == pcPrimaryPressed); + PseudoClasses.Set(pcPrimaryPointerOver, pseudoClass == pcPrimaryPointerOver); + PseudoClasses.Set(pcSecondaryPressed, pseudoClass == pcSecondaryPressed); PseudoClasses.Set(pcSecondaryPointerOver, pseudoClass == pcSecondaryPointerOver); } } @@ -297,7 +323,7 @@ namespace Avalonia.Controls /// /// Opens the secondary button's flyout. /// - private void OpenFlyout() + protected void OpenFlyout() { if (Flyout != null) { @@ -308,7 +334,7 @@ namespace Avalonia.Controls /// /// Closes the secondary button's flyout. /// - private void CloseFlyout() + protected void CloseFlyout() { if (Flyout != null) { @@ -447,26 +473,63 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged += CanExecuteChanged; + CanExecuteChanged(this, EventArgs.Empty); + } + + _isAttachedToLogicalTree = true; } /// protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); + + if (Command != null) + { + Command.CanExecuteChanged -= CanExecuteChanged; + } + + _isAttachedToLogicalTree = false; } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs changedEventArgs) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (changedEventArgs.Property == FlyoutProperty) + if (e.Property == CommandProperty) + { + if (_isAttachedToLogicalTree) + { + // Must unregister events here while a reference to the old command still exists + if (e.OldValue is ICommand oldCommand) + { + oldCommand.CanExecuteChanged -= CanExecuteChanged; + } + + if (e.NewValue is ICommand newCommand) + { + newCommand.CanExecuteChanged += CanExecuteChanged; + } + } + + CanExecuteChanged(this, EventArgs.Empty); + } + else if (e.Property == CommandParameterProperty) + { + CanExecuteChanged(this, EventArgs.Empty); + } + else if (e.Property == FlyoutProperty) { - // Must unregister events here while a ref to the old flyout still exists - if (changedEventArgs.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout) + // Must unregister events here while a reference to the old flyout still exists + if (e.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout) { UnregisterFlyoutEvents(oldFlyout); } - if (changedEventArgs.NewValue.GetValueOrDefault() is FlyoutBase newFlyout) + if (e.NewValue.GetValueOrDefault() is FlyoutBase newFlyout) { RegisterFlyoutEvents(newFlyout); } @@ -474,7 +537,7 @@ namespace Avalonia.Controls UpdatePseudoClasses(); } - base.OnPropertyChanged(changedEventArgs); + base.OnPropertyChanged(e); } /// @@ -526,7 +589,7 @@ namespace Avalonia.Controls } /// - /// Invokes the when the primary button part is clicked. + /// Invokes the event when the primary button part is clicked. /// /// The event args from the internal Click event. protected virtual void OnClickPrimary(RoutedEventArgs e) @@ -534,7 +597,7 @@ namespace Avalonia.Controls // Note: It is not currently required to check enabled status; however, this is a failsafe if (IsEffectivelyEnabled) { - var eventArgs = new SplitButtonClickEventArgs(ClickEvent); + var eventArgs = new RoutedEventArgs(ClickEvent); RaiseEvent(eventArgs); if (!eventArgs.Handled && Command?.CanExecute(CommandParameter) == true) diff --git a/src/Avalonia.Controls/SplitButton/SplitButtonClickEventArgs.cs b/src/Avalonia.Controls/SplitButton/SplitButtonClickEventArgs.cs deleted file mode 100644 index 0359fb2f53..0000000000 --- a/src/Avalonia.Controls/SplitButton/SplitButtonClickEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Avalonia.Interactivity; - -namespace Avalonia.Controls -{ - /// - /// Provides event data for the event. - /// - public class SplitButtonClickEventArgs : RoutedEventArgs - { - public SplitButtonClickEventArgs() - { - } - - public SplitButtonClickEventArgs(RoutedEvent? routedEvent) - { - RoutedEvent = routedEvent; - } - - public SplitButtonClickEventArgs(RoutedEvent? routedEvent, IInteractive? source) - { - RoutedEvent = routedEvent; - Source = source; - } - } -} diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs new file mode 100644 index 0000000000..c062f577de --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -0,0 +1,147 @@ +using System; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Styling; + +namespace Avalonia.Controls +{ + /// + /// A button with primary and secondary parts that can each be pressed separately. + /// The primary part behaves like a with two states and + /// the secondary part opens a flyout. + /// + [PseudoClasses( + ":disabled", + ":secondaryButtonRight", + ":secondaryButtonSpan", + ":checkedFlyoutOpen", + ":flyoutOpen", + ":checkedTouchPressed", + ":checked", + ":checkedPrimaryPressed", + ":checkedPrimaryPointerOver", + ":checkedSecondaryPressed", + ":checkedSecondaryPointerOver", + ":touchPressed", + ":primaryPressed", + ":primaryPointerOver", + ":secondaryPressed", + ":secondaryPointerOver")] + public class ToggleSplitButton : SplitButton, IStyleable + { + /// + /// Raised when the property value changes. + /// + public event EventHandler IsCheckedChanged + { + add => AddHandler(IsCheckedChangedEvent, value); + remove => RemoveHandler(IsCheckedChangedEvent, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent IsCheckedChangedEvent = + RoutedEvent.Register( + nameof(IsCheckedChanged), + RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsCheckedProperty = + AvaloniaProperty.Register( + nameof(IsChecked)); + + //////////////////////////////////////////////////////////////////////// + // + // Constructor / Destructors + // + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the class. + /// + public ToggleSplitButton() + { + } + + //////////////////////////////////////////////////////////////////////// + // + // Properties + // + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets or sets a value indicating whether the is checked. + /// + public bool IsChecked + { + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } + + /// + internal override bool InternalIsChecked => IsChecked; + + /// + /// + /// Both and share + /// the same exact default style. + /// + Type IStyleable.StyleKey => typeof(SplitButton); + + //////////////////////////////////////////////////////////////////////// + // + // Methods + // + //////////////////////////////////////////////////////////////////////// + + /// + /// Toggles the property between true and false. + /// + protected void Toggle() + { + IsChecked = !IsChecked; + } + + //////////////////////////////////////////////////////////////////////// + // + // OnEvent Overridable Methods + // + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == IsCheckedProperty) + { + OnIsCheckedChanged(); + } + } + + /// + /// Invokes the event when the + /// property changes. + /// + protected virtual void OnIsCheckedChanged() + { + if (_hasLoaded) + { + var eventArgs = new RoutedEventArgs(IsCheckedChangedEvent); + RaiseEvent(eventArgs); + } + + UpdatePseudoClasses(); + } + + /// + protected override void OnClickPrimary(RoutedEventArgs e) + { + Toggle(); + + base.OnClickPrimary(e); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 1c20cd329f..1b48f22a8c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -235,124 +235,127 @@ - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - + + + + +