diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt new file mode 100644 index 0000000000..0a2415a263 --- /dev/null +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -0,0 +1,7 @@ +Compat issues with assembly Avalonia.Controls: +MembersMustExist : Member 'protected void Avalonia.Controls.ComboBox.PopupClosedOverride(Avalonia.Controls.Primitives.PopupClosedEventArgs)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.Primitives.Popup.StaysOpenProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.add_Closed(System.EventHandler)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Controls.Primitives.Popup.remove_Closed(System.EventHandler)' does not exist in the implementation but it does exist in the contract. +TypesMustExist : Type 'Avalonia.Controls.Primitives.PopupClosedEventArgs' does not exist in the implementation but it does exist in the contract. +Total Issues: 5 diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 31101dc0f1..c164f282e8 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1647,7 +1647,7 @@ namespace Avalonia.Controls /// /// The source object. /// The event data. - private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e) + private void DropDownPopup_Closed(object sender, EventArgs e) { // Force the drop down dependency property to be false. if (IsDropDownOpen) @@ -1655,11 +1655,6 @@ namespace Avalonia.Controls IsDropDownOpen = false; } - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } - // Fire the DropDownClosed event if (_popupHasOpened) { diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index b987f065be..046b55d49a 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -889,17 +889,12 @@ namespace Avalonia.Controls _ignoreButtonClick = false; } } - private void PopUp_Closed(object sender, PopupClosedEventArgs e) + private void PopUp_Closed(object sender, EventArgs e) { IsDropDownOpen = false; if(!_isPopupClosing) { - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } - _isPopupClosing = true; Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 75a0e41e7b..27313b0b4c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -290,24 +290,6 @@ namespace Avalonia.Controls _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; - _popup.Closed += PopupClosed; - } - - /// - /// Called when the ComboBox popup is closed, with the - /// that caused the popup to close. - /// - /// The event args. - /// - /// This method can be overridden to control whether the event that caused the popup to close - /// is swallowed or passed through. - /// - protected virtual void PopupClosedOverride(PopupClosedEventArgs e) - { - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } } internal void ItemFocused(ComboBoxItem dropDownItem) @@ -318,13 +300,11 @@ namespace Avalonia.Controls } } - private void PopupClosed(object sender, PopupClosedEventArgs e) + private void PopupClosed(object sender, EventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; - PopupClosedOverride(e); - if (CanFocus(this)) { Focus(); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 5929dd39d4..b4e4dd5071 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -265,7 +265,8 @@ namespace Avalonia.Controls PlacementMode = PlacementMode, PlacementRect = PlacementRect, PlacementTarget = PlacementTarget ?? control, - StaysOpen = false + IsLightDismissEnabled = true, + OverlayDismissEventPassThrough = true, }; _popup.Opened += PopupOpened; diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs index ee9d0fd6b6..426f265084 100644 --- a/src/Avalonia.Controls/IMenuElement.cs +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Avalonia.Input; +#nullable enable + namespace Avalonia.Controls { /// @@ -11,7 +13,7 @@ namespace Avalonia.Controls /// /// Gets or sets the currently selected submenu item. /// - IMenuItem SelectedItem { get; set; } + IMenuItem? SelectedItem { get; set; } /// /// Gets the submenu items. diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs index 132d565cb7..94d761f725 100644 --- a/src/Avalonia.Controls/IMenuItem.cs +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls +#nullable enable + +namespace Avalonia.Controls { /// /// Represents a . @@ -29,7 +31,7 @@ /// /// Gets the parent . /// - new IMenuElement Parent { get; } + new IMenuElement? Parent { get; } /// /// Raises a click event on the menu item. diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6e0ad66699..1aa7945901 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -295,7 +295,7 @@ namespace Avalonia.Controls if (next != null) { - focus.Focus(next, NavigationMethod.Directional); + focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers); e.Handled = true; } diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3c21cd2c38..a085bfb6bc 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -136,7 +136,8 @@ namespace Avalonia.Controls e.Handled = UpdateSelectionFromEventSource( e.Source, true, - (e.KeyModifiers & KeyModifiers.Shift) != 0); + (e.KeyModifiers & KeyModifiers.Shift) != 0, + (e.KeyModifiers & KeyModifiers.Control) != 0); } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 7205af0e75..4da044fec1 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,9 +1,12 @@ using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; +#nullable enable + namespace Avalonia.Controls { /// @@ -14,6 +17,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); + private LightDismissOverlayLayer? _overlay; + /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 4554cb2bcf..0434928280 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -8,6 +8,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -51,9 +53,7 @@ namespace Avalonia.Controls /// The menu interaction handler. public MenuBase(IMenuInteractionHandler interactionHandler) { - Contract.Requires(interactionHandler != null); - - InteractionHandler = interactionHandler; + InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler)); } /// @@ -77,7 +77,7 @@ namespace Avalonia.Controls IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler; /// - IMenuItem IMenuElement.SelectedItem + IMenuItem? IMenuElement.SelectedItem { get { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 789e4f8926..b4d3272471 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; @@ -12,6 +13,8 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls { /// @@ -22,7 +25,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty CommandProperty = + public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( menuItem => menuItem.Command, (menuItem, command) => menuItem.Command = command, @@ -94,7 +97,7 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - private ICommand _command; + private ICommand? _command; private bool _commandCanExecute = true; private Popup _popup; @@ -118,6 +121,32 @@ namespace Avalonia.Controls public MenuItem() { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + // + // In addition to the hack from WPF, we also make sure to return null when we have + // no parent. If we don't do this, inheritance falls back to the logical tree, + // causing the shared size scope in the parent MenuItem to be used, breaking + // menu layout. + + var parentSharedSizeScope = this.GetObservable(VisualParentProperty) + .SelectMany(x => + { + var parent = x as Control; + return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? + Observable.Return(null); + }); + + this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); } /// @@ -165,7 +194,7 @@ namespace Avalonia.Controls /// /// Gets or sets the command associated with the menu item. /// - public ICommand Command + public ICommand? Command { get { return _command; } set { SetAndRaise(CommandProperty, ref _command, value); } @@ -245,7 +274,7 @@ namespace Avalonia.Controls bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// - IMenuElement IMenuItem.Parent => Parent as IMenuElement; + IMenuElement? IMenuItem.Parent => Parent as IMenuElement; protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; @@ -253,7 +282,7 @@ namespace Avalonia.Controls bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); /// - IMenuItem IMenuElement.SelectedItem + IMenuItem? IMenuElement.SelectedItem { get { @@ -324,27 +353,6 @@ namespace Avalonia.Controls } } - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - if (this.GetVisualParent() is IControl parent) - { - // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached - // property to store SharedSizeGroup state, except property inheritance is done - // down the logical tree. In this case, the control which is setting - // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing - // the way Grid stores shared size state, the developers of WPF just created a - // binding of the internal state of the visual parent to the menu item. We don't - // have much choice but to do the same for now unless we want to refactor Grid, - // which I honestly am not brave enough to do right now. Here's the same hack in - // the WPF codebase: - // - // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 - SetValue(DefinitionBase.PrivateSharedSizeScopeProperty, parent.GetValue(DefinitionBase.PrivateSharedSizeScopeProperty)); - } - } - /// /// Called when the is clicked. /// @@ -545,7 +553,7 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue) + if ((bool)e.NewValue!) { Focus(); } @@ -557,7 +565,7 @@ namespace Avalonia.Controls /// The property change event. private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e) { - var value = (bool)e.NewValue; + var value = (bool)e.NewValue!; if (value) { diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 2a7ea12d79..6d6398bcda 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -3,7 +3,6 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -235,7 +234,9 @@ namespace Avalonia.Controls.Platform // If the the parent is an IMenu which successfully moved its selection, // and the current menu is open then close the current menu and open the // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) + if (item.IsSubMenuOpen && + item.Parent is IMenu && + item.Parent.SelectedItem is object) { item.Close(); Open(item.Parent.SelectedItem, true); @@ -363,6 +364,11 @@ namespace Avalonia.Controls.Platform } else { + if (item.IsTopLevel && item.Parent is IMainMenu mainMenu) + { + mainMenu.Open(); + } + Open(item, false); } @@ -385,7 +391,7 @@ namespace Avalonia.Controls.Platform { if (e.Source == Menu) { - Menu.MoveSelection(NavigationDirection.First, true); + Menu?.MoveSelection(NavigationDirection.First, true); } } diff --git a/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs new file mode 100644 index 0000000000..752eedb68a --- /dev/null +++ b/src/Avalonia.Controls/Primitives/LightDismissOverlayLayer.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Rendering; +using Avalonia.Styling; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Controls.Primitives +{ + /// + /// A layer that is used to dismiss a when the user clicks outside. + /// + public class LightDismissOverlayLayer : Border, ICustomHitTest + { + public IInputElement? InputPassThroughElement { get; set; } + + /// + /// Returns the light dismiss overlay for a specified visual. + /// + /// The visual. + /// The light dismiss overlay, or null if none found. + public static LightDismissOverlayLayer? GetLightDismissOverlayLayer(IVisual visual) + { + visual = visual ?? throw new ArgumentNullException(nameof(visual)); + + VisualLayerManager? manager; + + if (visual is TopLevel topLevel) + { + manager = topLevel.GetTemplateChildren() + .OfType() + .FirstOrDefault(); + } + else + { + manager = visual.FindAncestorOfType(); + } + + return manager?.LightDismissOverlayLayer; + } + + public bool HitTest(Point point) + { + if (InputPassThroughElement is object) + { + var p = point.Transform(this.TransformToVisual(VisualRoot)!.Value); + var hit = VisualRoot.GetVisualAt(p, x => x != this); + + if (hit is object) + { + return !InputPassThroughElement.IsVisualAncestorOf(hit); + } + } + + return true; + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1fcf8d61bc..a676892384 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,12 +1,10 @@ using System; -using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; @@ -86,12 +84,27 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(ObeyScreenEdges), true); #pragma warning restore 618 + public static readonly StyledProperty OverlayDismissEventPassThroughProperty = + AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); + + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.RegisterDirect( + nameof(OverlayInputPassThroughElement), + o => o.OverlayInputPassThroughElement, + (o, v) => o.OverlayInputPassThroughElement = v); + /// /// Defines the property. /// public static readonly StyledProperty HorizontalOffsetProperty = AvaloniaProperty.Register(nameof(HorizontalOffset)); + /// + /// Defines the property. + /// + public static readonly StyledProperty IsLightDismissEnabledProperty = + AvaloniaProperty.Register(nameof(IsLightDismissEnabled)); + /// /// Defines the property. /// @@ -101,8 +114,13 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty StaysOpenProperty = - AvaloniaProperty.Register(nameof(StaysOpen), true); + [Obsolete("Use IsLightDismissEnabledProperty")] + public static readonly DirectProperty StaysOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(StaysOpen), + o => o.StaysOpen, + (o, v) => o.StaysOpen = v, + true); /// /// Defines the property. @@ -113,6 +131,7 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; + private IInputElement _overlayInputPassThroughElement; /// /// Initializes static members of the class. @@ -127,7 +146,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when the popup closes. /// - public event EventHandler? Closed; + public event EventHandler? Closed; /// /// Raised when the popup opens. @@ -165,6 +184,18 @@ namespace Avalonia.Controls.Primitives set; } + /// + /// Gets or sets a value that determines how the can be dismissed. + /// + /// + /// Light dismiss is when the user taps on any area other than the popup. + /// + public bool IsLightDismissEnabled + { + get => GetValue(IsLightDismissEnabledProperty); + set => SetValue(IsLightDismissEnabledProperty, value); + } + /// /// Gets or sets a value indicating whether the popup is currently open. /// @@ -246,6 +277,32 @@ namespace Avalonia.Controls.Primitives set => SetValue(ObeyScreenEdgesProperty, value); } + /// + /// Gets or sets a value indicating whether the event that closes the popup is passed + /// through to the parent window. + /// + /// + /// When is set to true, clicks outside the the popup + /// cause the popup to close. When is set to + /// false, these clicks will be handled by the popup 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); + } + + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the popup's overlay. + /// + public IInputElement OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + /// /// Gets or sets the Horizontal offset of the popup in relation to the . /// @@ -268,10 +325,11 @@ namespace Avalonia.Controls.Primitives /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. /// + [Obsolete("Use IsLightDismissEnabled")] public bool StaysOpen { - get { return GetValue(StaysOpenProperty); } - set { SetValue(StaysOpenProperty, value); } + get => !IsLightDismissEnabled; + set => IsLightDismissEnabled = !value; } /// @@ -363,14 +421,12 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, + DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, (x, handler) => x.Closed -= handler)); } } - DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); - DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => @@ -384,6 +440,29 @@ namespace Avalonia.Controls.Primitives state.popupHost.Dispose(); }); + if (IsLightDismissEnabled) + { + var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget); + + if (dismissLayer != null) + { + dismissLayer.IsVisible = true; + dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; + + DeferCleanup(Disposable.Create(() => + { + dismissLayer.IsVisible = false; + dismissLayer.InputPassThroughElement = null; + })); + + DeferCleanup(SubscribeToEventHandler>( + dismissLayer, + PointerPressedDismissOverlay, + (x, handler) => x.PointerPressed += handler, + (x, handler) => x.PointerPressed -= handler)); + } + } + _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -401,7 +480,7 @@ namespace Avalonia.Controls.Primitives /// /// Closes the popup. /// - public void Close() => CloseCore(null); + public void Close() => CloseCore(); /// /// Measures the control. @@ -471,7 +550,7 @@ namespace Avalonia.Controls.Primitives } } - private void CloseCore(EventArgs? closeEvent) + private void CloseCore() { if (_openState is null) { @@ -491,24 +570,46 @@ namespace Avalonia.Controls.Primitives IsOpen = false; } - Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); + Closed?.Invoke(this, EventArgs.Empty); } private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; - if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) + if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - CloseCore(e); + CloseCore(); } } - private void PointerPressedOutside(object sender, PointerPressedEventArgs e) + private void PointerPressedDismissOverlay(object sender, PointerPressedEventArgs e) { - if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) + if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v)) { - CloseCore(e); + CloseCore(); + + if (OverlayDismissEventPassThrough) + { + PassThroughEvent(e); + } + } + } + + private void PassThroughEvent(PointerPressedEventArgs e) + { + if (e.Source is LightDismissOverlayLayer layer && + layer.GetVisualRoot() is IInputElement root) + { + var p = e.GetCurrentPoint(root); + var hit = root.InputHitTest(p.Position, x => x != layer); + + if (hit != null) + { + e.Pointer.Capture(hit); + hit.RaiseEvent(e); + e.Handled = true; + } } } @@ -602,7 +703,7 @@ namespace Avalonia.Controls.Primitives private void WindowDeactivated(object sender, EventArgs e) { - if (!StaysOpen) + if (IsLightDismissEnabled) { Close(); } @@ -610,7 +711,7 @@ namespace Avalonia.Controls.Primitives private void ParentClosed(object sender, EventArgs e) { - if (!StaysOpen) + if (IsLightDismissEnabled) { Close(); } @@ -618,7 +719,7 @@ namespace Avalonia.Controls.Primitives private void WindowLostFocus() { - if(!StaysOpen) + if (IsLightDismissEnabled) Close(); } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs deleted file mode 100644 index c51543438c..0000000000 --- a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Avalonia.Interactivity; - -#nullable enable - -namespace Avalonia.Controls.Primitives -{ - /// - /// Holds data for the event. - /// - public class PopupClosedEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// - public PopupClosedEventArgs(EventArgs? closeEvent) - { - CloseEvent = closeEvent; - } - - /// - /// Gets the event that closed the popup, if any. - /// - /// - /// If is false, then this property will hold details of the - /// interaction that caused the popup to close if the close was caused by e.g. a pointer press - /// outside the popup. It can be used to mark the event as handled if the event should not - /// be propagated. - /// - public EventArgs? CloseEvent { get; } - } -} diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index ff83351190..d8d3450c6f 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Avalonia.LogicalTree; -using Avalonia.Metadata; +using Avalonia.Media; namespace Avalonia.Controls.Primitives { @@ -8,7 +8,8 @@ namespace Avalonia.Controls.Primitives { private const int AdornerZIndex = int.MaxValue - 100; private const int ChromeZIndex = int.MaxValue - 99; - private const int OverlayZIndex = int.MaxValue - 98; + private const int LightDismissOverlayZIndex = int.MaxValue - 98; + private const int OverlayZIndex = int.MaxValue - 97; private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); @@ -62,6 +63,27 @@ namespace Avalonia.Controls.Primitives } } + public LightDismissOverlayLayer LightDismissOverlayLayer + { + get + { + if (IsPopup) + return null; + var rv = FindLayer(); + if (rv == null) + { + rv = new LightDismissOverlayLayer + { + Background = Brushes.Transparent, + IsVisible = false + }; + + AddLayer(rv, LightDismissOverlayZIndex); + } + return rv; + } + } + T FindLayer() where T : class { foreach (var layer in _layers) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 59862da230..b009778f97 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -69,6 +69,8 @@ namespace Avalonia.DesignerSupport window = new Window() {Content = (Control)control}; } + Design.ApplyDesignModeProperties(window, control); + if (!window.IsSet(Window.SizeToContentProperty)) { if (double.IsNaN(window.Width)) @@ -83,7 +85,6 @@ namespace Avalonia.DesignerSupport } } window.Show(); - Design.ApplyDesignModeProperties(window, control); return window; } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index 4babe711f2..cbe36583e6 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Input { /// @@ -22,7 +24,7 @@ namespace Avalonia.Input /// public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); return element.GetVisualsAt(p, s_hitTestDelegate).Cast(); } @@ -33,13 +35,34 @@ namespace Avalonia.Input /// The element to test. /// The point on . /// The topmost at the specified position. - public static IInputElement InputHitTest(this IInputElement element, Point p) + public static IInputElement? InputHitTest(this IInputElement element, Point p) { - Contract.Requires(element != null); + element = element ?? throw new ArgumentNullException(nameof(element)); return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement; } + /// + /// Returns the topmost active input element at a point on an . + /// + /// The element to test. + /// The point on . + /// + /// A filter predicate. If the predicate returns false then the visual and all its + /// children will be excluded from the results. + /// + /// The topmost at the specified position. + public static IInputElement? InputHitTest( + this IInputElement element, + Point p, + Func filter) + { + element = element ?? throw new ArgumentNullException(nameof(element)); + filter = filter ?? throw new ArgumentNullException(nameof(filter)); + + return element.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement; + } + private static bool IsHitTestVisible(IVisual visual) { var element = visual as IInputElement; diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 788b60892b..66d0f17ede 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -19,7 +19,7 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" - StaysOpen="False"> + IsLightDismissEnabled="True"> + IsLightDismissEnabled="True"> diff --git a/src/Avalonia.Themes.Default/GridSplitter.xaml b/src/Avalonia.Themes.Default/GridSplitter.xaml index dc5cd002dc..6d9cb4f31f 100644 --- a/src/Avalonia.Themes.Default/GridSplitter.xaml +++ b/src/Avalonia.Themes.Default/GridSplitter.xaml @@ -2,8 +2,8 @@