Browse Source

Move more Button property changed handling into OnPropertyChanged override

pull/7451/head
robloo 4 years ago
parent
commit
d8e071039b
  1. 142
      src/Avalonia.Controls/Button.cs
  2. 735
      src/Avalonia.Controls/SplitButton/SplitButton.cs

142
src/Avalonia.Controls/Button.cs

@ -59,13 +59,13 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Button, object>(nameof(CommandParameter));
/// <summary>
/// Defines the <see cref="IsDefaultProperty"/> property.
/// Defines the <see cref="IsDefault"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsDefaultProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsDefault));
/// <summary>
/// Defines the <see cref="IsCancelProperty"/> property.
/// Defines the <see cref="IsCancel"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsCancelProperty =
AvaloniaProperty.Register<Button, bool>(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<Button>((lbl, args) => lbl.OnAccessKey(args));
}
@ -390,116 +386,88 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty)
if (change.Property == CommandProperty)
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == FlyoutProperty)
{
// If flyout is changed while one is already open, make sure we
// close the old one first
if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
oldFlyout.IsOpen)
if (((ILogical)this).IsAttachedToLogicalTree)
{
oldFlyout.Hide();
if (change.OldValue.GetValueOrDefault() is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= CanExecuteChanged;
}
if (change.NewValue.GetValueOrDefault() is ICommand newCommand)
{
newCommand.CanExecuteChanged += CanExecuteChanged;
}
}
}
}
/// <inheritdoc/>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
CanExecuteChanged(this, EventArgs.Empty);
}
else if (change.Property == CommandParameterProperty)
{
if (value.Type == BindingValueType.BindingError)
CanExecuteChanged(this, EventArgs.Empty);
}
else if (change.Property == IsCancelProperty)
{
var isCancel = change.NewValue.GetValueOrDefault<bool>();
if (VisualRoot is IInputElement inputRoot)
{
if (_commandCanExecute)
if (isCancel)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
ListenForCancel(inputRoot);
}
else
{
StopListeningForCancel(inputRoot);
}
}
}
}
/// <summary>
/// Called when the <see cref="Command"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is Button button)
else if (change.Property == IsDefaultProperty)
{
if (((ILogical)button).IsAttachedToLogicalTree)
var isDefault = change.NewValue.GetValueOrDefault<bool>();
if (VisualRoot is IInputElement inputRoot)
{
if (e.OldValue is ICommand oldCommand)
if (isDefault)
{
oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
ListenForDefault(inputRoot);
}
if (e.NewValue is ICommand newCommand)
else
{
newCommand.CanExecuteChanged += button.CanExecuteChanged;
StopListeningForDefault(inputRoot);
}
}
button.CanExecuteChanged(button, EventArgs.Empty);
}
}
/// <summary>
/// Called when the <see cref="CommandParameter"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is Button button)
else if (change.Property == IsPressedProperty)
{
button.CanExecuteChanged(button, EventArgs.Empty);
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
}
/// <summary>
/// Called when the <see cref="IsDefault"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void IsDefaultChanged(AvaloniaPropertyChangedEventArgs e)
{
var button = e.Sender as Button;
var isDefault = (bool)e.NewValue;
if (button?.VisualRoot is IInputElement inputRoot)
else if (change.Property == FlyoutProperty)
{
if (isDefault)
{
button.ListenForDefault(inputRoot);
}
else
// If flyout is changed while one is already open, make sure we
// close the old one first
if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
oldFlyout.IsOpen)
{
button.StopListeningForDefault(inputRoot);
oldFlyout.Hide();
}
}
}
/// <summary>
/// Called when the <see cref="IsCancel"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void IsCancelChanged(AvaloniaPropertyChangedEventArgs e)
/// <inheritdoc/>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
var button = e.Sender as Button;
var isCancel = (bool)e.NewValue;
if (button?.VisualRoot is IInputElement inputRoot)
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
{
if (isCancel)
{
button.ListenForCancel(inputRoot);
}
else
if (value.Type == BindingValueType.BindingError)
{
button.StopListeningForCancel(inputRoot);
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}

735
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -0,0 +1,735 @@
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
{
/// <summary>
/// A button with primary and secondary parts that can each be pressed separately.
/// The primary part behaves like a <see cref="Button"/> and the secondary part opens a flyout.
/// </summary>
[PseudoClasses(
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";
/// <summary>
/// Raised when the user presses the primary part of the <see cref="SplitButton"/>.
/// </summary>
public event EventHandler<RoutedEventArgs> Click
{
add => AddHandler(ClickEvent, value);
remove => RemoveHandler(ClickEvent, value);
}
/// <summary>
/// Defines the <see cref="Click"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<SplitButton, RoutedEventArgs>(
nameof(Click),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly DirectProperty<SplitButton, ICommand> CommandProperty =
AvaloniaProperty.RegisterDirect<SplitButton, ICommand>(
nameof(Command),
splitButton => splitButton.Command,
(splitButton, command) => splitButton.Command = command,
enableDataValidation: true);
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly StyledProperty<object> CommandParameterProperty =
AvaloniaProperty.Register<SplitButton, object>(
nameof(CommandParameter));
/// <summary>
/// Defines the <see cref="Flyout"/> property
/// </summary>
public static readonly StyledProperty<FlyoutBase> FlyoutProperty =
AvaloniaProperty.Register<SplitButton, FlyoutBase>(
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
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Initializes a new instance of the <see cref="SplitButton"/> class.
/// </summary>
public SplitButton()
{
}
////////////////////////////////////////////////////////////////////////
// Properties
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Gets or sets the <see cref="ICommand"/> invoked when the primary part is pressed.
/// </summary>
public ICommand Command
{
get => _Command;
set => SetAndRaise(CommandProperty, ref _Command, value);
}
/// <summary>
/// Gets or sets a parameter to be passed to the <see cref="Command"/>.
/// </summary>
public object CommandParameter
{
get => GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="FlyoutBase"/> that is shown when the secondary part is pressed.
/// </summary>
public FlyoutBase Flyout
{
get => GetValue(FlyoutProperty);
set => SetValue(FlyoutProperty, value);
}
/// <summary>
/// Gets a value indicating whether the button is currently checked.
/// </summary>
/// <remarks>
/// This property exists only for the derived <see cref="ToggleSplitButton"/> and is
/// unused (set to false) within <see cref="SplitButton"/>. Doing this allows the
/// two controls to share a default style.
/// </remarks>
internal virtual bool InternalIsChecked => false;
/// <inheritdoc/>
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
////////////////////////////////////////////////////////////////////////
// Methods
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/>
public void CanExecuteChanged(object sender, EventArgs e)
{
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
/// <summary>
/// Updates the visual state of the control by applying latest PseudoClasses.
/// </summary>
protected void UpdatePseudoClasses()
{
// Place the secondary button
//
// In WinUI, the span of the secondary button is changed to full-width for touch-based
// devices in certain conditions. The full reasoning for this is unknown. Some theories
// include:
//
// My guess is that the design team at MS decided that it's a better experience
// for touch users to make them select from the drop down rather than the shortcut
// top level button, so touch basically just turns this into a drop down button.
// Whether that's ideal or not is going is a subjective opinion.
//
// For Avalonia, it may not always make sense to disable the primary button like that
// on touch-first platforms. Users and developers would normally expect a control to
// function the same on all platforms. Therefore, this functionality is disabled here
// but could be re-enabled in the future if more reasons become known.
//
// Finally, 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 (!IsEffectivelyEnabled)
{
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);
}
}
/// <summary>
/// Opens the secondary button's flyout.
/// </summary>
protected void OpenFlyout()
{
if (Flyout != null)
{
Flyout.ShowAt(this);
}
}
/// <summary>
/// Closes the secondary button's flyout.
/// </summary>
protected void CloseFlyout()
{
if (Flyout != null)
{
Flyout.Hide();
}
}
/// <summary>
/// Registers all flyout events.
/// </summary>
/// <param name="flyout">The flyout to connect events to.</param>
private void RegisterFlyoutEvents(FlyoutBase flyout)
{
if (flyout != null)
{
flyout.Opened += Flyout_Opened;
flyout.Closed += Flyout_Closed;
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
}
}
/// <summary>
/// Explicitly unregisters all flyout events.
/// </summary>
/// <param name="flyout">The flyout to disconnect events from.</param>
private void UnregisterFlyoutEvents(FlyoutBase flyout)
{
if (flyout != null)
{
flyout.Opened -= Flyout_Opened;
flyout.Closed -= Flyout_Closed;
_flyoutPropertyChangedDisposable?.Dispose();
_flyoutPropertyChangedDisposable = null;
}
}
/// <summary>
/// Explicitly unregisters all events related to the two buttons in OnApplyTemplate().
/// </summary>
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
////////////////////////////////////////////////////////////////////////
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
UnregisterEvents();
UnregisterFlyoutEvents(Flyout);
_primaryButton = e.NameScope.Find<Button>("PART_PrimaryButton");
_secondaryButton = e.NameScope.Find<Button>("PART_SecondaryButton");
_buttonPropertyChangedDisposable = new CompositeDisposable();
if (_primaryButton != null)
{
_primaryButton.Click += PrimaryButton_Click;
_buttonPropertyChangedDisposable.Add(_primaryButton.GetPropertyChangedObservable(Button.IsPressedProperty).Subscribe(Button_VisualPropertyChanged));
_buttonPropertyChangedDisposable.Add(_primaryButton.GetPropertyChangedObservable(Button.IsPointerOverProperty).Subscribe(Button_VisualPropertyChanged));
// Register for pointer events to keep track of the last used pointer type and update visual states
_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;
_buttonPropertyChangedDisposable.Add(_secondaryButton.GetPropertyChangedObservable(Button.IsPressedProperty).Subscribe(Button_VisualPropertyChanged));
_buttonPropertyChangedDisposable.Add(_secondaryButton.GetPropertyChangedObservable(Button.IsPointerOverProperty).Subscribe(Button_VisualPropertyChanged));
// Register for pointer events to keep track of the last used pointer type and update visual states
_secondaryButton.PointerEnter += Button_PointerEvent;
_secondaryButton.PointerLeave += Button_PointerEvent;
_secondaryButton.PointerPressed += Button_PointerEvent;
_secondaryButton.PointerReleased += Button_PointerEvent;
_secondaryButton.PointerCaptureLost += Button_PointerCaptureLost;
}
RegisterFlyoutEvents(Flyout);
UpdatePseudoClasses();
_hasLoaded = true;
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (Command != null)
{
Command.CanExecuteChanged += CanExecuteChanged;
CanExecuteChanged(this, EventArgs.Empty);
}
_isAttachedToLogicalTree = true;
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
if (Command != null)
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
_isAttachedToLogicalTree = false;
}
/// <inheritdoc/>
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> e)
{
if (e.Property == CommandProperty)
{
if (_isAttachedToLogicalTree)
{
// Must unregister events here while a reference to the old command still exists
if (e.OldValue.GetValueOrDefault() is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= CanExecuteChanged;
}
if (e.NewValue.GetValueOrDefault() 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 reference to the old flyout still exists
if (e.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout)
{
UnregisterFlyoutEvents(oldFlyout);
}
if (e.NewValue.GetValueOrDefault() is FlyoutBase newFlyout)
{
RegisterFlyoutEvents(newFlyout);
}
UpdatePseudoClasses();
}
base.OnPropertyChanged(e);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
var key = e.Key;
if (key == Key.Space || key == Key.Enter) // Key.GamepadA is not currently supported
{
_isKeyDown = true;
UpdatePseudoClasses();
}
base.OnKeyDown(e);
}
/// <inheritdoc/>
protected override void OnKeyUp(KeyEventArgs e)
{
var key = e.Key;
if (key == Key.Space || key == Key.Enter) // Key.GamepadA is not currently supported
{
_isKeyDown = false;
UpdatePseudoClasses();
// Consider this a click on the primary button
if (IsEffectivelyEnabled)
{
OnClickPrimary(null);
e.Handled = true;
}
}
else if (key == Key.Down && IsEffectivelyEnabled)
{
// WinUI requires the VirtualKey.Menu (alt) + VirtualKey.Down to open the flyout
// Avalonia will only require Key.Down which is better cross-platform
OpenFlyout();
e.Handled = true;
}
else if (key == Key.F4 && IsEffectivelyEnabled)
{
OpenFlyout();
e.Handled = true;
}
base.OnKeyUp(e);
}
/// <summary>
/// Invokes the <see cref="Click"/> event when the primary button part is clicked.
/// </summary>
/// <param name="e">The event args from the internal Click event.</param>
protected virtual void OnClickPrimary(RoutedEventArgs e)
{
// Note: It is not currently required to check enabled status; however, this is a failsafe
if (IsEffectivelyEnabled)
{
var eventArgs = new RoutedEventArgs(ClickEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
eventArgs.Handled = true;
}
}
}
/// <summary>
/// Invoked when the secondary button part is clicked.
/// </summary>
/// <param name="e">The event args from the internal Click event.</param>
protected virtual void OnClickSecondary(RoutedEventArgs e)
{
// Note: It is not currently required to check enabled status; however, this is a failsafe
if (IsEffectivelyEnabled)
{
OpenFlyout();
}
}
////////////////////////////////////////////////////////////////////////
// Event Handling
////////////////////////////////////////////////////////////////////////
/// <summary>
/// Event handler for when the internal primary button part is pressed.
/// </summary>
private void PrimaryButton_Click(object sender, RoutedEventArgs e)
{
OnClickPrimary(e);
}
/// <summary>
/// Event handler for when the internal secondary button part is pressed.
/// </summary>
private void SecondaryButton_Click(object sender, RoutedEventArgs e)
{
OnClickSecondary(e);
}
/// <summary>
/// Event handler for when pointer events occur in the primary or secondary buttons.
/// </summary>
private void Button_PointerEvent(object sender, PointerEventArgs e)
{
// Warning: Code must match with Button_PointerCaptureLost
if (_lastPointerType != e.Pointer.Type)
{
_lastPointerType = e.Pointer.Type;
UpdatePseudoClasses();
}
}
/// <summary>
/// Event handler for when the pointer capture is lost in the primary or secondary buttons.
/// </summary>
/// <remarks>
/// In upstream WinUI this is not a separate event handler. However, Avalonia has different args.
/// </remarks>
private void Button_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e)
{
// Warning: Code must match with Button_PointerEvent
if (_lastPointerType != e.Pointer.Type)
{
_lastPointerType = e.Pointer.Type;
UpdatePseudoClasses();
}
}
/// <summary>
/// Called when a primary or secondary button property changes that affects the visual states.
/// </summary>
private void Button_VisualPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdatePseudoClasses();
}
/// <summary>
/// Called when the <see cref="FlyoutBase.Placement"/> property changes.
/// </summary>
private void Flyout_PlacementPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdatePseudoClasses();
}
/// <summary>
/// Event handler for when the split button's flyout is opened.
/// </summary>
private void Flyout_Opened(object sender, EventArgs e)
{
var flyout = sender as FlyoutBase;
// It is possible to share flyouts among multiple controls including SplitButton.
// This can cause a problem here since all controls that share a flyout receive
// the same Opened/Closed events at the same time.
// For SplitButton that means they all would be updating their pseudoclasses accordingly.
// In other words, all SplitButtons with a shared Flyout would have the backgrounds changed together.
// To fix this, only continue here if the Flyout target matches this SplitButton instance.
if (object.ReferenceEquals(flyout?.Target, this))
{
_isFlyoutOpen = true;
UpdatePseudoClasses();
}
}
/// <summary>
/// Event handler for when the split button's flyout is closed.
/// </summary>
private void Flyout_Closed(object sender, EventArgs e)
{
var flyout = sender as FlyoutBase;
// See comments in Flyout_Opened
if (object.ReferenceEquals(flyout?.Target, this))
{
_isFlyoutOpen = false;
UpdatePseudoClasses();
}
}
}
}
Loading…
Cancel
Save