From 9ab31f60b40d676fbe1a876dbf70a5e498904ebf Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 5 Jul 2022 17:17:51 +0200 Subject: [PATCH 1/3] Fix button flyout toggle Expose OverlayDismissEventPassThrough on FlyoutBase --- src/Avalonia.Controls/Button.cs | 56 ++++++++++++++------- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 48 ++++++++++++++---- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6dba33516b8..8e5d4e1e064 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; @@ -281,24 +282,29 @@ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs /// protected override void OnKeyDown(KeyEventArgs e) { - if (e.Key == Key.Enter) + switch (e.Key) { - OnClick(); - e.Handled = true; - } - else if (e.Key == Key.Space) - { - if (ClickMode == ClickMode.Press) - { + case Key.Enter: OnClick(); + e.Handled = true; + break; + + case Key.Space: + { + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - } - else if (e.Key == Key.Escape && Flyout != null) - { - // If Flyout doesn't have focusable content, close the flyout here - Flyout.Hide(); + + case Key.Escape when Flyout != null: + // If Flyout doesn't have focusable content, close the flyout here + CloseFlyout(); + break; } base.OnKeyDown(e); @@ -327,7 +333,14 @@ protected virtual void OnClick() { if (IsEffectivelyEnabled) { - OpenFlyout(); + if (_isFlyoutOpen) + { + CloseFlyout(); + } + else + { + OpenFlyout(); + } var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); @@ -348,6 +361,14 @@ protected virtual void OpenFlyout() Flyout?.ShowAt(this); } + /// + /// Closes the button's flyout. + /// + protected virtual void CloseFlyout() + { + Flyout?.Hide(); + } + /// /// Invoked when the button's flyout is opened. /// @@ -494,8 +515,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang // If flyout is changed while one is already open, make sure we // close the old one first - if (oldFlyout != null && - oldFlyout.IsOpen) + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 1504d2b25f8..a0f3407b7a0 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -12,11 +12,6 @@ namespace Avalonia.Controls.Primitives { public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { - static FlyoutBase() - { - Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); - } - /// /// Defines the property /// @@ -49,6 +44,12 @@ static FlyoutBase() public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + /// + /// Defines the OverlayDismissEventPassThrough property + /// + public static readonly StyledProperty OverlayDismissEventPassThroughProperty = + Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; @@ -58,6 +59,12 @@ static FlyoutBase() private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + static FlyoutBase() + { + OverlayDismissEventPassThroughProperty.OverrideDefaultValue(true); + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } + public FlyoutBase() { _popupLazy = new Lazy(() => CreatePopup()); @@ -101,6 +108,21 @@ public Control? Target private set => SetAndRaise(TargetProperty, ref _target, value); } + /// + /// Gets or sets a value indicating whether the event that closes the flyout is passed + /// through to the parent window. + /// + /// + /// Clicks outside the the flyout cause the flyout to close. When is set to + /// false, these clicks will be handled by the flyout and not be registered by the parent + /// window. When set to true, the events will be passed through to the parent window. + /// + public bool OverlayDismissEventPassThrough + { + get => GetValue(OverlayDismissEventPassThroughProperty); + set => SetValue(OverlayDismissEventPassThroughProperty, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; event Action? IPopupHostProvider.PopupHostChanged @@ -175,6 +197,8 @@ protected virtual bool HideCore(bool canCancel = true) IsOpen = false; Popup.IsOpen = false; + Popup.OverlayInputPassThroughElement = null; + ((ISetLogicalParent)Popup).SetParent(null); // Ensure this isn't active @@ -231,6 +255,9 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = Popup.Child = CreatePresenter(); } + Popup.OverlayInputPassThroughElement = placementTarget; + Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; + if (CancelOpening()) { return false; @@ -356,10 +383,11 @@ protected virtual void OnClosed() private Popup CreatePopup() { - var popup = new Popup(); - popup.WindowManagerAddShadowHint = false; - popup.IsLightDismissEnabled = true; - popup.OverlayDismissEventPassThrough = true; + var popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true + }; popup.Opened += OnPopupOpened; popup.Closed += OnPopupClosed; @@ -372,7 +400,7 @@ private void OnPopupOpened(object? sender, EventArgs e) { IsOpen = true; - _popupHostChangedHandler?.Invoke(Popup!.Host); + _popupHostChangedHandler?.Invoke(Popup.Host); } private void OnPopupClosing(object? sender, CancelEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1501d97470d..3573ad9aaaf 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -501,7 +501,7 @@ public void Open() if (dismissLayer != null) { dismissLayer.IsVisible = true; - dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; + dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement; Disposable.Create(() => { From 44b8df516c461dbbc38f825c5dab9aa01ee94dd2 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:26:55 +0200 Subject: [PATCH 2/3] Set OverlayDismissEventPassThrough to false, add OverlayInputPassThroughElement property --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 50 ++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index a0f3407b7a0..bf938abc79e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -16,8 +16,8 @@ public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider /// Defines the property /// public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), - x => x.IsOpen); + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); /// /// Defines the property @@ -39,16 +39,18 @@ public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider x => x.ShowMode, (x, v) => x.ShowMode = v); /// - /// Defines the AttachedFlyout property + /// Defines the property /// - public static readonly AttachedProperty AttachedFlyoutProperty = - AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); /// - /// Defines the OverlayDismissEventPassThrough property + /// Defines the AttachedFlyout property /// - public static readonly StyledProperty OverlayDismissEventPassThroughProperty = - Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + public static readonly AttachedProperty AttachedFlyoutProperty = + AvaloniaProperty.RegisterAttached("AttachedFlyout", null); private readonly Lazy _popupLazy; private bool _isOpen; @@ -58,10 +60,10 @@ public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; static FlyoutBase() { - OverlayDismissEventPassThroughProperty.OverrideDefaultValue(true); Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); } @@ -109,25 +111,20 @@ public Control? Target } /// - /// Gets or sets a value indicating whether the event that closes the flyout is passed - /// through to the parent window. + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. /// - /// - /// Clicks outside the the flyout cause the flyout to close. When is set to - /// false, these clicks will be handled by the flyout and not be registered by the parent - /// window. When set to true, the events will be passed through to the parent window. - /// - public bool OverlayDismissEventPassThrough + public IInputElement? OverlayInputPassThroughElement { - get => GetValue(OverlayDismissEventPassThroughProperty); - set => SetValue(OverlayDismissEventPassThroughProperty, value); + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); } IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; remove => _popupHostChangedHandler -= value; } @@ -200,7 +197,7 @@ protected virtual bool HideCore(bool canCancel = true) Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null); - + // Ensure this isn't active _transientDisposable?.Dispose(); _transientDisposable = null; @@ -255,8 +252,7 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = Popup.Child = CreatePresenter(); } - Popup.OverlayInputPassThroughElement = placementTarget; - Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; + Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; if (CancelOpening()) { @@ -386,7 +382,9 @@ private Popup CreatePopup() var popup = new Popup { WindowManagerAddShadowHint = false, - IsLightDismissEnabled = true + IsLightDismissEnabled = true, + //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss. + OverlayDismissEventPassThrough = false }; popup.Opened += OnPopupOpened; From 29d957282ece12d6f497027ed4fa9d28cfce33fd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 29 Jul 2022 11:28:19 +0200 Subject: [PATCH 3/3] Remove unsetting --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index bf938abc79e..00ebcab70e6 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -194,7 +194,6 @@ protected virtual bool HideCore(bool canCancel = true) IsOpen = false; Popup.IsOpen = false; - Popup.OverlayInputPassThroughElement = null; ((ISetLogicalParent)Popup).SetParent(null);