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 @@
-
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+