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.
This commit is contained in:
Tig
2025-12-16 13:37:46 -07:00
parent 4dc193b6a3
commit 21407c710f

View File

@@ -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