Merge branch 'v2_develop' into copilot/fix-54af9417-84eb-4b53-b2ca-14c9bd07be5a

This commit is contained in:
Tig
2025-12-17 12:05:24 -07:00
committed by GitHub
13 changed files with 966 additions and 183 deletions

View File

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

View File

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

View File

@@ -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 ();

View File

@@ -123,7 +123,6 @@ public record struct Thickness
driver?.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar);
}
// Draw the Left side
// Draw the Left side
if (Left > 0)
{

View File

@@ -96,7 +96,7 @@ public class Margin : Adornment
margin.ClearCachedClip ();
}
foreach (View subview in view.SubViews)
foreach (View subview in view.SubViews.OrderBy (v => v.HasFocus && v.ShadowStyle != ShadowStyle.None).Reverse ())
{
stack.Push (subview);
}

View File

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

View File

@@ -11,8 +11,8 @@ namespace Terminal.Gui.Views;
/// or <see langword="null"/> if the user pressed <see cref="Application.QuitKey"/> (typically Esc).
/// </para>
/// <para>
/// <see cref="Query(IApplication?, string, string, string[])"/> uses the default Dialog color scheme.
/// <see cref="ErrorQuery(IApplication?, string, string, string[])"/> uses the Error color scheme.
/// <see cref="Query(IApplication, string, string, string[])"/> uses the default Dialog color scheme.
/// <see cref="ErrorQuery(IApplication, string, string, string[])"/> uses the Error color scheme.
/// </para>
/// <para>
/// <b>Important:</b> All MessageBox methods require an <see cref="IApplication"/> instance to be passed.
@@ -126,11 +126,11 @@ public static class MessageBox
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="ErrorQuery(IApplication?, string, string, string[])"/> which automatically sizes the
/// Consider using <see cref="ErrorQuery(IApplication, string, string, string[])"/> which automatically sizes the
/// MessageBox.
/// </remarks>
public static int? ErrorQuery (
IApplication? app,
IApplication app,
int width,
int height,
string title,
@@ -165,7 +165,7 @@ public static class MessageBox
/// <remarks>
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
/// </remarks>
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
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="ErrorQuery(IApplication?, string, string, int, string[])"/> which automatically sizes the
/// Consider using <see cref="ErrorQuery(IApplication, string, string, int, string[])"/> which automatically sizes the
/// MessageBox.
/// </remarks>
public static int? ErrorQuery (
IApplication? app,
IApplication app,
int width,
int height,
string title,
@@ -236,7 +236,7 @@ public static class MessageBox
/// <remarks>
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
/// </remarks>
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
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="ErrorQuery(IApplication?, string, string, int, bool, string[])"/> which automatically
/// Consider using <see cref="ErrorQuery(IApplication, string, string, int, bool, string[])"/> which automatically
/// sizes the MessageBox.
/// </remarks>
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.
/// </remarks>
public static int? ErrorQuery (
IApplication? app,
IApplication app,
string title,
string message,
int defaultButton = 0,
@@ -352,10 +352,10 @@ public static class MessageBox
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="Query(IApplication?, string, string, string[])"/> which automatically sizes the
/// Consider using <see cref="Query(IApplication, string, string, string[])"/> which automatically sizes the
/// MessageBox.
/// </remarks>
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
/// <remarks>
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
/// </remarks>
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
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="Query(IApplication?, string, string, int, string[])"/> which automatically sizes the
/// Consider using <see cref="Query(IApplication, string, string, int, string[])"/> which automatically sizes the
/// MessageBox.
/// </remarks>
public static int? Query (
IApplication? app,
IApplication app,
int width,
int height,
string title,
@@ -455,7 +455,7 @@ public static class MessageBox
/// <remarks>
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
/// </remarks>
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
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
/// <remarks>
/// Consider using <see cref="Query(IApplication?, string, string, int, bool, string[])"/> which automatically sizes
/// Consider using <see cref="Query(IApplication, string, string, int, bool, string[])"/> which automatically sizes
/// the MessageBox.
/// </remarks>
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.
/// </remarks>
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<Button> buttonList = new ();
List<Button> buttonList = [];
Clicked = null;
if (buttons is { })
if (buttons.Length > 0)
{
if (defaultButton > buttons.Length - 1)
{
defaultButton = buttons.Length - 1;
}
foreach (string s in buttons)
foreach (string buttonText in buttons)
{
var b = new Button
{
Text = s,
Text = buttonText,
Data = count
};
@@ -596,7 +593,7 @@ public static class MessageBox
b.Accepting += (s, e) =>
{
if (e?.Context?.Source is Button button)
if (e.Context?.Source is Button button)
{
Clicked = (int)button.Data!;
}
@@ -605,10 +602,7 @@ public static class MessageBox
Clicked = defaultButton;
}
if (e is { })
{
e.Handled = true;
}
e.Handled = true;
(s as View)?.App?.RequestStop ();
};
@@ -619,7 +613,7 @@ public static class MessageBox
}
}
var d = new Dialog
Dialog dialog = new ()
{
Title = title,
ButtonAlignment = DefaultButtonAlignment,
@@ -628,38 +622,39 @@ public static class MessageBox
Buttons = buttonList.ToArray ()
};
d.Width = Dim.Auto (
DimAutoStyle.Auto,
Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * (DefaultMinimumWidth / 100f))),
Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * 0.9f)));
// ReSharper disable AccessToDisposedClosure
dialog.Width = Dim.Auto (
DimAutoStyle.Auto,
Dim.Func (_ => (int)((app.Screen.Width - dialog.GetAdornmentsThickness ().Horizontal) * (DefaultMinimumWidth / 100f))),
Dim.Func (_ => (int)((app.Screen.Width - dialog.GetAdornmentsThickness ().Horizontal) * 0.9f)));
d.Height = Dim.Auto (
dialog.Height = Dim.Auto (
DimAutoStyle.Auto,
Dim.Func (_ => (int)((app.Screen.Height - d.GetAdornmentsThickness ().Vertical) * (DefaultMinimumHeight / 100f))),
Dim.Func (_ => (int)((app.Screen.Height - d.GetAdornmentsThickness ().Vertical) * 0.9f)));
Dim.Func (_ => (int)((app.Screen.Height - dialog.GetAdornmentsThickness ().Vertical) * (DefaultMinimumHeight / 100f))),
Dim.Func (_ => (int)((app.Screen.Height - dialog.GetAdornmentsThickness ().Vertical) * 0.9f)));
if (width != 0)
{
d.Width = width;
dialog.Width = width;
}
if (height != 0)
{
d.Height = height;
dialog.Height = height;
}
d.SchemeName = useErrorColors ? SchemeManager.SchemesToSchemeName (Schemes.Error) : SchemeManager.SchemesToSchemeName (Schemes.Dialog);
dialog.SchemeName = useErrorColors ? SchemeManager.SchemesToSchemeName (Schemes.Error) : SchemeManager.SchemesToSchemeName (Schemes.Dialog);
d.HotKeySpecifier = new ('\xFFFF');
d.Text = message;
d.TextAlignment = Alignment.Center;
d.VerticalTextAlignment = Alignment.Start;
d.TextFormatter.WordWrap = wrapMessage;
d.TextFormatter.MultiLine = !wrapMessage;
dialog.HotKeySpecifier = new ('\xFFFF');
dialog.Text = message;
dialog.TextAlignment = Alignment.Center;
dialog.VerticalTextAlignment = Alignment.Start;
dialog.TextFormatter.WordWrap = wrapMessage;
dialog.TextFormatter.MultiLine = !wrapMessage;
// Run the modal; do not shut down the mainloop driver when done
app.Run (d);
d.Dispose ();
// Run the modal
app.Run (dialog);
dialog.Dispose ();
return Clicked;
}

View File

@@ -63,7 +63,7 @@ public class AdornmentTests (ITestOutputHelper output)
Assert.Equal (6, view.Width);
Assert.Equal (3, view.Height);
view.SetClipToScreen ();
view.App.LayoutAndDraw (true);
view.Draw ();
DriverAssert.AssertDriverContentsWithFrameAre (

View File

@@ -15,6 +15,9 @@ public class BorderArrangementTests (ITestOutputHelper output)
app.Init ("fake");
app.Driver?.SetScreenSize (6, 5);
// Using a replacement char to make sure wide glyphs are handled correctly
// in the shadow area, to not confusing with a space char.
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
@@ -101,9 +104,24 @@ public class BorderArrangementTests (ITestOutputHelper output)
app.Init ("fake");
app.Driver?.SetScreenSize (8, 7);
// Using a replacement char to make sure wide glyphs are handled correctly
// in the shadow area, to not confusing with a space char.
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
// Don't remove this array even if it seems unused, it is used to map the attributes indexes in the DriverAssert
// Otherwise the test won't detect issues with attributes not visibly by the naked eye
Attribute [] attributes =
[
new (ColorName16.Blue, ColorName16.BrightBlue, TextStyle.None),
new (ColorName16.BrightBlue, ColorName16.Blue, TextStyle.None),
new (ColorName16.Green, ColorName16.BrightGreen, TextStyle.None),
new (ColorName16.Magenta, ColorName16.BrightMagenta, TextStyle.None),
new (ColorName16.BrightMagenta, ColorName16.Magenta, TextStyle.None)
];
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
superview.SetScheme (new () { Normal = attributes [0], Focus = attributes [1] });
superview.Text = """
🍎🍎🍎🍎
@@ -115,17 +133,22 @@ public class BorderArrangementTests (ITestOutputHelper output)
🍎🍎🍎🍎
""";
View view = new ()
View view = new () { X = 6, Width = 2, Height = 1, Text = "🦮" };
view.SetScheme (new () { Normal = attributes [2] });
View view2 = new ()
{
X = 2, Width = 6, Height = 6, Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
};
view.Border!.Thickness = new (1);
view.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
superview.Add (view);
view2.Border!.Thickness = new (1);
view2.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
view2.SetScheme (new () { Normal = attributes [3], HotNormal = attributes [4] });
superview.Add (view, view2);
app.Begin (superview);
Assert.Equal ("Absolute(2)", view.X.ToString ());
Assert.Equal ("Absolute(2)", view2.X.ToString ());
DriverAssert.AssertDriverContentsAre (
"""
@@ -140,6 +163,20 @@ public class BorderArrangementTests (ITestOutputHelper output)
output,
app.Driver);
DriverAssert.AssertDriverAttributesAre (
"""
11333333
11333333
11333333
11333333
11333333
11333333
11111111
""",
output,
app.Driver,
attributes);
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
app.LayoutAndDraw ();
@@ -156,8 +193,22 @@ public class BorderArrangementTests (ITestOutputHelper output)
output,
app.Driver);
DriverAssert.AssertDriverAttributesAre (
"""
11433333
11333333
11333333
11333333
11333333
11333333
11111111
""",
output,
app.Driver,
attributes);
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
Assert.Equal ("Absolute(1)", view.X.ToString ());
Assert.Equal ("Absolute(1)", view2.X.ToString ());
app.LayoutAndDraw ();
DriverAssert.AssertDriverContentsAre (
@@ -173,13 +224,27 @@ public class BorderArrangementTests (ITestOutputHelper output)
output,
app.Driver);
DriverAssert.AssertDriverAttributesAre (
"""
14333332
13333330
13333330
13333330
13333330
13333330
11111111
""",
output,
app.Driver,
attributes);
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
Assert.Equal ("Absolute(0)", view.X.ToString ());
Assert.Equal ("Absolute(0)", view2.X.ToString ());
app.LayoutAndDraw ();
DriverAssert.AssertDriverContentsAre (
"""
i 🍎
i 🦮
🍎
🍎
🍎
@@ -189,5 +254,19 @@ public class BorderArrangementTests (ITestOutputHelper output)
""",
output,
app.Driver);
DriverAssert.AssertDriverAttributesAre (
"""
43333322
33333311
33333311
33333311
33333311
33333311
11111111
""",
output,
app.Driver,
attributes);
}
}

View File

@@ -80,20 +80,37 @@ public class ShadowTests (ITestOutputHelper output)
}
[Theory]
[InlineData (ShadowStyle.None)]
[InlineData (ShadowStyle.Opaque)]
[InlineData (ShadowStyle.Transparent)]
public void ShadowWidth_ShadowHeight_Defaults_To_One (ShadowStyle style)
public void ShadowWidth_ShadowHeight_Defaults (ShadowStyle style)
{
View view = new () { ShadowStyle = style };
Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
if (view.ShadowStyle == ShadowStyle.None)
{
Assert.Equal (new (0, 0), view.Margin!.ShadowSize);
}
else
{
Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
}
}
[Fact]
public void ShadowStyle_Opaque_Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Different_Of_One ()
{
View view = new () { ShadowStyle = ShadowStyle.Opaque };
view.Margin!.ShadowSize = new (3, 4);
Assert.Equal (1, view.Margin.ShadowSize.Width);
Assert.Equal (1, view.Margin.ShadowSize.Height);
}
[Theory]
[InlineData (ShadowStyle.None, 0)]
[InlineData (ShadowStyle.Opaque, 1)]
[InlineData (ShadowStyle.Transparent, 1)]
public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_One (ShadowStyle style, int expectedLength)
public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_Zero (ShadowStyle style, int expectedLength)
{
View view = new () { ShadowStyle = style };
view.Margin!.ShadowSize = new (-1, -1);
@@ -119,6 +136,58 @@ public class ShadowTests (ITestOutputHelper output)
Assert.Equal (new (0, 0, 1, 1), view.Margin.Thickness);
}
[Theory]
[InlineData (ShadowStyle.None, 2, 1, 3, 0, 0, 0)]
[InlineData (ShadowStyle.Opaque, 1, 1, 1, 1, 1, 1)]
[InlineData (ShadowStyle.Transparent, 2, 1, 3, 2, 2, 3)]
public void Changing_ShadowWidth_ShadowHeight_Correctly_Set_Thickness (
ShadowStyle style,
int expectedLength1,
int expectedLength2,
int expectedLength3,
int expectedThickness1,
int expectedThickness2,
int expectedThickness3
)
{
View view = new () { ShadowStyle = style };
view.Margin!.ShadowSize = new (2, 2);
Assert.Equal (expectedLength1, view.Margin!.ShadowSize.Width);
Assert.Equal (expectedLength1, view.Margin.ShadowSize.Height);
Assert.Equal (new (0, 0, expectedThickness1, expectedThickness1), view.Margin.Thickness);
view.Margin!.ShadowSize = new (1, 1);
Assert.Equal (expectedLength2, view.Margin!.ShadowSize.Width);
Assert.Equal (expectedLength2, view.Margin.ShadowSize.Height);
Assert.Equal (new (0, 0, expectedThickness2, expectedThickness2), view.Margin.Thickness);
view.Margin!.ShadowSize = new (3, 3);
Assert.Equal (expectedLength3, view.Margin!.ShadowSize.Width);
Assert.Equal (expectedLength3, view.Margin.ShadowSize.Height);
Assert.Equal (new (0, 0, expectedThickness3, expectedThickness3), view.Margin.Thickness);
view.ShadowStyle = ShadowStyle.None;
Assert.Equal (expectedLength3, view.Margin!.ShadowSize.Width);
Assert.Equal (expectedLength3, view.Margin.ShadowSize.Height);
Assert.Equal (new (0, 0, 0, 0), view.Margin.Thickness);
}
[Theory]
[InlineData (ShadowStyle.None, 0, 1)]
[InlineData (ShadowStyle.Opaque, 1, 1)]
[InlineData (ShadowStyle.Transparent, 1, 1)]
public void Changing_Thickness_Correctly_Set_Thickness (ShadowStyle style, int expectedLength, int expectedThickness)
{
View view = new () { ShadowStyle = style };
Assert.Equal (new (0, 0, expectedLength, expectedLength), view.Margin!.Thickness);
view.Margin!.Thickness = new (0, 0, 1, 1);
Assert.Equal (expectedLength, view.Margin!.ShadowSize.Width);
Assert.Equal (expectedLength, view.Margin.ShadowSize.Height);
Assert.Equal (new (0, 0, expectedThickness, expectedThickness), view.Margin.Thickness);
}
[Fact]
public void ShadowStyle_Transparent_Handles_Wide_Glyphs_Correctly ()
{
@@ -126,6 +195,9 @@ public class ShadowTests (ITestOutputHelper output)
app.Init ("fake");
app.Driver?.SetScreenSize (6, 5);
// Using a replacement char to make sure wide glyphs are handled correctly
// in the shadow area, to not confusing with a space char.
app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
@@ -143,6 +215,8 @@ public class ShadowTests (ITestOutputHelper output)
superview.Add (view);
app.Begin (superview);
Assert.Equal (new (2, 1), view.Margin!.ShadowSize);
Assert.Equal (new (0, 0, 2, 1), view.Margin!.Thickness);
DriverAssert.AssertDriverContentsAre (
"""
@@ -158,6 +232,8 @@ public class ShadowTests (ITestOutputHelper output)
view.Margin!.ShadowSize = new (1, 2);
app.LayoutAndDraw ();
Assert.Equal (new (1, 2), view.Margin!.ShadowSize);
Assert.Equal (new (0, 0, 2, 2), view.Margin!.Thickness);
DriverAssert.AssertDriverContentsAre (
"""
@@ -169,6 +245,22 @@ public class ShadowTests (ITestOutputHelper output)
""",
output,
app.Driver);
view.Width = Dim.Fill (1);
app.LayoutAndDraw ();
Assert.Equal (new (1, 2), view.Margin!.ShadowSize);
Assert.Equal (new (0, 0, 2, 2), view.Margin!.Thickness);
DriverAssert.AssertDriverContentsAre (
"""
🍎
<EFBFBD>
<EFBFBD>
<EFBFBD> 🍎<EFBFBD>
<EFBFBD> 🍎<EFBFBD>
""",
output,
app.Driver);
}
[Fact]
@@ -326,20 +418,20 @@ public class ShadowTests (ITestOutputHelper output)
}
[Theory]
[InlineData (ShadowStyle.None, 3)]
[InlineData (ShadowStyle.Opaque, 4)]
[InlineData (ShadowStyle.Transparent, 4)]
public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expected)
[InlineData (ShadowStyle.None, 3, 4, 4)]
[InlineData (ShadowStyle.Opaque, 4, 5, 4)]
[InlineData (ShadowStyle.Transparent, 4, 5, 4)]
public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expectedThickness, int expectedThicknessAdjust, int expectedThicknessNone)
{
var view = new View ();
view.Margin!.Thickness = new (3);
view.ShadowStyle = style;
Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
Assert.Equal (new (3, 3, expectedThickness, expectedThickness), view.Margin.Thickness);
view.Margin.Thickness = new (3, 3, expected + 1, expected + 1);
Assert.Equal (new (3, 3, expected + 1, expected + 1), view.Margin.Thickness);
view.Margin.Thickness = new (3, 3, expectedThickness + 1, expectedThickness + 1);
Assert.Equal (new (3, 3, expectedThicknessAdjust, expectedThicknessAdjust), view.Margin.Thickness);
view.ShadowStyle = ShadowStyle.None;
Assert.Equal (new (3, 3, 4, 4), view.Margin.Thickness);
Assert.Equal (new (3, 3, expectedThicknessNone, expectedThicknessNone), view.Margin.Thickness);
view.Dispose ();
}
@@ -427,12 +519,16 @@ public class ShadowTests (ITestOutputHelper output)
app.Driver.Refresh ();
// Assert
Assert.Equal (new (0, 0, 2, 2), viewWithShadow.Frame);
Assert.Equal (new (0, 0, 1, 1), viewWithShadow.Viewport);
_output.WriteLine ("Actual driver contents:");
_output.WriteLine (app.Driver.ToString ());
_output.WriteLine ("\nActual driver output:");
string? output = app.Driver.GetOutput ().GetLastOutput ();
_output.WriteLine (output);
// Printed with bright black (dark gray) text on bright black (dark gray) background making it invisible
DriverAssert.AssertDriverOutputIs ("""
\x1b[30m\x1b[107m*\x1b[90m\x1b[100mB
""", _output, app.Driver);

View File

@@ -630,7 +630,11 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
output,
driver);
// After a full redraw, all cells should be clean
foreach (Cell cell in driver.Contents!)
{
Assert.False (cell.IsDirty);
}
}
[Fact]

View File

@@ -0,0 +1,117 @@
using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Mouse;
public class HighlightStatesTests (ITestOutputHelper output)
{
[Fact]
public void HighlightStates_SubView_With_Single_Runnable_WorkAsExpected ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.Driver?.SetScreenSize (6, 1);
Attribute focus = new (ColorName16.White, ColorName16.Black, TextStyle.None);
Attribute highlight = new (ColorName16.Blue, ColorName16.Black, TextStyle.Italic);
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
superview.SetScheme (new () { Focus = focus, Highlight = highlight });
View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hi |", HighlightStates = MouseState.In };
superview.Add (view);
app.Begin (superview);
for (var i = 0; i < app.Driver?.Cols; i++)
{
Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
}
DriverAssert.AssertDriverContentsAre ("| Hi |", output, app.Driver);
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.ReportMousePosition });
app.LayoutAndDraw ();
for (var i = 0; i < app.Driver?.Cols; i++)
{
Assert.Equal (highlight, app.Driver.Contents? [0, i].Attribute);
}
DriverAssert.AssertDriverContentsAre ("| Hi |", output, app.Driver);
app.Dispose ();
}
[Fact]
public void HighlightStates_SubView_With_Multiple_Runnable_WorkAsExpected ()
{
IApplication app = Application.Create ();
app.Init ("fake");
app.Driver?.SetScreenSize (9, 5);
Attribute focus = new (ColorName16.White, ColorName16.Black, TextStyle.None);
Attribute highlight = new (ColorName16.Blue, ColorName16.Black, TextStyle.Italic);
Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
superview.SetScheme (new () { Focus = focus, Highlight = highlight });
View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hi |", HighlightStates = MouseState.In };
superview.Add (view);
app.Begin (superview);
Attribute normal = new (ColorName16.Green, ColorName16.Magenta, TextStyle.None);
Attribute highlight2 = new (ColorName16.Red, ColorName16.Yellow, TextStyle.Italic);
Runnable modalSuperview = new () { Y = 1, Width = 9, Height = 4, BorderStyle = LineStyle.Single };
modalSuperview.SetScheme (new () { Normal = normal, Highlight = highlight2 });
View view2 = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hey |", HighlightStates = MouseState.In };
modalSuperview.Add (view2);
app.Begin (modalSuperview);
for (var i = 0; i < app.Driver?.Cols; i++)
{
Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
}
for (var i = 0; i < app.Driver?.Cols; i++)
{
Assert.Equal (normal, app.Driver.Contents? [2, i].Attribute);
}
DriverAssert.AssertDriverContentsAre ("""
| Hi |
| Hey |
"""
, output, app.Driver);
app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 2), Flags = MouseFlags.ReportMousePosition });
app.LayoutAndDraw ();
for (var i = 0; i < app.Driver?.Cols; i++)
{
Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
}
for (var i = 1; i < app.Driver?.Cols - 1; i++)
{
Assert.Equal (highlight2, app.Driver?.Contents? [2, i].Attribute);
}
DriverAssert.AssertDriverContentsAre ("""
| Hi |
| Hey |
""",
output, app.Driver);
app.Dispose ();
}
}

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