using System; using System.Windows.Input; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Reactive; 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. /// [TemplatePart("PART_PrimaryButton", typeof(Button))] [TemplatePart("PART_SecondaryButton", typeof(Button))] [PseudoClasses(pcFlyoutOpen, pcPressed)] public class SplitButton : ContentControl, ICommandSource, IClickableControl { internal const string pcChecked = ":checked"; internal const string pcPressed = ":pressed"; internal 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 StyledProperty CommandProperty = Button.CommandProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); /// /// Defines the property /// public static readonly StyledProperty FlyoutProperty = Button.FlyoutProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty HotKeyProperty = Button.HotKeyProperty.AddOwner(); private Button? _primaryButton = null; private Button? _secondaryButton = null; private KeyGesture? _hotkey = default; private bool _commandCanExecute = true; private bool _isAttachedToLogicalTree = false; private bool _isFlyoutOpen = false; private bool _isKeyboardPressed = false; private IDisposable? _flyoutPropertyChangedDisposable; /// /// Initializes a new instance of the class. /// public SplitButton() { } /// /// Gets or sets the invoked when the primary part is pressed. /// public ICommand? Command { get => GetValue(CommandProperty); set => SetValue(CommandProperty, 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 or sets an associated with this control /// public KeyGesture? HotKey { get => GetValue(HotKeyProperty); set => SetValue(HotKeyProperty, 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; /// void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); /// private void CanExecuteChanged(object? sender, EventArgs e) { (var command, var parameter) = (Command, CommandParameter); CanExecuteChanged(command, parameter); } private void CanExecuteChanged(ICommand? command, object? parameter) { if (!((ILogical)this).IsAttachedToLogicalTree) { return; } var canExecute = command is null || command.CanExecute(parameter); 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() { Flyout?.ShowAt(this); } /// /// Closes the secondary button's flyout. /// protected void CloseFlyout() { 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(Popup.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; _secondaryButton.RemoveHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed); } } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); UnregisterEvents(); UnregisterFlyoutEvents(Flyout); _primaryButton = e.NameScope.Find