diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 3d869bb723..7beb496ab5 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,8 +22,8 @@ - - + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml deleted file mode 100644 index 2ce1469118..0000000000 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - A button control - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml new file mode 100644 index 0000000000..7de0872eae --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A standard button control + + + + + + + + + + + + + + + + + + + + + + + + + + + + A button control with multiple states: checked, unchecked or indeterminate. + + + + + + + + + + + + + + A button control that raises its Click event repeatedly when it is pressed and held. + + + + + + + + + + + + + + + + A button with primary and secondary parts that can each be pressed 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 ToggleButton with two states and the secondary part opens a flyout. + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs similarity index 90% rename from samples/ControlCatalog/Pages/ButtonPage.xaml.cs rename to samples/ControlCatalog/Pages/ButtonsPage.xaml.cs index 5e555c8c91..5594bab176 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs @@ -3,11 +3,11 @@ using Avalonia.Markup.Xaml; namespace ControlCatalog.Pages { - public class ButtonPage : UserControl + public class ButtonsPage : UserControl { private int repeatButtonClickCount = 0; - public ButtonPage() + public ButtonsPage() { InitializeComponent(); diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml index 4db755b6fc..6afe6dd135 100644 --- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml +++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index a2efc7fba0..a7a4759182 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -85,8 +85,8 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty FlyoutProperty = - AvaloniaProperty.Register(nameof(Flyout)); + public static readonly StyledProperty FlyoutProperty = + AvaloniaProperty.Register(nameof(Flyout)); private ICommand? _command; private bool _commandCanExecute = true; @@ -186,7 +186,7 @@ namespace Avalonia.Controls /// /// Gets or sets the Flyout that should be shown with this button. /// - public FlyoutBase Flyout + public FlyoutBase? Flyout { get => GetValue(FlyoutProperty); set => SetValue(FlyoutProperty, value); diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs new file mode 100644 index 0000000000..f1d07b2679 --- /dev/null +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -0,0 +1,485 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Mixins; +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 pressed separately. + /// The primary part behaves like a and the secondary part opens a flyout. + /// + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class SplitButton : ContentControl, ICommandSource + { + protected const string pcChecked = ":checked"; + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + + /// + /// 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 = + Button.CommandProperty.AddOwner( + splitButton => splitButton.Command, + (splitButton, command) => splitButton.Command = command); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty FlyoutProperty = + Button.FlyoutProperty.AddOwner(); + + private ICommand? _Command; + + private Button? _primaryButton = null; + private Button? _secondaryButton = null; + + private bool _commandCanExecute = true; + private bool _isAttachedToLogicalTree = false; + private bool _isFlyoutOpen = false; + private bool _isKeyboardPressed = false; + + 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); + } + + /// + /// 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 + //////////////////////////////////////////////////////////////////////// + + /// + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + /// + private void CanExecuteChanged(object? sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + protected void UpdatePseudoClasses() + { + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, _isKeyboardPressed); + PseudoClasses.Set(pcChecked, InternalIsChecked); + } + + /// + /// Opens the secondary button's flyout. + /// + protected void OpenFlyout() + { + if (Flyout != null) + { + Flyout.ShowAt(this); + } + } + + /// + /// Closes the secondary button's flyout. + /// + protected 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() + { + if (_primaryButton != null) + { + _primaryButton.Click -= PrimaryButton_Click; + } + + if (_secondaryButton != null) + { + _secondaryButton.Click -= SecondaryButton_Click; + } + } + + //////////////////////////////////////////////////////////////////////// + // OnEvent Overridable Methods + //////////////////////////////////////////////////////////////////////// + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterEvents(); + UnregisterFlyoutEvents(Flyout); + + _primaryButton = e.NameScope.Find