using System; using System.Reactive.Disposables; using System.Windows.Input; using Avalonia.Controls.Metadata; 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( ":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")] 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 _isAttachedToLogicalTree = 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); } /// /// 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 //////////////////////////////////////////////////////////////////////// /// public 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() { 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. if (_lastPointerType == PointerType.Touch || _isKeyDown) { PseudoClasses.Set(pcSecondaryButtonSpan, true); PseudoClasses.Set(pcSecondaryButtonRight, false); } else { PseudoClasses.Set(pcSecondaryButtonSpan, false); PseudoClasses.Set(pcSecondaryButtonRight, true); } // Change the visual state if (!IsEnabled) { SetExclusivePseudoClass(pcDisabled); } else if (_primaryButton != null && _secondaryButton != null) { if (_isFlyoutOpen) { if (InternalIsChecked) { SetExclusivePseudoClass(pcCheckedFlyoutOpen); } else { SetExclusivePseudoClass(pcFlyoutOpen); } } // SplitButton and ToggleSplitButton share a template -- this section is driving the checked states for ToggleSplitButton. else if (InternalIsChecked) { if (_lastPointerType == PointerType.Touch || _isKeyDown) { if (_primaryButton.IsPressed || _secondaryButton.IsPressed || _isKeyDown) { SetExclusivePseudoClass(pcCheckedTouchPressed); } else { SetExclusivePseudoClass(pcChecked); } } else if (_primaryButton.IsPressed) { SetExclusivePseudoClass(pcCheckedPrimaryPressed); } else if (_primaryButton.IsPointerOver) { SetExclusivePseudoClass(pcCheckedPrimaryPointerOver); } else if (_secondaryButton.IsPressed) { SetExclusivePseudoClass(pcCheckedSecondaryPressed); } else if (_secondaryButton.IsPointerOver) { SetExclusivePseudoClass(pcCheckedSecondaryPointerOver); } else { SetExclusivePseudoClass(pcChecked); } } else { if (_lastPointerType == PointerType.Touch || _isKeyDown) { if (_primaryButton.IsPressed || _secondaryButton.IsPressed || _isKeyDown) { SetExclusivePseudoClass(pcTouchPressed); } else { // Calling without a parameter is treated as ':normal' and will clear all other // PseudoClasses returning to the default state SetExclusivePseudoClass(); } } else if (_primaryButton.IsPressed) { SetExclusivePseudoClass(pcPrimaryPressed); } else if (_primaryButton.IsPointerOver) { SetExclusivePseudoClass(pcPrimaryPointerOver); } else if (_secondaryButton.IsPressed) { SetExclusivePseudoClass(pcSecondaryPressed); } else if (_secondaryButton.IsPointerOver) { SetExclusivePseudoClass(pcSecondaryPointerOver); } else { // Calling without a parameter is treated as ':normal' and will clear all other // PseudoClasses returning to the default state SetExclusivePseudoClass(); } } } // Local function to enable the specified PseudoClass and disable all others // This more closely matches the VisualStateManager of WinUI where the default style originated void SetExclusivePseudoClass(string pseudoClass = "") { PseudoClasses.Set(pcDisabled, pseudoClass == pcDisabled); PseudoClasses.Set(pcCheckedFlyoutOpen, pseudoClass == pcCheckedFlyoutOpen); 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(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(pcSecondaryPointerOver, pseudoClass == pcSecondaryPointerOver); } } /// /// 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() { _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