mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Merge pull request #4500 from tig/v2_4488-PopoverMenu
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 ();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@Terminal.Gui.Views.PopoverMenu 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`
|
||||
|
||||
@Terminal.Gui.App.PopoverBaseImpl 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
|
||||
|
||||
@Terminal.Gui.App.PopoverBaseImpl 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
|
||||
|
||||
@Terminal.Gui.Views.PopoverMenu 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
|
||||
|
||||
@Terminal.Gui.Views.PopoverMenu 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
|
||||
Reference in New Issue
Block a user