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/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index a15b49fd4..b2f4b0313 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -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) { diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 998478b15..5c73bc6da 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -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); } 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 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