From 4dc193b6a32f34197860c1d3d8007d95a60cc8e8 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 16 Dec 2025 13:30:58 -0700 Subject: [PATCH 1/4] Expand and clarify popover XML documentation Significantly improve and expand XML documentation for IPopover, PopoverBaseImpl, and PopoverMenu. Add detailed remarks on popover lifecycle, registration requirements, focus, input, and mouse handling. Provide usage examples, parameter and exception documentation, and clarify method/property summaries. Warn about the need to register popovers before showing. No functional code changes except for minor summary tweaks for clarity. --- Examples/UICatalog/Scenarios/Menus.cs | 2 + Terminal.Gui/App/IPopover.cs | 66 ++++---- Terminal.Gui/App/PopoverBaseImpl.cs | 66 ++++---- Terminal.Gui/Views/Menu/PopoverMenu.cs | 201 ++++++++++++++++++++----- 4 files changed, 239 insertions(+), 96 deletions(-) diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index 4796473ac..f044749ba 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/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 diff --git a/Terminal.Gui/App/IPopover.cs b/Terminal.Gui/App/IPopover.cs index be577b871..00ff8087b 100644 --- a/Terminal.Gui/App/IPopover.cs +++ b/Terminal.Gui/App/IPopover.cs @@ -8,54 +8,64 @@ namespace Terminal.Gui.App; /// /// 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 and are typically shown using -/// . /// /// -/// 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. +/// IMPORTANT: Popovers must be registered with using +/// before they can be shown with . /// /// +/// Lifecycle:
+/// When registered, the popover's lifetime is managed by the application. Registered popovers are +/// automatically disposed when is called. Call +/// to manage the lifetime directly. +///
+/// +/// Visibility and Hiding:
/// Popovers are automatically hidden when: -/// -/// The user clicks outside the popover (unless occluded by a subview of the popover). -/// The user presses (typically Esc). -/// Another popover is shown. -/// ///
+/// +/// The user clicks outside the popover (unless clicking on a subview). +/// The user presses (typically Esc). +/// Another popover is shown. +/// is set to . +/// /// /// Focus and Input:
-/// When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a -/// subview), -/// presses , 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. ///
/// /// Layout:
-/// When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override -/// this behavior -/// by setting and in your derived class. +/// When becoming visible, popovers are automatically laid out to fill the screen by default. +/// Override and to customize size. ///
/// -/// Mouse:
-/// Popovers are transparent to mouse events (see ), -/// meaning mouse events in a popover that are not also within a subview of the popover will not be captured. +/// Mouse Events:
+/// Popovers use , meaning mouse events +/// outside subviews are not captured. ///
/// -/// Custom Popovers:
-/// To create a custom popover, inherit from and add your own content and logic. +/// Creating Custom Popovers:
+/// Inherit from and add your own content and logic. ///
/// public interface IPopover { /// - /// Gets or sets the that this Popover is associated with. If null, it is not associated with - /// any Runnable and will receive all keyboard - /// events from the . If set, it will only receive keyboard events the Runnable would normally - /// receive. - /// When is called, the is set to the current - /// if not already set. + /// Gets or sets the that this popover is associated with. /// + /// + /// + /// If , the popover is not associated with any runnable and will receive all keyboard + /// events from the application. + /// + /// + /// If set, the popover will only receive keyboard events when the associated runnable is active. + /// + /// + /// When is called, this property is automatically set to + /// if not already set. + /// + /// IRunnable? Current { get; set; } } diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs index ff118df35..81673e405 100644 --- a/Terminal.Gui/App/PopoverBaseImpl.cs +++ b/Terminal.Gui/App/PopoverBaseImpl.cs @@ -2,36 +2,36 @@ namespace Terminal.Gui.App; /// -/// Abstract base class for popover views in Terminal.Gui. +/// Abstract base class for popover views in Terminal.Gui. Implements . /// /// /// -/// Popover Lifecycle:
-/// To display a popover, use . To hide a popover, either call -/// , -/// set to , or show another popover. +/// IMPORTANT: Popovers must be registered with using +/// before they can be shown. ///
/// -/// Focus and Input:
-/// When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a -/// subview), -/// presses , or another popover is shown, the popover will be hidden -/// automatically. +/// Requirements:
+/// Derived classes must: ///
+/// +/// Set to include and . +/// Add a key binding for (typically bound to ). +/// /// -/// Layout:
-/// When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override -/// this behavior -/// by setting and in your derived class. +/// Default Behavior:
+/// This base class provides: ///
+/// +/// Fills the screen by default ( = , = ). +/// Transparent viewport settings for proper mouse event handling. +/// Automatic layout when becoming visible. +/// Focus restoration when hidden. +/// Default implementation that hides the popover. +/// /// -/// Mouse:
-/// Popovers are transparent to mouse events (see ), -/// meaning mouse events in a popover that are not also within a subview of the popover will not be captured. -///
-/// -/// Custom Popovers:
-/// To create a custom popover, inherit from and add your own content and logic. +/// Lifecycle:
+/// Use to display and or +/// set to to hide. ///
///
public abstract class PopoverBaseImpl : View, IPopover @@ -40,7 +40,15 @@ public abstract class PopoverBaseImpl : View, IPopover /// Initializes a new instance of the class. /// /// - /// By default, the popover fills the available screen area and is focusable. + /// + /// Sets up default popover behavior: + /// + /// + /// Fills the screen ( = , = ). + /// Sets to . + /// Configures with and . + /// Adds bound to which hides the popover when invoked. + /// /// protected PopoverBaseImpl () { @@ -87,15 +95,19 @@ public abstract class PopoverBaseImpl : View, IPopover } /// - /// Called when the property is changing. + /// Called when the property is changing. Handles layout and focus management. /// - /// - /// When becoming visible, the popover is laid out to fit the screen. - /// When becoming hidden, focus is restored to the previous view. - /// /// /// to cancel the visibility change; otherwise, . /// + /// + /// + /// When becoming visible: Lays out the popover to fit the screen. + /// + /// + /// When becoming hidden: Restores focus to the previously focused view in the view hierarchy. + /// + /// protected override bool OnVisibleChanging () { bool ret = base.OnVisibleChanging (); diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 7f1e45979..acd3c2bd6 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -1,16 +1,29 @@ - - namespace Terminal.Gui.Views; /// -/// 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 as part of . +/// A -derived view that provides a cascading menu. +/// Can be used as a context menu or a drop-down menu as part of . /// /// /// -/// To use as a context menu, register the popover menu with and call -/// . +/// IMPORTANT: Must be registered with via +/// before calling or +/// . +/// +/// +/// Usage Example: +/// +/// +/// 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); +/// +/// +/// See and for lifecycle, focus, and keyboard handling details. /// /// public class PopoverMenu : PopoverBaseImpl, IDesignable @@ -22,9 +35,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// /// Initializes a new instance of the class. If any of the elements of - /// is , - /// a see will be created instead. + /// is , a will be created instead. /// + /// The views to use as menu items. Null elements become separator lines. + /// + /// Remember to call before calling . + /// public PopoverMenu (IEnumerable? menuItems) : this ( new Menu (menuItems?.Select (item => item ?? new Line ())) { @@ -32,17 +48,27 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable }) { } - /// + /// + /// Initializes a new instance of the class with the specified menu items. + /// + /// The menu items to display in the popover. + /// + /// Remember to call before calling . + /// public PopoverMenu (IEnumerable? menuItems) : this ( - new Menu (menuItems) - { - Title = "Popover Root" - }) + new Menu (menuItems) + { + Title = "Popover Root" + }) { } /// /// Initializes a new instance of the class with the specified root . /// + /// The root menu that contains the top-level menu items. + /// + /// Remember to call before calling . + /// 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; - /// Specifies the key that will activate the context menu. + /// + /// Gets or sets the key that will activate the popover menu when it is registered but not visible. + /// + /// + /// This key binding works as a global hotkey when the popover is registered with + /// . The default value is ( with + /// Shift). + /// public Key Key { get => _key; @@ -144,10 +177,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } } - /// Raised when is changed. + /// + /// Raised when the property is changed. + /// public event EventHandler? KeyChanged; - /// The default key for activating popover menus. + /// + /// Gets or sets the default key for activating popover menus. The default value is with Shift. + /// + /// + /// This is a configuration property that affects all new instances. + /// [ConfigurationProperty (Scope = typeof (SettingsScope))] public static Key DefaultKey { get; set; } = Key.F10.WithShift; @@ -159,12 +199,25 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable /// /// Makes the popover menu visible and locates it at . 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 . /// - /// If , the current mouse position will be used. + /// + /// The ideal screen-relative position for the menu. If , the current mouse position will be + /// used. + /// + /// + /// + /// IMPORTANT: The popover must be registered with before calling this + /// method. + /// Call first. + /// + /// + /// This method internally calls , which will throw + /// if the popover is not registered. + /// + /// + /// Thrown if the popover has not been registered. public void MakeVisible (Point? idealScreenPosition = null) { if (Visible) @@ -180,12 +233,18 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// - /// Locates the popover menu at . 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 . 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 (if possible). /// - /// If , the current mouse position will be used. + /// + /// The ideal screen-relative position for the menu. If , the current mouse position will be + /// used. + /// + /// + /// This method only sets the position; it does not make the popover visible. Use to + /// both position and show the popover. + /// public void SetPosition (Point? idealScreenPosition = null) { idealScreenPosition ??= App?.Mouse.LastMousePosition; @@ -212,6 +271,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// + /// + /// When becoming visible, the root menu is added and shown. When becoming hidden, the root menu is removed + /// and the popover is hidden via . + /// protected override void OnVisibleChanged () { // Logging.Debug ($"{Title} - Visible: {Visible}"); @@ -231,8 +294,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable private Menu? _root; /// - /// Gets or sets the that is the root of the Popover Menu. + /// Gets or sets the that is the root of the popover menu hierarchy. /// + /// + /// + /// The root menu contains the top-level menu items. Setting this property updates key bindings and + /// event subscriptions for all menus in the hierarchy. + /// + /// + /// When set, all submenus are configured with appropriate event handlers for selection and acceptance. + /// + /// public Menu? Root { get => _root; @@ -306,6 +378,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// + /// + /// This method checks all menu items in the hierarchy for a matching key binding and invokes the + /// appropriate menu item if found. + /// 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 } /// - /// Gets all the submenus in the PopoverMenu. + /// Gets all the submenus in the popover menu hierarchy, including the root menu. /// - /// + /// An enumerable collection of all instances in the hierarchy. + /// + /// This method performs a depth-first traversal of the menu tree, starting from . + /// public IEnumerable GetAllSubMenus () { List result = []; @@ -358,9 +437,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// - /// Gets all the MenuItems in the PopoverMenu. + /// Gets all the menu items in the popover menu hierarchy. /// - /// + /// An enumerable collection of all instances across all menus in the hierarchy. + /// + /// This method traverses all menus returned by and collects their menu items. + /// internal IEnumerable GetMenuItemsOfAllSubMenus () { List result = []; @@ -380,9 +462,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// - /// Pops up the submenu of the specified MenuItem, if there is one. + /// Shows the submenu of the specified , if it has one. /// - /// + /// The menu item whose submenu should be shown. + /// + /// + /// If another submenu is currently visible at the same level, it will be hidden before showing the new one. + /// + /// + /// The submenu is positioned to the right of the menu item, adjusted to ensure full visibility on screen. + /// + /// internal void ShowSubMenu (MenuItem? menuItem) { var menu = menuItem?.SuperView as Menu; @@ -416,11 +506,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable } /// - /// Gets the most visible screen-relative location for . + /// Calculates the most visible screen-relative location for the specified . /// - /// The menu to locate. - /// Ideal screen-relative location. - /// + /// The menu to position. + /// The ideal screen-relative location. + /// The adjusted screen-relative position that ensures maximum visibility of the menu. + /// + /// This method adjusts the position to keep the menu fully visible on screen, considering screen boundaries. + /// 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 } /// + /// + /// + /// When the popover is not visible, only hotkey commands are processed. + /// + /// + /// This method raises for commands that originate from menu items in the hierarchy. + /// + /// 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 } /// + /// + /// Thrown if attempting to add a or directly to the popover. + /// + /// + /// Do not add or views directly to the popover. + /// Use the property instead. + /// 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 } /// + /// + /// This method unsubscribes from all menu events and disposes the root menu. + /// protected override void Dispose (bool disposing) { if (disposing) @@ -600,7 +710,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable base.Dispose (disposing); } - /// + /// + /// Enables the popover menu for use in design-time scenarios. + /// + /// The type of the target view context. + /// The target view to associate with the menu commands. + /// if successfully enabled for design; otherwise, . + /// + /// 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. + /// public bool EnableForDesign (ref TContext targetView) where TContext : notnull { // Note: This menu is used by unit tests. If you modify it, you'll likely have to update From 21407c710f2d1a2b561ad44c26f1bed5f02072fd Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 16 Dec 2025 13:37:46 -0700 Subject: [PATCH 2/4] Rewrite and expand Popovers.md documentation Completely overhauled Popovers.md with a comprehensive guide to popovers in Terminal.Gui. Added detailed explanations, requirements, and best practices for creating, registering, and managing popovers. Included extensive code examples for PopoverMenu, custom popovers, and common scenarios such as context menus and autocomplete. Documented keyboard and mouse event handling, lifecycle management, and provided an API reference. Removed outdated and unstructured content. --- docfx/docs/Popovers.md | 372 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 361 insertions(+), 11 deletions(-) diff --git a/docfx/docs/Popovers.md b/docfx/docs/Popovers.md index 54cc24d10..c1569ac11 100644 --- a/docfx/docs/Popovers.md +++ b/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 -@Terminal.Gui.Views.PopoverMenu provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBar. \ No newline at end of file +### 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 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 \ No newline at end of file From c19ca48c7b15d25106c13f795e57e2a7ebaece90 Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 17 Dec 2025 05:42:48 -0700 Subject: [PATCH 3/4] Require non-null IApplication in MessageBox API (#4499) --- Terminal.Gui/Views/MessageBox.cs | 101 +++++++++++++++---------------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 07fccc069..09fae545a 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -11,8 +11,8 @@ namespace Terminal.Gui.Views; /// or if the user pressed (typically Esc). /// /// -/// uses the default Dialog color scheme. -/// uses the Error color scheme. +/// uses the default Dialog color scheme. +/// uses the Error color scheme. /// /// /// Important: All MessageBox methods require an instance to be passed. @@ -126,11 +126,11 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically sizes the + /// Consider using which automatically sizes the /// MessageBox. /// public static int? ErrorQuery ( - IApplication? app, + IApplication app, int width, int height, string title, @@ -165,7 +165,7 @@ public static class MessageBox /// /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int? ErrorQuery (IApplication? app, string title, string message, params string [] buttons) + public static int? ErrorQuery (IApplication app, string title, string message, params string [] buttons) { return QueryFull ( app, @@ -195,11 +195,11 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically sizes the + /// Consider using which automatically sizes the /// MessageBox. /// public static int? ErrorQuery ( - IApplication? app, + IApplication app, int width, int height, string title, @@ -236,7 +236,7 @@ public static class MessageBox /// /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int? ErrorQuery (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) + public static int? ErrorQuery (IApplication app, string title, string message, int defaultButton = 0, params string [] buttons) { return QueryFull ( app, @@ -270,11 +270,11 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically + /// Consider using which automatically /// sizes the MessageBox. /// public static int? ErrorQuery ( - IApplication? app, + IApplication app, int width, int height, string title, @@ -317,7 +317,7 @@ public static class MessageBox /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// public static int? ErrorQuery ( - IApplication? app, + IApplication app, string title, string message, int defaultButton = 0, @@ -352,10 +352,10 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically sizes the + /// Consider using which automatically sizes the /// MessageBox. /// - public static int? Query (IApplication? app, int width, int height, string title, string message, params string [] buttons) + public static int? Query (IApplication app, int width, int height, string title, string message, params string [] buttons) { return QueryFull ( app, @@ -384,7 +384,7 @@ public static class MessageBox /// /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int? Query (IApplication? app, string title, string message, params string [] buttons) + public static int? Query (IApplication app, string title, string message, params string [] buttons) { return QueryFull ( app, @@ -414,11 +414,11 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically sizes the + /// Consider using which automatically sizes the /// MessageBox. /// public static int? Query ( - IApplication? app, + IApplication app, int width, int height, string title, @@ -455,7 +455,7 @@ public static class MessageBox /// /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// - public static int? Query (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons) + public static int? Query (IApplication app, string title, string message, int defaultButton = 0, params string [] buttons) { return QueryFull ( app, @@ -489,11 +489,11 @@ public static class MessageBox /// /// Thrown if is . /// - /// Consider using which automatically sizes + /// Consider using which automatically sizes /// the MessageBox. /// public static int? Query ( - IApplication? app, + IApplication app, int width, int height, string title, @@ -536,7 +536,7 @@ public static class MessageBox /// The MessageBox is centered and auto-sized based on title, message, and buttons. /// public static int? Query ( - IApplication? app, + IApplication app, string title, string message, int defaultButton = 0, @@ -557,7 +557,7 @@ public static class MessageBox } private static int? QueryFull ( - IApplication? app, + IApplication app, bool useErrorColors, int width, int height, @@ -568,25 +568,22 @@ public static class MessageBox params string [] buttons ) { - ArgumentNullException.ThrowIfNull (app); - - // Create button array for Dialog var count = 0; - List