diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index fdb04f4ade..78dc994df7 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls { ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); - MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); + MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } /// @@ -75,13 +75,14 @@ namespace Avalonia.Controls { if (control != null) { - if(_popup == null) + if (_popup == null) { _popup = new Popup() { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, - StaysOpen = false + StaysOpen = false, + ObeyScreenEdges = true }; _popup.Closed += PopupClosed; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b8817d28f3..656f3890cd 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -40,6 +40,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty ObeyScreenEdgesProperty = + AvaloniaProperty.Register(nameof(ObeyScreenEdges)); + /// /// Defines the property. /// @@ -136,6 +142,16 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary + /// when its opened at a position where it would otherwise overlap the screen edge. + /// + public bool ObeyScreenEdges + { + get => GetValue(ObeyScreenEdgesProperty); + set => SetValue(ObeyScreenEdgesProperty, value); + } + /// /// Gets or sets the Horizontal offset of the popup in relation to the /// @@ -216,12 +232,12 @@ namespace Avalonia.Controls.Primitives var window = _topLevel as Window; if (window != null) { - window.Deactivated += WindowDeactivated; + window.Deactivated += WindowDeactivated; } else { var parentPopuproot = _topLevel as PopupRoot; - if(parentPopuproot != null && parentPopuproot.Parent!=null) + if (parentPopuproot != null && parentPopuproot.Parent != null) { ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed; } @@ -234,13 +250,18 @@ namespace Avalonia.Controls.Primitives _popupRoot.Show(); + if (ObeyScreenEdges) + { + _popupRoot.SnapInsideScreenEdges(); + } + _ignoreIsOpenChanged = true; IsOpen = true; _ignoreIsOpenChanged = false; Opened?.Invoke(this, EventArgs.Empty); } - + /// /// Closes the popup. /// @@ -346,8 +367,10 @@ namespace Avalonia.Controls.Primitives /// The popup's position in screen coordinates. protected virtual Point GetPosition() { - return GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, + var result = GetPosition(PlacementTarget ?? this.GetVisualParent(), PlacementMode, PopupRoot, HorizontalOffset, VerticalOffset); + + return result; } internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset) @@ -399,8 +422,8 @@ namespace Avalonia.Controls.Primitives { if (!StaysOpen) { - if(!IsChildOrThis((IVisual)e.Source)) - { + if (!IsChildOrThis((IVisual)e.Source)) + { Close(); e.Handled = true; } @@ -412,12 +435,12 @@ namespace Avalonia.Controls.Primitives IVisual root = child.GetVisualRoot(); while (root is PopupRoot) { - if (root == PopupRoot) return true; + if (root == PopupRoot) return true; root = ((PopupRoot)root).Parent.GetVisualRoot(); } return false; } - + private void WindowDeactivated(object sender, EventArgs e) { if (!StaysOpen) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 507a085fed..457a7bd4b4 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,10 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; @@ -75,6 +77,30 @@ namespace Avalonia.Controls.Primitives /// public void Dispose() => PlatformImpl?.Dispose(); + /// + /// Moves the Popups position so that it doesnt overlap screen edges. + /// This method can be called immediately after Show has been called. + /// + public void SnapInsideScreenEdges() + { + var window = this.GetSelfAndLogicalAncestors().OfType().First(); + + var screen = window.Screens.ScreenFromPoint(Position); + + var screenX = Position.X + Bounds.Width - screen.Bounds.X; + var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; + + if (screenX > screen.Bounds.Width) + { + Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); + } + + if (screenY > screen.Bounds.Height) + { + Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + } + } + /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 66f226d2f6..efb31175fa 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -45,7 +45,8 @@ + IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}" + ObeyScreenEdges="True"> @@ -92,7 +93,8 @@ + StaysOpen="True" + ObeyScreenEdges="True">