Browse Source

Merge pull request #4500 from tig/v2_4488-PopoverMenu

Tig 2 days ago
parent
commit
9e0fa4c106

+ 2 - 0
Examples/UICatalog/Scenarios/Menus.cs

@@ -347,6 +347,8 @@ public class Menus : Scenario
             };
 
             ContextMenu.EnableForDesign (ref host);
+            Application.Popover.Register (ContextMenu);
+
             ContextMenu.Visible = false;
 
             // Demo of PopoverMenu as a context menu

+ 38 - 28
Terminal.Gui/App/IPopover.cs

@@ -8,54 +8,64 @@ namespace Terminal.Gui.App;
 ///     <para>
 ///         A popover is a transient UI element that appears above other content to display contextual information or UI,
 ///         such as menus, tooltips, or dialogs.
-///         Popovers are managed by <see cref="ApplicationPopover"/> and are typically shown using
-///         <see cref="ApplicationPopover.Show"/>.
 ///     </para>
 ///     <para>
-///         Popovers are not modal; they do not block input to the rest of the application, but they do receive focus and
-///         input events while visible.
-///         When a popover is shown, it is responsible for handling its own layout and content.
+///         <b>IMPORTANT:</b> Popovers must be registered with <see cref="Application.Popover"/> using
+///         <see cref="ApplicationPopover.Register"/> before they can be shown with <see cref="ApplicationPopover.Show"/>.
 ///     </para>
 ///     <para>
+///         <b>Lifecycle:</b><br/>
+///         When registered, the popover's lifetime is managed by the application. Registered popovers are
+///         automatically disposed when <see cref="Application.Shutdown"/> is called. Call
+///         <see cref="ApplicationPopover.DeRegister"/> to manage the lifetime directly.
+///     </para>
+///     <para>
+///         <b>Visibility and Hiding:</b><br/>
 ///         Popovers are automatically hidden when:
-///         <list type="bullet">
-///             <item>The user clicks outside the popover (unless occluded by a subview of the popover).</item>
-///             <item>The user presses <see cref="Application.QuitKey"/> (typically <c>Esc</c>).</item>
-///             <item>Another popover is shown.</item>
-///         </list>
 ///     </para>
+///     <list type="bullet">
+///         <item>The user clicks outside the popover (unless clicking on a subview).</item>
+///         <item>The user presses <see cref="Application.QuitKey"/> (typically <c>Esc</c>).</item>
+///         <item>Another popover is shown.</item>
+///         <item><see cref="View.Visible"/> is set to <see langword="false"/>.</item>
+///     </list>
 ///     <para>
 ///         <b>Focus and Input:</b><br/>
-///         When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-///         subview),
-///         presses <see cref="Application.QuitKey"/>, or another popover is shown, the popover will be hidden
-///         automatically.
+///         Popovers are not modal but do receive focus and input events while visible.
+///         Registered popovers receive keyboard events even when not visible, enabling global hotkey support.
 ///     </para>
 ///     <para>
 ///         <b>Layout:</b><br/>
-///         When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-///         this behavior
-///         by setting <see cref="View.Width"/> and <see cref="View.Height"/> in your derived class.
+///         When becoming visible, popovers are automatically laid out to fill the screen by default.
+///         Override <see cref="View.Width"/> and <see cref="View.Height"/> to customize size.
 ///     </para>
 ///     <para>
-///         <b>Mouse:</b><br/>
-///         Popovers are transparent to mouse events (see <see cref="ViewportSettingsFlags.TransparentMouse"/>),
-///         meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
+///         <b>Mouse Events:</b><br/>
+///         Popovers use <see cref="ViewportSettingsFlags.TransparentMouse"/>, meaning mouse events
+///         outside subviews are not captured.
 ///     </para>
 ///     <para>
-///         <b>Custom Popovers:</b><br/>
-///         To create a custom popover, inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
+///         <b>Creating Custom Popovers:</b><br/>
+///         Inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
 ///     </para>
 /// </remarks>
 public interface IPopover
 {
     /// <summary>
-    ///     Gets or sets the <see cref="Current"/> that this Popover is associated with. If null, it is not associated with
-    ///     any Runnable and will receive all keyboard
-    ///     events from the <see cref="IApplication"/>. If set, it will only receive keyboard events the Runnable would normally
-    ///     receive.
-    ///     When <see cref="ApplicationPopover.Register"/> is called, the <see cref="Current"/> is set to the current
-    ///     <see cref="IApplication.TopRunnableView"/> if not already set.
+    ///     Gets or sets the <see cref="IRunnable"/> that this popover is associated with.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If <see langword="null"/>, the popover is not associated with any runnable and will receive all keyboard
+    ///         events from the application.
+    ///     </para>
+    ///     <para>
+    ///         If set, the popover will only receive keyboard events when the associated runnable is active.
+    ///     </para>
+    ///     <para>
+    ///         When <see cref="ApplicationPopover.Register"/> is called, this property is automatically set to
+    ///         <see cref="IApplication.TopRunnableView"/> if not already set.
+    ///     </para>
+    /// </remarks>
     IRunnable? Current { get; set; }
 }

+ 39 - 27
Terminal.Gui/App/PopoverBaseImpl.cs

@@ -2,36 +2,36 @@
 namespace Terminal.Gui.App;
 
 /// <summary>
-///     Abstract base class for popover views in Terminal.Gui.
+///     Abstract base class for popover views in Terminal.Gui. Implements <see cref="IPopover"/>.
 /// </summary>
 /// <remarks>
 ///     <para>
-///         <b>Popover Lifecycle:</b><br/>
-///         To display a popover, use <see cref="ApplicationPopover.Show"/>. To hide a popover, either call
-///         <see cref="ApplicationPopover.Hide"/>,
-///         set <see cref="View.Visible"/> to <see langword="false"/>, or show another popover.
+///         <b>IMPORTANT:</b> Popovers must be registered with <see cref="Application.Popover"/> using
+///         <see cref="ApplicationPopover.Register"/> before they can be shown.
 ///     </para>
 ///     <para>
-///         <b>Focus and Input:</b><br/>
-///         When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-///         subview),
-///         presses <see cref="Application.QuitKey"/>, or another popover is shown, the popover will be hidden
-///         automatically.
+///         <b>Requirements:</b><br/>
+///         Derived classes must:
 ///     </para>
+///     <list type="bullet">
+///         <item>Set <see cref="View.ViewportSettings"/> to include <see cref="ViewportSettingsFlags.Transparent"/> and <see cref="ViewportSettingsFlags.TransparentMouse"/>.</item>
+///         <item>Add a key binding for <see cref="Command.Quit"/> (typically bound to <see cref="Application.QuitKey"/>).</item>
+///     </list>
 ///     <para>
-///         <b>Layout:</b><br/>
-///         When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-///         this behavior
-///         by setting <see cref="View.Width"/> and <see cref="View.Height"/> in your derived class.
+///         <b>Default Behavior:</b><br/>
+///         This base class provides:
 ///     </para>
+///     <list type="bullet">
+///         <item>Fills the screen by default (<see cref="View.Width"/> = <see cref="Dim.Fill"/>, <see cref="View.Height"/> = <see cref="Dim.Fill"/>).</item>
+///         <item>Transparent viewport settings for proper mouse event handling.</item>
+///         <item>Automatic layout when becoming visible.</item>
+///         <item>Focus restoration when hidden.</item>
+///         <item>Default <see cref="Command.Quit"/> implementation that hides the popover.</item>
+///     </list>
 ///     <para>
-///         <b>Mouse:</b><br/>
-///         Popovers are transparent to mouse events (see <see cref="ViewportSettingsFlags.TransparentMouse"/>),
-///         meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
-///     </para>
-///     <para>
-///         <b>Custom Popovers:</b><br/>
-///         To create a custom popover, inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
+///         <b>Lifecycle:</b><br/>
+///         Use <see cref="ApplicationPopover.Show"/> to display and <see cref="ApplicationPopover.Hide"/> or
+///         set <see cref="View.Visible"/> to <see langword="false"/> to hide.
 ///     </para>
 /// </remarks>
 public abstract class PopoverBaseImpl : View, IPopover
@@ -40,7 +40,15 @@ public abstract class PopoverBaseImpl : View, IPopover
     ///     Initializes a new instance of the <see cref="PopoverBaseImpl"/> class.
     /// </summary>
     /// <remarks>
-    ///     By default, the popover fills the available screen area and is focusable.
+    ///     <para>
+    ///         Sets up default popover behavior:
+    ///     </para>
+    ///     <list type="bullet">
+    ///         <item>Fills the screen (<see cref="View.Width"/> = <see cref="Dim.Fill"/>, <see cref="View.Height"/> = <see cref="Dim.Fill"/>).</item>
+    ///         <item>Sets <see cref="View.CanFocus"/> to <see langword="true"/>.</item>
+    ///         <item>Configures <see cref="View.ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/> and <see cref="ViewportSettingsFlags.TransparentMouse"/>.</item>
+    ///         <item>Adds <see cref="Command.Quit"/> bound to <see cref="Application.QuitKey"/> which hides the popover when invoked.</item>
+    ///     </list>
     /// </remarks>
     protected PopoverBaseImpl ()
     {
@@ -87,15 +95,19 @@ public abstract class PopoverBaseImpl : View, IPopover
     }
 
     /// <summary>
-    ///     Called when the <see cref="View.Visible"/> property is changing.
+    ///     Called when the <see cref="View.Visible"/> property is changing. Handles layout and focus management.
     /// </summary>
-    /// <remarks>
-    ///     When becoming visible, the popover is laid out to fit the screen.
-    ///     When becoming hidden, focus is restored to the previous view.
-    /// </remarks>
     /// <returns>
     ///     <see langword="true"/> to cancel the visibility change; otherwise, <see langword="false"/>.
     /// </returns>
+    /// <remarks>
+    ///     <para>
+    ///         <b>When becoming visible:</b> Lays out the popover to fit the screen.
+    ///     </para>
+    ///     <para>
+    ///         <b>When becoming hidden:</b> Restores focus to the previously focused view in the view hierarchy.
+    ///     </para>
+    /// </remarks>
     protected override bool OnVisibleChanging ()
     {
         bool ret = base.OnVisibleChanging ();

+ 160 - 41
Terminal.Gui/Views/Menu/PopoverMenu.cs

@@ -1,16 +1,29 @@
-
-
 namespace Terminal.Gui.Views;
 
 /// <summary>
-///     Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down
-///     all other content. Can be used as a context menu or a drop-down
-///     menu as part of <see cref="MenuBar"/> as part of <see cref="MenuBar"/>.
+///     A <see cref="PopoverBaseImpl"/>-derived view that provides a cascading menu.
+///     Can be used as a context menu or a drop-down menu as part of <see cref="MenuBar"/>.
 /// </summary>
 /// <remarks>
 ///     <para>
-///         To use as a context menu, register the popover menu with <see cref="IApplication.Popover"/> and call
-///         <see cref="MakeVisible"/>.
+///         <b>IMPORTANT:</b> Must be registered with <see cref="Application.Popover"/> via
+///         <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/> or
+///         <see cref="ApplicationPopover.Show"/>.
+///     </para>
+///     <para>
+///         <b>Usage Example:</b>
+///     </para>
+///     <code>
+///         var menu = new PopoverMenu ([
+///             new MenuItem ("Cut", Command.Cut),
+///             new MenuItem ("Copy", Command.Copy),
+///             new MenuItem ("Paste", Command.Paste)
+///         ]);
+///         Application.Popover?.Register (menu);
+///         menu.MakeVisible (); // or Application.Popover?.Show (menu);
+///     </code>
+///     <para>
+///         See <see cref="PopoverBaseImpl"/> and <see cref="IPopover"/> for lifecycle, focus, and keyboard handling details.
 ///     </para>
 /// </remarks>
 public class PopoverMenu : PopoverBaseImpl, IDesignable
@@ -22,9 +35,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="PopoverMenu"/> class. If any of the elements of
-    ///     <paramref name="menuItems"/> is <see langword="null"/>,
-    ///     a see <see cref="Line"/> will be created instead.
+    ///     <paramref name="menuItems"/> is <see langword="null"/>, a <see cref="Line"/> will be created instead.
     /// </summary>
+    /// <param name="menuItems">The views to use as menu items. Null elements become separator lines.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (IEnumerable<View>? menuItems) : this (
                                                               new Menu (menuItems?.Select (item => item ?? new Line ()))
                                                               {
@@ -32,17 +48,27 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
                                                               })
     { }
 
-    /// <inheritdoc/>
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="PopoverMenu"/> class with the specified menu items.
+    /// </summary>
+    /// <param name="menuItems">The menu items to display in the popover.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (IEnumerable<MenuItem>? menuItems) : this (
-                                                                    new Menu (menuItems)
-                                                                    {
-                                                                        Title = "Popover Root"
-                                                                    })
+                                                                  new Menu (menuItems)
+                                                                  {
+                                                                      Title = "Popover Root"
+                                                                  })
     { }
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="PopoverMenu"/> class with the specified root <see cref="Menu"/>.
     /// </summary>
+    /// <param name="root">The root menu that contains the top-level menu items.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (Menu? root)
     {
         // Do this to support debugging traces where Title gets set
@@ -132,7 +158,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     private Key _key = DefaultKey;
 
-    /// <summary>Specifies the key that will activate the context menu.</summary>
+    /// <summary>
+    ///     Gets or sets the key that will activate the popover menu when it is registered but not visible.
+    /// </summary>
+    /// <remarks>
+    ///     This key binding works as a global hotkey when the popover is registered with
+    ///     <see cref="Application.Popover"/>. The default value is <see cref="DefaultKey"/> (<see cref="Key.F10"/> with
+    ///     Shift).
+    /// </remarks>
     public Key Key
     {
         get => _key;
@@ -144,10 +177,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         }
     }
 
-    /// <summary>Raised when <see cref="Key"/> is changed.</summary>
+    /// <summary>
+    ///     Raised when the <see cref="Key"/> property is changed.
+    /// </summary>
     public event EventHandler<KeyChangedEventArgs>? KeyChanged;
 
-    /// <summary>The default key for activating popover menus.</summary>
+    /// <summary>
+    ///     Gets or sets the default key for activating popover menus. The default value is <see cref="Key.F10"/> with Shift.
+    /// </summary>
+    /// <remarks>
+    ///     This is a configuration property that affects all new <see cref="PopoverMenu"/> instances.
+    /// </remarks>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key DefaultKey { get; set; } = Key.F10.WithShift;
 
@@ -159,12 +199,25 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     /// <summary>
     ///     Makes the popover menu visible and locates it at <paramref name="idealScreenPosition"/>. The actual position of the
-    ///     menu
-    ///     will be adjusted to
-    ///     ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
-    ///     first MenuItem.
+    ///     menu will be adjusted to ensure the menu fully fits on the screen, with the mouse cursor positioned over
+    ///     the first cell of the first <see cref="MenuItem"/>.
     /// </summary>
-    /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
+    /// <param name="idealScreenPosition">
+    ///     The ideal screen-relative position for the menu. If <see langword="null"/>, the current mouse position will be
+    ///     used.
+    /// </param>
+    /// <remarks>
+    ///     <para>
+    ///         IMPORTANT: The popover must be registered with <see cref="Application.Popover"/> before calling this
+    ///         method.
+    ///         Call <see cref="ApplicationPopover.Register"/> first.
+    ///     </para>
+    ///     <para>
+    ///         This method internally calls <see cref="ApplicationPopover.Show"/>, which will throw
+    ///         <see cref="InvalidOperationException"/> if the popover is not registered.
+    ///     </para>
+    /// </remarks>
+    /// <exception cref="InvalidOperationException">Thrown if the popover has not been registered.</exception>
     public void MakeVisible (Point? idealScreenPosition = null)
     {
         if (Visible)
@@ -180,12 +233,18 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Locates the popover menu at <paramref name="idealScreenPosition"/>. The actual position of the menu will be
-    ///     adjusted to
-    ///     ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
-    ///     first MenuItem (if possible).
+    ///     Sets the position of the popover menu at <paramref name="idealScreenPosition"/>. The actual position will be
+    ///     adjusted to ensure the menu fully fits on the screen, with the mouse cursor positioned over the first cell of
+    ///     the first <see cref="MenuItem"/> (if possible).
     /// </summary>
-    /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
+    /// <param name="idealScreenPosition">
+    ///     The ideal screen-relative position for the menu. If <see langword="null"/>, the current mouse position will be
+    ///     used.
+    /// </param>
+    /// <remarks>
+    ///     This method only sets the position; it does not make the popover visible. Use <see cref="MakeVisible"/> to
+    ///     both position and show the popover.
+    /// </remarks>
     public void SetPosition (Point? idealScreenPosition = null)
     {
         idealScreenPosition ??= App?.Mouse.LastMousePosition;
@@ -212,6 +271,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     When becoming visible, the root menu is added and shown. When becoming hidden, the root menu is removed
+    ///     and the popover is hidden via <see cref="ApplicationPopover.Hide"/>.
+    /// </remarks>
     protected override void OnVisibleChanged ()
     {
         // Logging.Debug ($"{Title} - Visible: {Visible}");
@@ -231,8 +294,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     private Menu? _root;
 
     /// <summary>
-    ///     Gets or sets the <see cref="Menu"/> that is the root of the Popover Menu.
+    ///     Gets or sets the <see cref="Menu"/> that is the root of the popover menu hierarchy.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The root menu contains the top-level menu items. Setting this property updates key bindings and
+    ///         event subscriptions for all menus in the hierarchy.
+    ///     </para>
+    ///     <para>
+    ///         When set, all submenus are configured with appropriate event handlers for selection and acceptance.
+    ///     </para>
+    /// </remarks>
     public Menu? Root
     {
         get => _root;
@@ -306,6 +378,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     This method checks all menu items in the hierarchy for a matching key binding and invokes the
+    ///     appropriate menu item if found.
+    /// </remarks>
     protected override bool OnKeyDownNotHandled (Key key)
     {
         // See if any of our MenuItems have this key as Key
@@ -325,9 +401,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets all the submenus in the PopoverMenu.
+    ///     Gets all the submenus in the popover menu hierarchy, including the root menu.
     /// </summary>
-    /// <returns></returns>
+    /// <returns>An enumerable collection of all <see cref="Menu"/> instances in the hierarchy.</returns>
+    /// <remarks>
+    ///     This method performs a depth-first traversal of the menu tree, starting from <see cref="Root"/>.
+    /// </remarks>
     public IEnumerable<Menu> GetAllSubMenus ()
     {
         List<Menu> result = [];
@@ -358,9 +437,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets all the MenuItems in the PopoverMenu.
+    ///     Gets all the menu items in the popover menu hierarchy.
     /// </summary>
-    /// <returns></returns>
+    /// <returns>An enumerable collection of all <see cref="MenuItem"/> instances across all menus in the hierarchy.</returns>
+    /// <remarks>
+    ///     This method traverses all menus returned by <see cref="GetAllSubMenus"/> and collects their menu items.
+    /// </remarks>
     internal IEnumerable<MenuItem> GetMenuItemsOfAllSubMenus ()
     {
         List<MenuItem> result = [];
@@ -380,9 +462,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Pops up the submenu of the specified MenuItem, if there is one.
+    ///     Shows the submenu of the specified <see cref="MenuItem"/>, if it has one.
     /// </summary>
-    /// <param name="menuItem"></param>
+    /// <param name="menuItem">The menu item whose submenu should be shown.</param>
+    /// <remarks>
+    ///     <para>
+    ///         If another submenu is currently visible at the same level, it will be hidden before showing the new one.
+    ///     </para>
+    ///     <para>
+    ///         The submenu is positioned to the right of the menu item, adjusted to ensure full visibility on screen.
+    ///     </para>
+    /// </remarks>
     internal void ShowSubMenu (MenuItem? menuItem)
     {
         var menu = menuItem?.SuperView as Menu;
@@ -416,11 +506,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets the most visible screen-relative location for <paramref name="menu"/>.
+    ///     Calculates the most visible screen-relative location for the specified <paramref name="menu"/>.
     /// </summary>
-    /// <param name="menu">The menu to locate.</param>
-    /// <param name="idealLocation">Ideal screen-relative location.</param>
-    /// <returns></returns>
+    /// <param name="menu">The menu to position.</param>
+    /// <param name="idealLocation">The ideal screen-relative location.</param>
+    /// <returns>The adjusted screen-relative position that ensures maximum visibility of the menu.</returns>
+    /// <remarks>
+    ///     This method adjusts the position to keep the menu fully visible on screen, considering screen boundaries.
+    /// </remarks>
     internal Point GetMostVisibleLocationForSubMenu (Menu menu, Point idealLocation)
     {
         var pos = Point.Empty;
@@ -489,6 +582,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     private void MenuOnAccepting (object? sender, CommandEventArgs e)
     {
         var senderView = sender as View;
+
         // Logging.Debug ($"{Title} ({e.Context?.Source?.Title}) Command: {e.Context?.Command} - Sender: {senderView?.GetType ().Name}");
 
         if (e.Context?.Command != Command.HotKey)
@@ -524,6 +618,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     <para>
+    ///         When the popover is not visible, only hotkey commands are processed.
+    ///     </para>
+    ///     <para>
+    ///         This method raises <see cref="View.Accepted"/> for commands that originate from menu items in the hierarchy.
+    ///     </para>
+    /// </remarks>
     protected override bool OnAccepting (CommandEventArgs args)
     {
         // Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}");
@@ -560,8 +662,6 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         return false;
     }
 
-
-
     private void MenuOnSelectedMenuItemChanged (object? sender, MenuItem? e)
     {
         // Logging.Debug ($"{Title} - e.Title: {e?.Title}");
@@ -569,6 +669,13 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <exception cref="InvalidOperationException">
+    ///     Thrown if attempting to add a <see cref="Menu"/> or <see cref="MenuItem"/> directly to the popover.
+    /// </exception>
+    /// <remarks>
+    ///     Do not add <see cref="MenuItem"/> or <see cref="Menu"/> views directly to the popover.
+    ///     Use the <see cref="Root"/> property instead.
+    /// </remarks>
     protected override void OnSubViewAdded (View view)
     {
         if (Root is null && (view is Menu || view is MenuItem))
@@ -580,6 +687,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     This method unsubscribes from all menu events and disposes the root menu.
+    /// </remarks>
     protected override void Dispose (bool disposing)
     {
         if (disposing)
@@ -600,7 +710,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         base.Dispose (disposing);
     }
 
-    /// <inheritdoc/>
+    /// <summary>
+    ///     Enables the popover menu for use in design-time scenarios.
+    /// </summary>
+    /// <typeparam name="TContext">The type of the target view context.</typeparam>
+    /// <param name="targetView">The target view to associate with the menu commands.</param>
+    /// <returns><see langword="true"/> if successfully enabled for design; otherwise, <see langword="false"/>.</returns>
+    /// <remarks>
+    ///     This method creates a default set of menu items (Cut, Copy, Paste, Select All, Quit) for design-time use.
+    ///     It is primarily used for demonstration and testing purposes.
+    /// </remarks>
     public bool EnableForDesign<TContext> (ref TContext targetView) where TContext : notnull
     {
         // Note: This menu is used by unit tests. If you modify it, you'll likely have to update

+ 361 - 11
docfx/docs/Popovers.md

@@ -1,18 +1,368 @@
 # Popovers Deep Dive
 
-Normally Views cannot draw outside of their `Viewport`. Options for influencing content outside of the `Viewport` include:
+Popovers are transient UI elements that appear above other content to display contextual information, such as menus, tooltips, autocomplete suggestions, and dialog boxes. Terminal.Gui's popover system provides a flexible, non-modal way to present temporary UI without blocking the rest of the application.
 
-1) Modifying the `Border` behavior
-2) Modifying the `Margin` behavior
-3) Using @Terminal.Gui.App.Application.Popover
+## Overview
 
-Popovers are useful for scenarios such as menus, autocomplete popups, and drop-down combo boxes.
+Normally, Views cannot draw outside of their `Viewport`. To display content that appears to "pop over" other views, Terminal.Gui provides the popover system via @Terminal.Gui.App.Application.Popover. Popovers differ from alternatives like modifying `Border` or `Margin` behavior because they:
 
-A `Popover` is any View that meets these characteristics:
+- Are managed centrally by the application
+- Support focus and keyboard event routing
+- Automatically hide in response to user actions
+- Can receive global hotkeys even when not visible
 
-- Implements the @Terminal.Gui.App.IPopover interface 
-- Is Focusable (`CetFocus = true`)
-- Is Transparent (`ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse`
-- Sets `Visible = false` when it receives `Application.QuitKey`
+## Creating a Popover
 
[email protected] provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBar.
+### Using PopoverMenu
+
+The easiest way to create a popover is to use @Terminal.Gui.Views.PopoverMenu, which provides a cascading menu implementation:
+
+```csharp
+// Create a popover menu with menu items
+PopoverMenu contextMenu = new ([
+    new MenuItem ("Cut", Command.Cut),
+    new MenuItem ("Copy", Command.Copy),
+    new MenuItem ("Paste", Command.Paste),
+    new MenuItem ("Select All", Command.SelectAll)
+]);
+
+// IMPORTANT: Register before showing
+Application.Popover?.Register (contextMenu);
+
+// Show at mouse position or specific location
+contextMenu.MakeVisible (); // Uses current mouse position
+// OR
+contextMenu.MakeVisible (new Point (10, 5)); // Specific location
+```
+
+### Creating a Custom Popover
+
+To create a custom popover, inherit from @Terminal.Gui.App.PopoverBaseImpl:
+
+```csharp
+public class MyCustomPopover : PopoverBaseImpl
+{
+    public MyCustomPopover ()
+    {
+        // PopoverBaseImpl already sets up required defaults:
+        // - ViewportSettings with Transparent and TransparentMouse flags
+        // - Command.Quit binding to hide the popover
+        // - Width/Height set to Dim.Fill()
+        
+        // Add your custom content
+        Label label = new () { Text = "Custom Popover Content" };
+        Add (label);
+        
+        // Optionally override size
+        Width = 40;
+        Height = 10;
+    }
+}
+
+// Usage:
+MyCustomPopover myPopover = new ();
+Application.Popover?.Register (myPopover);
+Application.Popover?.Show (myPopover);
+```
+
+## Popover Requirements
+
+A View qualifies as a popover if it:
+
+1. **Implements @Terminal.Gui.App.IPopover** - Provides the `Current` property for runnable association
+2. **Is Focusable** - `CanFocus = true` to receive keyboard input
+3. **Is Transparent** - `ViewportSettings` includes both:
+   - `ViewportSettings.Transparent` - Allows content beneath to show through
+   - `ViewportSettings.TransparentMouse` - Mouse clicks outside subviews pass through
+4. **Handles Quit** - Binds `Application.QuitKey` to `Command.Quit` and sets `Visible = false`
+
[email protected] provides all these requirements by default.
+
+## Registration and Lifecycle
+
+### Registration (REQUIRED)
+
+**All popovers must be registered before they can be shown:**
+
+```csharp
+PopoverMenu popover = new ([...]);
+
+// REQUIRED: Register with the application
+Application.Popover?.Register (popover);
+
+// Now you can show it
+Application.Popover?.Show (popover);
+// OR
+popover.MakeVisible (); // For PopoverMenu
+```
+
+**Why Registration is Required:**
+- Enables keyboard event routing to the popover
+- Allows global hotkeys to work even when popover is hidden
+- Manages popover lifecycle and disposal
+
+### Showing and Hiding
+
+**Show a popover:**
+```csharp
+Application.Popover?.Show (popover);
+```
+
+**Hide a popover:**
+```csharp
+// Method 1: Via ApplicationPopover
+Application.Popover?.Hide (popover);
+
+// Method 2: Set Visible property
+popover.Visible = false;
+
+// Automatic hiding occurs when:
+// - User presses Application.QuitKey (typically Esc)
+// - User clicks outside the popover (not on a subview)
+// - Another popover is shown
+```
+
+### Lifecycle Management
+
+**Registered popovers:**
+- Have their lifetime managed by the application
+- Are automatically disposed when `Application.Shutdown ()` is called
+- Receive keyboard events based on their associated runnable
+
+**To manage lifetime manually:**
+```csharp
+// Deregister to take ownership of disposal
+Application.Popover?.DeRegister (popover);
+
+// Now you're responsible for disposal
+popover.Dispose ();
+```
+
+## Keyboard Event Routing
+
+### Global Hotkeys
+
+Registered popovers receive keyboard events even when not visible, enabling global hotkey support:
+
+```csharp
+PopoverMenu menu = new ([...]);
+menu.Key = Key.F10.WithShift; // Default hotkey
+
+Application.Popover?.Register (menu);
+
+// Now pressing Shift+F10 anywhere in the app will show the menu
+```
+
+### Runnable Association
+
+The @Terminal.Gui.App.IPopover.Current property associates a popover with a specific @Terminal.Gui.IRunnable:
+
+- If `null`: Popover receives all keyboard events from the application
+- If set: Popover only receives events when the associated runnable is active
+- Automatically set to `Application.TopRunnableView` during registration
+
+```csharp
+// Associate with a specific runnable
+myPopover.Current = myWindow; // Only active when myWindow is the top runnable
+```
+
+## Focus and Input
+
+**When visible:**
+- Popovers receive focus automatically
+- All keyboard input goes to the popover until hidden
+- Mouse clicks on subviews are captured
+- Mouse clicks outside subviews pass through (due to `TransparentMouse`)
+
+**When hidden:**
+- Only registered hotkeys are processed
+- Other keyboard input is not captured
+
+## Layout and Positioning
+
+### Default Layout
+
[email protected] sets `Width = Dim.Fill ()` and `Height = Dim.Fill ()`, making the popover fill the screen by default. The transparent viewport settings allow content beneath to remain visible.
+
+### Custom Sizing
+
+Override `Width` and `Height` to customize size:
+
+```csharp
+public class MyPopover : PopoverBaseImpl
+{
+    public MyPopover ()
+    {
+        Width = 40;  // Fixed width
+        Height = Dim.Auto (); // Auto height based on content
+    }
+}
+```
+
+### Positioning with PopoverMenu
+
[email protected] provides positioning helpers:
+
+```csharp
+// Position at specific screen coordinates
+menu.SetPosition (new Point (10, 5));
+
+// Show and position in one call
+menu.MakeVisible (new Point (10, 5));
+
+// Uses mouse position if null
+menu.MakeVisible (); // Uses Application.Mouse.LastMousePosition
+```
+
+The menu automatically adjusts position to ensure it remains fully visible on screen.
+
+## Built-in Popover Types
+
+### PopoverMenu
+
[email protected] is a sophisticated cascading menu implementation used for:
+- Context menus
+- @Terminal.Gui.MenuBar drop-down menus
+- Custom menu scenarios
+
+**Key Features:**
+- Cascading submenus with automatic positioning
+- Keyboard navigation (arrow keys, hotkeys)
+- Automatic key binding from Commands
+- Mouse support
+- Separator lines via `new Line ()`
+
+**Example with submenus:**
+```csharp
+PopoverMenu fileMenu = new ([
+    new MenuItem ("New", Command.New),
+    new MenuItem ("Open", Command.Open),
+    new MenuItem {
+        Title = "Recent",
+        SubMenu = new Menu ([
+            new MenuItem ("File1.txt", Command.Open),
+            new MenuItem ("File2.txt", Command.Open)
+        ])
+    },
+    new Line (),
+    new MenuItem ("Exit", Command.Quit)
+]);
+
+Application.Popover?.Register (fileMenu);
+fileMenu.MakeVisible ();
+```
+
+## Mouse Event Handling
+
+Popovers use `ViewportSettings.TransparentMouse`, which means:
+
+- **Clicks on popover subviews**: Captured and handled normally
+- **Clicks outside subviews**: Pass through to views beneath
+- **Clicks on background**: Automatically hide the popover
+
+This creates the expected behavior where clicking outside a menu or dialog closes it.
+
+## Best Practices
+
+1. **Always Register First**
+   ```csharp
+   // WRONG - Will throw InvalidOperationException
+   PopoverMenu menu = new ([...]);
+   menu.MakeVisible ();
+   
+   // CORRECT
+   PopoverMenu menu = new ([...]);
+   Application.Popover?.Register (menu);
+   menu.MakeVisible ();
+   ```
+
+2. **Use PopoverMenu for Menus**
+   - Don't reinvent the wheel for standard menu scenarios
+   - Leverage built-in keyboard navigation and positioning
+
+3. **Manage Lifecycle Appropriately**
+   - Let the application manage disposal for long-lived popovers
+   - Deregister and manually dispose short-lived or conditional popovers
+
+4. **Test Global Hotkeys**
+   - Ensure hotkeys don't conflict with application-level keys
+   - Consider providing configuration for custom hotkeys
+
+5. **Handle Edge Cases**
+   - Test positioning near screen edges
+   - Verify behavior with multiple runnables
+   - Test with keyboard-only navigation
+
+## Common Scenarios
+
+### Context Menu on Right-Click
+
+```csharp
+PopoverMenu contextMenu = new ([...]);
+contextMenu.MouseFlags = MouseFlags.Button3Clicked; // Right-click
+Application.Popover?.Register (contextMenu);
+
+myView.MouseClick += (s, e) =>
+{
+    if (e.MouseEvent.Flags == MouseFlags.Button3Clicked)
+    {
+        contextMenu.MakeVisible (myView.ScreenToViewport (e.MouseEvent.Position));
+        e.Handled = true;
+    }
+};
+```
+
+### Autocomplete Popup
+
+```csharp
+public class AutocompletePopover : PopoverBaseImpl
+{
+    private ListView _listView;
+    
+    public AutocompletePopover ()
+    {
+        Width = 30;
+        Height = 10;
+        
+        _listView = new ListView
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
+        Add (_listView);
+    }
+    
+    public void ShowSuggestions (IEnumerable<string> suggestions, Point position)
+    {
+        _listView.SetSource (suggestions.ToList ());
+        // Position below the text entry field
+        X = position.X;
+        Y = position.Y + 1;
+        Visible = true;
+    }
+}
+```
+
+### Global Command Palette
+
+```csharp
+PopoverMenu commandPalette = new (GetAllCommands ());
+commandPalette.Key = Key.P.WithCtrl; // Ctrl+P to show
+
+Application.Popover?.Register (commandPalette);
+
+// Now Ctrl+P anywhere in the app shows the command palette
+```
+
+## API Reference
+
+- @Terminal.Gui.App.IPopover - Interface for popover views
+- @Terminal.Gui.App.PopoverBaseImpl - Abstract base class for custom popovers
+- @Terminal.Gui.Views.PopoverMenu - Cascading menu implementation
+- @Terminal.Gui.App.ApplicationPopover - Popover manager (accessed via `Application.Popover`)
+
+## See Also
+
+- [Keyboard Deep Dive](keyboard.md) - Understanding keyboard event routing
+- [Mouse Deep Dive](mouse.md) - Mouse event handling
+- [MenuBar Overview](menubar.md) - Using PopoverMenu with MenuBar