From 619d889b39d3f4ead2291fd700d63364b640f65b Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 23 Jan 2022 22:06:59 -0500 Subject: [PATCH 01/48] Add initial SplitButton code --- .../SplitButton/SplitButton.cs | 600 ++++++++++++++++++ .../SplitButton/SplitButtonClickEventArgs.cs | 25 + 2 files changed, 625 insertions(+) create mode 100644 src/Avalonia.Controls/SplitButton/SplitButton.cs create mode 100644 src/Avalonia.Controls/SplitButton/SplitButtonClickEventArgs.cs diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs new file mode 100644 index 0000000000..22098961bd --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -0,0 +1,600 @@ +using System; +using System.Reactive.Disposables; +using System.Windows.Input; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +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. + /// + public class SplitButton : ContentControl, ICommandSource + { + /// + /// Raised when the user presses the primary part of the . + /// + public event EventHandler Click + { + add => AddHandler(ClickEvent, value); + remove => RemoveHandler(ClickEvent, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ClickEvent = + RoutedEvent.Register( + nameof(Click), + RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + AvaloniaProperty.RegisterDirect( + nameof(Command), + splitButton => splitButton.Command, + (splitButton, command) => splitButton.Command = command, + enableDataValidation: true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register( + nameof(CommandParameter)); + + /// + /// Defines the property + /// + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register( + nameof(Flyout)); + + private ICommand _Command; + + 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 CompositeDisposable _buttonPropertyChangedDisposable; + private IDisposable _flyoutPropertyChangedDisposable; + + //////////////////////////////////////////////////////////////////////// + // + // Constructor / Destructors + // + //////////////////////////////////////////////////////////////////////// + + /// + /// Initializes a new instance of the class. + /// + public SplitButton() + { + } + + //////////////////////////////////////////////////////////////////////// + // + // Properties + // + //////////////////////////////////////////////////////////////////////// + + /// + /// Gets or sets the invoked when the primary part is pressed. + /// + public ICommand Command + { + get => _Command; + set => SetAndRaise(CommandProperty, ref _Command, value); + } + + /// + /// Gets or sets a parameter to be passed to the . + /// + public object CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Gets or sets the that is shown when the secondary part is pressed. + /// + public FlyoutBase Flyout + { + get => GetValue(FlyoutProperty); + set => SetValue(FlyoutProperty, value); + } + + //////////////////////////////////////////////////////////////////////// + // + // Methods + // + //////////////////////////////////////////////////////////////////////// + + /// + public void CanExecuteChanged(object sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + + /// + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + private void UpdatePseudoClasses() + { + /* + // place the secondary button + if (m_lastPointerDeviceType == winrt::PointerDeviceType::Touch || m_isKeyDown) + { + winrt::VisualStateManager::GoToState(*this, L"SecondaryButtonSpan", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"SecondaryButtonRight", useTransitions); + } + + // change visual state + auto primaryButton = m_primaryButton.get(); + auto secondaryButton = m_secondaryButton.get(); + + if (!IsEnabled()) + { + winrt::VisualStateManager::GoToState(*this, L"Disabled", useTransitions); + } + else if (primaryButton && secondaryButton) + { + if (m_isFlyoutOpen) + { + if (InternalIsChecked()) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedFlyoutOpen", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"FlyoutOpen", useTransitions); + } + } + // SplitButton and ToggleSplitButton share a template -- this section is driving the checked states for ToggleSplitButton. + else if (InternalIsChecked()) + { + if (m_lastPointerDeviceType == winrt::PointerDeviceType::Touch || m_isKeyDown) + { + if (primaryButton.IsPressed() || secondaryButton.IsPressed() || m_isKeyDown) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedTouchPressed", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"Checked", useTransitions); + } + } + else if (primaryButton.IsPressed()) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedPrimaryPressed", useTransitions); + } + else if (primaryButton.IsPointerOver()) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedPrimaryPointerOver", useTransitions); + } + else if (secondaryButton.IsPressed()) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedSecondaryPressed", useTransitions); + } + else if (secondaryButton.IsPointerOver()) + { + winrt::VisualStateManager::GoToState(*this, L"CheckedSecondaryPointerOver", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"Checked", useTransitions); + } + } + else + { + if (m_lastPointerDeviceType == winrt::PointerDeviceType::Touch || m_isKeyDown) + { + if (primaryButton.IsPressed() || secondaryButton.IsPressed() || m_isKeyDown) + { + winrt::VisualStateManager::GoToState(*this, L"TouchPressed", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"Normal", useTransitions); + } + } + else if (primaryButton.IsPressed()) + { + winrt::VisualStateManager::GoToState(*this, L"PrimaryPressed", useTransitions); + } + else if (primaryButton.IsPointerOver()) + { + winrt::VisualStateManager::GoToState(*this, L"PrimaryPointerOver", useTransitions); + } + else if (secondaryButton.IsPressed()) + { + winrt::VisualStateManager::GoToState(*this, L"SecondaryPressed", useTransitions); + } + else if (secondaryButton.IsPointerOver()) + { + winrt::VisualStateManager::GoToState(*this, L"SecondaryPointerOver", useTransitions); + } + else + { + winrt::VisualStateManager::GoToState(*this, L"Normal", useTransitions); + } + } + } + */ + } + + /// + /// Opens the secondary button's flyout. + /// + private void OpenFlyout() + { + if (Flyout != null) + { + Flyout.ShowAt(this); + } + } + + /// + /// Closes the secondary button's flyout. + /// + private void CloseFlyout() + { + if (Flyout != null) + { + Flyout.Hide(); + } + } + + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + + _flyoutPropertyChangedDisposable?.Dispose(); + _flyoutPropertyChangedDisposable = null; + } + } + + /// + /// Explicitly unregisters all events related to the two buttons in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + _buttonPropertyChangedDisposable?.Dispose(); + _buttonPropertyChangedDisposable = null; + + if (_primaryButton != null) + { + _primaryButton.Click -= PrimaryButton_Click; + + _primaryButton.PointerEnter -= Button_PointerEvent; + _primaryButton.PointerLeave -= Button_PointerEvent; + _primaryButton.PointerPressed -= Button_PointerEvent; + _primaryButton.PointerReleased -= Button_PointerEvent; + _primaryButton.PointerCaptureLost -= Button_PointerCaptureLost; + } + + if (_secondaryButton != null) + { + _secondaryButton.Click -= SecondaryButton_Click; + + _secondaryButton.PointerEnter -= Button_PointerEvent; + _secondaryButton.PointerLeave -= Button_PointerEvent; + _secondaryButton.PointerPressed -= Button_PointerEvent; + _secondaryButton.PointerReleased -= Button_PointerEvent; + _secondaryButton.PointerCaptureLost -= Button_PointerCaptureLost; + } + } + + //////////////////////////////////////////////////////////////////////// + // + // OnEvent Overridable Methods + // + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterEvents(); + UnregisterFlyoutEvents(Flyout); + + _primaryButton = e.NameScope.Find + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b3049cf66468dcc1c954189a7d0ce82c950e8682 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 14:22:58 -0500 Subject: [PATCH 05/48] Add missing space --- src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index f07fc6f2e0..857d70084c 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -150,7 +150,7 @@ - + From 7a9dffa5867a3c106298379f066c0ee3c521607c Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 14:23:18 -0500 Subject: [PATCH 06/48] Fix ICommandSource comments --- src/Avalonia.Input/ICommandSource.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index 410b3a2e47..a6b5189440 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -20,12 +20,11 @@ namespace Avalonia.Input /// object? CommandParameter { get; } - /// - /// Bor the behavior CanExecuteChanged + /// Called for the CanExecuteChanged event when changes are detected. /// - /// - /// + /// The event sender. + /// The event args. void CanExecuteChanged(object sender, System.EventArgs e); /// From a6f158274566f8bd3f1c5f7cfa90ddda2c438232 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 14:23:40 -0500 Subject: [PATCH 07/48] Add sample SplitButtons to the ControlCatalog --- .../ControlCatalog/Pages/SplitButtonPage.xaml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/samples/ControlCatalog/Pages/SplitButtonPage.xaml b/samples/ControlCatalog/Pages/SplitButtonPage.xaml index a2e12a1528..2ada3bf3f4 100644 --- a/samples/ControlCatalog/Pages/SplitButtonPage.xaml +++ b/samples/ControlCatalog/Pages/SplitButtonPage.xaml @@ -2,5 +2,36 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.SplitButtonPage"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4322e216c4947b394e38d4b5d0a7b32e54f212ec Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 14:56:30 -0500 Subject: [PATCH 08/48] Remove code that shouldn't be there --- src/Avalonia.Controls/SplitButton/SplitButton.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 4af8b44629..a6a3112d05 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -1,7 +1,6 @@ using System; using System.Reactive.Disposables; using System.Windows.Input; -using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -400,8 +399,6 @@ namespace Avalonia.Controls { _primaryButton.Click += PrimaryButton_Click; - _primaryButton.GetPropertyChangedObservable(Button.IsPressedProperty); - _buttonPropertyChangedDisposable.Add(_primaryButton.GetPropertyChangedObservable(Button.IsPressedProperty).Subscribe(Button_VisualPropertyChanged)); _buttonPropertyChangedDisposable.Add(_primaryButton.GetPropertyChangedObservable(Button.IsPointerOverProperty).Subscribe(Button_VisualPropertyChanged)); From 29865672e24cd265ae8a53a2f2e2f0fad010286d Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 24 Jan 2022 23:46:23 -0500 Subject: [PATCH 09/48] 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 @@ - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - @@ -252,107 +252,107 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From d0dfac8a8b4d6d46aa073a47b71f64bd887c51fd Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 13:18:05 -0500 Subject: [PATCH 13/48] Remove ':normal' PseudoClass --- src/Avalonia.Controls/SplitButton/SplitButton.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 6bb2e2797e..bee9a5335f 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -164,7 +164,6 @@ namespace Avalonia.Controls /// protected void UpdatePseudoClasses() { - string pcNormal = ":normal"; // Not supported in XAML style string pcDisabled = ":disabled"; string pcSecondaryButtonRight = ":secondary-button-right"; @@ -263,7 +262,9 @@ namespace Avalonia.Controls } else { - SetExclusivePseudoClass(pcNormal); + // Calling without a parameter is treated as ':normal' and will clear all other + // PseudoClasses returning to the default state + SetExclusivePseudoClass(); } } else if (_primaryButton.IsPressed) @@ -284,7 +285,9 @@ namespace Avalonia.Controls } else { - SetExclusivePseudoClass(pcNormal); + // Calling without a parameter is treated as ':normal' and will clear all other + // PseudoClasses returning to the default state + SetExclusivePseudoClass(); } } } @@ -293,7 +296,6 @@ 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(pcDisabled, pseudoClass == pcDisabled); PseudoClasses.Set(pcCheckedFlyoutOpen, pseudoClass == pcCheckedFlyoutOpen); From dcd85a8126fb9555182320349192c037a3af73ae Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 13:22:40 -0500 Subject: [PATCH 14/48] Make PseudoClass names class constants --- .../SplitButton/SplitButton.cs | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index bee9a5335f..e2dda56758 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -14,24 +14,45 @@ namespace Avalonia.Controls /// The primary part behaves like a and the secondary part opens a flyout. /// [PseudoClasses( - ":disabled", - ":secondary-button-right", - ":secondary-button-span", - ":checked-flyout-open", - ":flyout-open", - ":checked-touch-pressed", - ":checked", - ":checked-primary-pressed", - ":checked-primary-pointerover", - ":checked-secondary-pressed", - ":checked-secondary-pointerover", - ":touch-pressed", - ":primary-pressed", - ":primary-pointerover", - ":secondary-pressed", - ":secondary-pointerover")] + pcDisabled, + pcSecondaryButtonRight, + pcSecondaryButtonSpan, + pcCheckedFlyoutOpen, + pcFlyoutOpen, + pcCheckedTouchPressed, + pcChecked, + pcCheckedPrimaryPressed, + pcCheckedPrimaryPointerOver, + pcCheckedSecondaryPressed, + pcCheckedSecondaryPointerOver, + pcTouchPressed, + pcPrimaryPressed, + pcPrimaryPointerOver, + pcSecondaryPressed, + pcSecondaryPointerOver)] public class SplitButton : ContentControl, ICommandSource { + private const string pcDisabled = ":disabled"; + + private const string pcSecondaryButtonRight = ":secondary-button-right"; + private const string pcSecondaryButtonSpan = ":secondary-button-span"; + + private const string pcCheckedFlyoutOpen = ":checked-flyout-open"; + private const string pcFlyoutOpen = ":flyout-open"; + + private const string pcCheckedTouchPressed = ":checked-touch-pressed"; + private const string pcChecked = ":checked"; + private const string pcCheckedPrimaryPressed = ":checked-primary-pressed"; + private const string pcCheckedPrimaryPointerOver = ":checked-primary-pointerover"; + private const string pcCheckedSecondaryPressed = ":checked-secondary-pressed"; + private const string pcCheckedSecondaryPointerOver = ":checked-secondary-pointerover"; + + private const string pcTouchPressed = ":touch-pressed"; + private const string pcPrimaryPressed = ":primary-pressed"; + private const string pcPrimaryPointerOver = ":primary-pointerover"; + private const string pcSecondaryPressed = ":secondary-pressed"; + private const string pcSecondaryPointerOver = ":secondary-pointerover"; + /// /// Raised when the user presses the primary part of the . /// @@ -164,27 +185,6 @@ namespace Avalonia.Controls /// protected void UpdatePseudoClasses() { - string pcDisabled = ":disabled"; - - string pcSecondaryButtonRight = ":secondary-button-right"; - string pcSecondaryButtonSpan = ":secondary-button-span"; - - string pcCheckedFlyoutOpen = ":checked-flyout-open"; - string pcFlyoutOpen = ":flyout-open"; - - string pcCheckedTouchPressed = ":checked-touch-pressed"; - string pcChecked = ":checked"; - string pcCheckedPrimaryPressed = ":checked-primary-pressed"; - string pcCheckedPrimaryPointerOver = ":checked-primary-pointerover"; - string pcCheckedSecondaryPressed = ":checked-secondary-pressed"; - string pcCheckedSecondaryPointerOver = ":checked-secondary-pointerover"; - - string pcTouchPressed = ":touch-pressed"; - string pcPrimaryPressed = ":primary-pressed"; - string pcPrimaryPointerOver = ":primary-pointerover"; - string pcSecondaryPressed = ":secondary-pressed"; - string pcSecondaryPointerOver = ":secondary-pointerover"; - // Place the secondary button // These are mutually exclusive PseudoClasses handled separately from SetExclusivePseudoClass(). // They must be applied in addition to the others. From 874a7f9a49d741f6887aa750b3360f0893465243 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 13:27:28 -0500 Subject: [PATCH 15/48] Fix incorrect names in SplitButton Styles --- .../Controls/SplitButton.xaml | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index a54926722f..8019ada8f3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -162,10 +162,10 @@ - - @@ -179,10 +179,10 @@ - - @@ -190,7 +190,7 @@ - @@ -202,7 +202,7 @@ - @@ -217,7 +217,7 @@ - @@ -229,7 +229,7 @@ - @@ -244,10 +244,10 @@ - - @@ -261,10 +261,10 @@ - - @@ -278,10 +278,10 @@ - - @@ -295,11 +295,11 @@ - - @@ -313,11 +313,11 @@ - - @@ -331,10 +331,10 @@ - - @@ -349,10 +349,10 @@ - - @@ -362,9 +362,9 @@ - - From 51e9ce9ef67dba49424bd82cf1b9a7e0d414cd40 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 13:27:45 -0500 Subject: [PATCH 16/48] Improve comments --- .../Accents/FluentControlResourcesDark.xaml | 2 +- .../Accents/FluentControlResourcesLight.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 085b03c96f..5b86de02d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -556,7 +556,7 @@ 1 32 - + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 702d2c60bc..eb68270354 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -550,7 +550,7 @@ 1 32 - + From aa20c89e685b83e33e418ce85403ad3b1e6a6438 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 13:41:38 -0500 Subject: [PATCH 17/48] Set the DropDownGlyphPath Fill color directly in checked states This is required for Avalonia after the switch to Path instead of TextBlock as in WinUI. WinUI glyph TextBlock just inherits the foreground color of the secondary button so doesn't need this. --- .../Controls/SplitButton.xaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 8019ada8f3..78ba4532da 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -250,6 +250,9 @@ + + + + + + + + + + From c2cebb32fa5423ba174b9a87817f11a3c3fa40a3 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 14:38:38 -0500 Subject: [PATCH 20/48] Pulled out 'child' Primary/SecondaryButton style --- .../Controls/SplitButton.xaml | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml index 681e5339ea..bd3996e399 100644 --- a/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml @@ -36,41 +36,9 @@ - - - - - + @@ -148,6 +116,34 @@ + + + + + - - + + --> + From 853d4140a301f09cfa4bc4727c086459de903169 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 25 Jan 2022 20:02:40 -0500 Subject: [PATCH 23/48] Move more Button property changed handling into OnPropertyChanged override --- src/Avalonia.Controls/Button.cs | 142 +++++++----------- .../SplitButton/SplitButton.cs | 4 +- 2 files changed, 57 insertions(+), 89 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 770eb63266..3735e6c010 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(CommandParameter)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsDefaultProperty = AvaloniaProperty.Register(nameof(IsDefault)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsCancelProperty = AvaloniaProperty.Register(nameof(IsCancel)); @@ -98,10 +98,6 @@ namespace Avalonia.Controls static Button() { FocusableProperty.OverrideDefaultValue(typeof(Button), true); - CommandProperty.Changed.Subscribe(CommandChanged); - CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); - IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); - IsCancelProperty.Changed.Subscribe(IsCancelChanged); AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler