diff --git a/Terminal.Gui/Core/ContextMenu.cs b/Terminal.Gui/Views/ContextMenu.cs similarity index 63% rename from Terminal.Gui/Core/ContextMenu.cs rename to Terminal.Gui/Views/ContextMenu.cs index 916f23b52..71d3c41b3 100644 --- a/Terminal.Gui/Core/ContextMenu.cs +++ b/Terminal.Gui/Views/ContextMenu.cs @@ -2,8 +2,24 @@ namespace Terminal.Gui { /// - /// A context menu window derived from containing menu items - /// which can be opened in any position. + /// ContextMenu provides a pop-up menu that can be positioned anywhere within a . + /// ContextMenu is analogous to and, once activated, works like a sub-menu + /// of a (but can be positioned anywhere). + /// + /// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of the ContextMenu frame + /// (either to the right or left, depending on where the ContextMenu is relative to the edge of the screen). By setting + /// to , this behavior can be changed such that all sub-menus are + /// drawn within the ContextMenu frame. + /// + /// + /// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to another key). + /// + /// + /// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling . + /// + /// + /// ContextMenus are located using screen using screen coordinates and appear above all other Views. + /// /// public sealed class ContextMenu : IDisposable { private static MenuBar menuBar; @@ -12,15 +28,15 @@ namespace Terminal.Gui { private Toplevel container; /// - /// Initialize a context menu with empty menu items. + /// Initializes a context menu with no menu items. /// public ContextMenu () : this (0, 0, new MenuBarItem ()) { } /// - /// Initialize a context menu with menu items from a host . + /// Initializes a context menu, with a specifiying the parent/hose of the menu. /// /// The host view. - /// The menu items. + /// The menu items for the context menu. public ContextMenu (View host, MenuBarItem menuItems) : this (host.Frame.X, host.Frame.Y, menuItems) { @@ -28,10 +44,10 @@ namespace Terminal.Gui { } /// - /// Initialize a context menu with menu items. + /// Initializes a context menu with menu items at a specific screen location. /// - /// The left position. - /// The top position. + /// The left position (screen relative). + /// The top position (screen relative). /// The menu items. public ContextMenu (int x, int y, MenuBarItem menuItems) { @@ -48,7 +64,7 @@ namespace Terminal.Gui { } /// - /// Disposes the all the context menu objects instances. + /// Disposes the context menu object. /// public void Dispose () { @@ -65,7 +81,7 @@ namespace Terminal.Gui { } /// - /// Open the menu items. + /// Shows (opens) the ContextMenu, displaying the s it contains. /// public void Show () { @@ -110,7 +126,7 @@ namespace Terminal.Gui { } else if (ForceMinimumPosToZero && position.Y < 0) { position.Y = 0; } - + menuBar = new MenuBar (new [] { MenuItems }) { X = position.X, Y = position.Y, @@ -139,7 +155,7 @@ namespace Terminal.Gui { } /// - /// Close the menu items. + /// Hides (closes) the ContextMenu. /// public void Hide () { @@ -158,7 +174,7 @@ namespace Terminal.Gui { public event Action MouseFlagsChanged; /// - /// Gets or set the menu position. + /// Gets or sets the menu position. /// public Point Position { get; set; } @@ -168,7 +184,7 @@ namespace Terminal.Gui { public MenuBarItem MenuItems { get; set; } /// - /// The used to activate the context menu by keyboard. + /// specifies they keyboard key that will activate the context menu with the keyboard. /// public Key Key { get => key; @@ -180,7 +196,7 @@ namespace Terminal.Gui { } /// - /// The used to activate the context menu by mouse. + /// specifies the mouse action used to activate the context menu by mouse. /// public MouseFlags MouseFlags { get => mouseFlags; @@ -192,7 +208,7 @@ namespace Terminal.Gui { } /// - /// Gets information whether menu is showing or not. + /// Gets whether the ContextMenu is showing or not. /// public static bool IsShow { get; private set; } @@ -203,8 +219,9 @@ namespace Terminal.Gui { public View Host { get; set; } /// - /// Gets or sets whether forces the minimum position to zero - /// if the left or right position are negative. + /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position + /// is less than zero. The default is which means the context menu will be forced to the right. + /// If set to , the context menu will be clipped on the left if x is less than zero. /// public bool ForceMinimumPosToZero { get; set; } = true; @@ -214,7 +231,9 @@ namespace Terminal.Gui { public MenuBar MenuBar { get => menuBar; } /// - /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. + /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the ContextMenu + /// and any sub-menus that would normally cascade will be displayed within a single frame. If (the default), + /// sub-menus will cascade using separate frames for each level of the menu hierarchy. /// public bool UseSubMenusSingleFrame { get; set; } } diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 42757a94e..10754106d 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,13 +1,3 @@ -// -// Menu.cs: application menus and submenus -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// TODO: -// Add accelerator support, but should also support chords (Shortcut in MenuItem) -// Allow menus inside menus - using System; using NStack; using System.Linq; @@ -21,23 +11,24 @@ namespace Terminal.Gui { [Flags] public enum MenuItemCheckStyle { /// - /// The menu item will be shown normally, with no check indicator. + /// The menu item will be shown normally, with no check indicator. The default. /// NoCheck = 0b_0000_0000, /// - /// The menu item will indicate checked/un-checked state (see . + /// The menu item will indicate checked/un-checked state (see ). /// Checked = 0b_0000_0001, /// - /// The menu item is part of a menu radio group (see and will indicate selected state. + /// The menu item is part of a menu radio group (see ) and will indicate selected state. /// Radio = 0b_0000_0010, }; /// - /// A has a title, an associated help text, and an action to execute on activation. + /// A has title, an associated help text, and an action to execute on activation. + /// MenuItems can also have a checked indicator (see ). /// public class MenuItem { ustring title; @@ -78,14 +69,28 @@ namespace Terminal.Gui { } /// - /// The HotKey is used when the menu is active, the shortcut can be triggered when the menu is not active. - /// For example HotKey would be "N" when the File Menu is open (assuming there is a "_New" entry - /// if the Shortcut is set to "Control-N", this would be a global hotkey that would trigger as well + /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the + /// of a MenuItem with an underscore ('_'). + /// + /// Pressing Alt-Hotkey for a (menu items on the menu bar) works even if the menu is not active). + /// Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem. + /// + /// + /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the File menu. + /// Pressing the N key will then activate the New MenuItem. + /// + /// + /// See also which enable global key-bindings to menu items. + /// /// public Rune HotKey; /// - /// This is the global setting that can be used as a global to invoke the action on the menu. + /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the that is + /// the parent of the or this . + /// + /// The will be drawn on the MenuItem to the right of the and text. See . + /// /// public Key Shortcut { get => shortcutHelper.Shortcut; @@ -97,12 +102,12 @@ namespace Terminal.Gui { } /// - /// The keystroke combination used in the as string. + /// Gets the text describing the keystroke combination defined by . /// public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (shortcutHelper.Shortcut); /// - /// Gets or sets the title. + /// Gets or sets the title of the menu item . /// /// The title. public ustring Title { @@ -116,34 +121,46 @@ namespace Terminal.Gui { } /// - /// Gets or sets the help text for the menu item. + /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . /// /// The help text. public ustring Help { get; set; } /// - /// Gets or sets the action to be invoked when the menu is triggered + /// Gets or sets the action to be invoked when the menu item is triggered. /// /// Method to invoke. public Action Action { get; set; } /// - /// Gets or sets the action to be invoked if the menu can be triggered + /// Gets or sets the action to be invoked to determine if the menu can be triggered. If returns + /// the menu item will be enabled. Otherwise, it will be disabled. /// - /// Function to determine if action is ready to be executed. + /// Function to determine if the action is can be executed or not. public Func CanExecute { get; set; } /// - /// Shortcut to check if the menu item is enabled + /// Returns if the menu item is enabled. This method is a wrapper around . /// public bool IsEnabled () { return CanExecute == null ? true : CanExecute (); } - internal int Width => 1 + TitleLength + (Help.ConsoleWidth > 0 ? Help.ConsoleWidth + 2 : 0) + - (Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + - (ShortcutTag.ConsoleWidth > 0 ? ShortcutTag.ConsoleWidth + 2 : 0) + 2; + // + // ┌─────────────────────────────┐ + // │ Quit Quit UI Catalog Ctrl+Q │ + // └─────────────────────────────┘ + // ┌─────────────────┐ + // │ ◌ TopLevel Alt+T │ + // └─────────────────┘ + // TODO: Replace the `2` literals with named constants + internal int Width => 1 + // space before Title + TitleLength + + 2 + // space after Title - BUGBUG: This should be 1 + (Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space + (Help.ConsoleWidth > 0 ? 2 + Help.ConsoleWidth : 0) + // Two spaces before Help + (ShortcutTag.ConsoleWidth > 0 ? 2 + ShortcutTag.ConsoleWidth : 0); // Pad two spaces before shortcut tag (which are also aligned right) /// /// Sets or gets whether the shows a check indicator or not. See . @@ -151,12 +168,12 @@ namespace Terminal.Gui { public bool Checked { set; get; } /// - /// Sets or gets the type selection indicator the menu item will be displayed with. + /// Sets or gets the of a menu item where is set to . /// public MenuItemCheckStyle CheckType { get; set; } /// - /// Gets or sets the parent for this . + /// Gets the parent for this . /// /// The parent. public MenuItem Parent { get; internal set; } @@ -167,7 +184,7 @@ namespace Terminal.Gui { internal bool IsFromSubMenu { get { return Parent != null; } } /// - /// Merely a debugging aid to see the interaction with main + /// Merely a debugging aid to see the interaction with main. /// public MenuItem GetMenuItem () { @@ -175,7 +192,7 @@ namespace Terminal.Gui { } /// - /// Merely a debugging aid to see the interaction with main + /// Merely a debugging aid to see the interaction with main. /// public bool GetMenuBarItem () { @@ -213,14 +230,15 @@ namespace Terminal.Gui { } /// - /// A contains s or s. + /// is a menu item on an app's . + /// MenuBarItems do not support . /// public class MenuBarItem : MenuItem { /// /// Initializes a new as a . /// /// Title for the menu item. - /// Help text to display. + /// Help text to display. Will be displayed next to the Title surrounded by parentheses. /// Action to invoke when the menu item is activated. /// Function to determine if the action can currently be executed. /// The parent of this if exist, otherwise is null. @@ -289,19 +307,6 @@ namespace Terminal.Gui { } } - //static int GetMaxTitleLength (MenuItem [] children) - //{ - // int maxLength = 0; - // foreach (var item in children) { - // int len = GetMenuBarItemLength (item.Title); - // if (len > maxLength) - // maxLength = len; - // item.IsFromSubMenu = true; - // } - - // return maxLength; - //} - void SetChildrensParent (MenuItem [] childrens) { foreach (var child in childrens) { @@ -363,12 +368,6 @@ namespace Terminal.Gui { Title = title; } - ///// - ///// Gets or sets the title to display. - ///// - ///// The title. - //public ustring Title { get; set; } - /// /// Gets or sets an array of objects that are the children of this /// @@ -391,8 +390,8 @@ namespace Terminal.Gui { } int minX = x; int minY = y; - int maxW = (items.Max (z => z?.Width) ?? 0) + 2; - int maxH = items.Length + 2; + int maxW = (items.Max (z => z?.Width) ?? 0) + 2; // This 2 is frame border? + int maxH = items.Length + 2; // This 2 is frame border? if (parent != null && x + maxW > Driver.Cols) { minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); } @@ -459,6 +458,7 @@ namespace Terminal.Gui { return GetNormalColor (); } + // Draws the Menu, within the Frame public override void Redraw (Rect bounds) { Driver.SetAttribute (GetNormalColor ()); @@ -477,13 +477,14 @@ namespace Terminal.Gui { Move (1, i + 1); Driver.SetAttribute (DetermineColorSchemeFor (item, i)); - for (int p = Bounds.X; p < Frame.Width - 2; p++) { + for (int p = Bounds.X; p < Frame.Width - 2; p++) { // This - 2 is for the border if (p < 0) continue; if (item == null) Driver.AddRune (Driver.HLine); else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) Driver.AddRune (Driver.LeftArrow); + // This `- 3` is left border + right border + one row in from right else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null) Driver.AddRune (Driver.RightArrow); else @@ -527,6 +528,7 @@ namespace Terminal.Gui { HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw }; + // The -3 is left/right border + one space (not sure what for) tf.Draw (ViewToScreen (new Rect (2, i + 1, Frame.Width - 3, 1)), i == current ? ColorScheme.Focus : GetNormalColor (), i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, @@ -832,17 +834,27 @@ namespace Terminal.Gui { } } - - /// - /// Provides a menu bar with drop-down and cascading menus. + /// + /// Provides a menu bar that spans the top of a View with drop-down and cascading menus. + /// + /// + /// By default, any sub-sub-menus (sub-menus of the s added to s) + /// are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame + /// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting + /// to , this behavior can be changed such that all sub-sub-menus are + /// drawn within a single frame below the MenuBar. + /// /// /// /// - /// The appears on the first row of the terminal. + /// The appears on the first row of the parent View and uses the full width. /// /// - /// The provides global hotkeys for the application. + /// The provides global hotkeys for the application. See . + /// + /// + /// See also: /// /// public class MenuBar : View { @@ -850,7 +862,7 @@ namespace Terminal.Gui { internal int selectedSub; /// - /// Gets or sets the array of s for the menu. Only set this when the is visible. + /// Gets or sets the array of s for the menu. Only set this after the is visible. /// /// The menu array. public MenuBarItem [] Menus { get; set; } @@ -873,7 +885,7 @@ namespace Terminal.Gui { static ustring shortcutDelimiter = "+"; /// - /// Used for change the shortcut delimiter separator. + /// Sets or gets the shortcut delimiter separator. The default is "+". /// public static ustring ShortcutDelimiter { get => shortcutDelimiter; @@ -893,6 +905,13 @@ namespace Terminal.Gui { /// /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. + /// + /// By default any sub-sub-menus (sub-menus of the main s) are displayed in a cascading manner, + /// where each sub-sub-menu pops out of the sub-menu frame + /// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting + /// to , this behavior can be changed such that all sub-sub-menus are + /// drawn within a single frame below the MenuBar. + /// /// public bool UseSubMenusSingleFrame { get => useSubMenusSingleFrame; @@ -1029,6 +1048,14 @@ namespace Terminal.Gui { isCleaning = false; } + // The column where the MenuBar starts + static int xOrigin = 0; + // Spaces before the Title + static int leftPadding = 1; + // Spaces after the Title + static int rightPadding = 1; + // Spaces after the submenu Title, before Help + static int parensAroundHelp = 3; /// public override void Redraw (Rect bounds) { @@ -1038,7 +1065,7 @@ namespace Terminal.Gui { Driver.AddRune (' '); Move (1, 0); - int pos = 1; + int pos = 0; for (int i = 0; i < Menus.Length; i++) { var menu = Menus [i]; @@ -1051,8 +1078,9 @@ namespace Terminal.Gui { hotColor = ColorScheme.HotNormal; normalColor = GetNormalColor (); } - DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} {menu.Help} ", hotColor, normalColor); - pos += 1 + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? menu.Help.ConsoleWidth + 2 : 0) + 2; + // Note Help on MenuBar is drawn with parens around it + DrawHotString (menu.Help.IsEmpty ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", hotColor, normalColor); + pos += leftPadding + menu.TitleLength + (menu.Help.ConsoleWidth > 0 ? leftPadding + menu.Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding; } PositionCursor (); } @@ -1067,14 +1095,10 @@ namespace Terminal.Gui { for (int i = 0; i < Menus.Length; i++) { if (i == selected) { pos++; - if (IsMenuOpen) - Move (pos + 1, 0); - else { - Move (pos + 1, 0); - } + Move (pos + 1, 0); return; } else { - pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2; + pos += leftPadding + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + parensAroundHelp : 0) + rightPadding; } } } @@ -1112,7 +1136,7 @@ namespace Terminal.Gui { public event Action MenuClosing; /// - /// Raised when all the menu are closed. + /// Raised when all the menu is closed. /// public event Action MenuAllClosed; @@ -1135,7 +1159,7 @@ namespace Terminal.Gui { internal bool isMenuClosing; /// - /// True if the menu is open; otherwise false. + /// if the menu is open; otherwise . /// public bool IsMenuOpen { get; protected set; } @@ -1168,7 +1192,7 @@ namespace Terminal.Gui { } /// - /// Virtual method that will invoke the + /// Virtual method that will invoke the . /// /// The current menu to be closed. /// Whether the current menu will be reopen. @@ -1181,7 +1205,7 @@ namespace Terminal.Gui { } /// - /// Virtual method that will invoke the + /// Virtual method that will invoke the . /// public virtual void OnMenuAllClosed () { @@ -1191,7 +1215,7 @@ namespace Terminal.Gui { View lastFocused; /// - /// Get the lasted focused view before open the menu. + /// Gets the view that was last focused before opening the menu. /// public View LastFocused { get; private set; } @@ -1209,6 +1233,7 @@ namespace Terminal.Gui { int pos = 0; switch (subMenu) { case null: + // Open a submenu below a MenuBar lastFocused = lastFocused ?? (SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused); if (openSubMenu != null && !CloseMenu (false, true)) return; @@ -1221,8 +1246,10 @@ namespace Terminal.Gui { openMenu.Dispose (); } + // This positions the submenu horizontally aligned with the first character of the + // menu it belongs to's text for (int i = 0; i < index; i++) - pos += 1 + Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + 2; + pos += Menus [i].TitleLength + (Menus [i].Help.ConsoleWidth > 0 ? Menus [i].Help.ConsoleWidth + 2 : 0) + leftPadding + rightPadding; openMenu = new Menu (this, Frame.X + pos, Frame.Y + 1, Menus [index]); openCurrentMenu = openMenu; openCurrentMenu.previousSubFocused = openMenu; @@ -1235,6 +1262,7 @@ namespace Terminal.Gui { openMenu.SetFocus (); break; default: + // Opens a submenu next to another submenu (openSubMenu) if (openSubMenu == null) openSubMenu = new List (); if (sIndex > -1) { @@ -1275,7 +1303,7 @@ namespace Terminal.Gui { } /// - /// Opens the current Menu programatically. + /// Opens the Menu programatically, as though the F9 key were pressed. /// public void OpenMenu () { @@ -1357,7 +1385,7 @@ namespace Terminal.Gui { } /// - /// Closes the current Menu programatically, if open and not canceled. + /// Closes the Menu programmatically if open and not canceled (as though F9 were pressed). /// public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) { @@ -1459,26 +1487,6 @@ namespace Terminal.Gui { if (openSubMenu.Count > 0) openCurrentMenu = openSubMenu.Last (); - //if (openMenu.Subviews.Count == 0) - // return; - //if (index == 0) { - // //SuperView.SetFocus (previousSubFocused); - // FocusPrev (); - // return; - //} - - //for (int i = openMenu.Subviews.Count - 1; i > index; i--) { - // isMenuClosing = true; - // if (openMenu.Subviews.Count - 1 > 0) - // SuperView.SetFocus (openMenu.Subviews [i - 1]); - // else - // SuperView.SetFocus (openMenu); - // if (openMenu != null) { - // Remove (openMenu.Subviews [i]); - // openMenu.Remove (openMenu.Subviews [i]); - // } - // RemoveSubMenu (i); - //} isMenuClosing = false; } @@ -1774,10 +1782,10 @@ namespace Terminal.Gui { if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked || (me.Flags == MouseFlags.ReportMousePosition && selected > -1) || (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) { - int pos = 1; + int pos = xOrigin; int cx = me.X; for (int i = 0; i < Menus.Length; i++) { - if (cx >= pos && cx < pos + 1 + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + 2) { + if (cx >= pos && cx < pos + leftPadding + Menus [i].TitleLength + Menus [i].Help.ConsoleWidth + rightPadding) { if (me.Flags == MouseFlags.Button1Clicked) { if (Menus [i].IsTopLevel) { var menu = new Menu (this, i, 0, Menus [i]); @@ -1806,7 +1814,7 @@ namespace Terminal.Gui { } return true; } - pos += 1 + Menus [i].TitleLength + 2; + pos += leftPadding + Menus [i].TitleLength + rightPadding; } } return false; @@ -1879,47 +1887,6 @@ namespace Terminal.Gui { handled = false; return false; } - //if (me.View != this && me.Flags != MouseFlags.Button1Pressed) - // return true; - //else if (me.View != this && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // host.CloseAllMenus (); - // return true; - //} - - - //if (!(me.View is MenuBar) && !(me.View is Menu) && me.Flags != MouseFlags.Button1Pressed)) - // return false; - - //if (Application.MouseGrabView != null) { - // if (me.View is MenuBar || me.View is Menu) { - // me.X -= me.OfX; - // me.Y -= me.OfY; - // me.View.MouseEvent (me); - // return true; - // } else if (!(me.View is MenuBar || me.View is Menu) && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // CloseAllMenus (); - // } - //} else if (!isMenuClosed && selected == -1 && me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.GrabMouse (this); - // return true; - //} - - //if (Application.MouseGrabView != null) { - // if (Application.MouseGrabView == me.View && me.View == current) { - // me.X -= me.OfX; - // me.Y -= me.OfY; - // } else if (me.View != current && me.View is MenuBar && me.View is Menu) { - // Application.UngrabMouse (); - // Application.GrabMouse (me.View); - // } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked) { - // Application.UngrabMouse (); - // CloseMenu (); - // } - //} else if ((!isMenuClosed && selected > -1)) { - // Application.GrabMouse (current); - //} handled = true; @@ -1973,12 +1940,13 @@ namespace Terminal.Gui { /// public MenuBarItem NewMenuBarItem { get; set; } /// - /// Flag that allows you to cancel the opening of the menu. + /// Flag that allows the cancellation of the event. If set to in the + /// event handler, the event will be canceled. /// public bool Cancel { get; set; } /// - /// Initializes a new instance of + /// Initializes a new instance of . /// /// The current parent. public MenuOpeningEventArgs (MenuBarItem currentMenu) @@ -1997,7 +1965,7 @@ namespace Terminal.Gui { public MenuBarItem CurrentMenu { get; } /// - /// Indicates whether the current menu will be reopen. + /// Indicates whether the current menu will reopen. /// public bool Reopen { get; } @@ -2007,15 +1975,16 @@ namespace Terminal.Gui { public bool IsSubMenu { get; } /// - /// Flag that allows you to cancel the opening of the menu. + /// Flag that allows the cancellation of the event. If set to in the + /// event handler, the event will be canceled. /// public bool Cancel { get; set; } /// - /// Initializes a new instance of + /// Initializes a new instance of . /// /// The current parent. - /// Whether the current menu will be reopen. + /// Whether the current menu will reopen. /// Indicates whether it is a sub-menu. public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu) { diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index e344b2325..ab83b487d 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -43,12 +43,14 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (1), }; - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { + var fileMenu = new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_Open CSV", "", () => Open()), new MenuItem ("_Save", "", () => Save()), - new MenuItem ("_Quit", "", () => Quit()), - }), + new MenuItem ("_Quit", "Quits The App", () => Quit()), + }); + //fileMenu.Help = "Help"; + var menu = new MenuBar (new MenuBarItem [] { + fileMenu, new MenuBarItem ("_Edit", new MenuItem [] { new MenuItem ("_New Column", "", () => AddColumn()), new MenuItem ("_New Row", "", () => AddRow()), diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 633158ee5..96f11363e 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -116,6 +116,7 @@ namespace UICatalog.Scenarios { new MenuBarItem ("_Languages", GetSupportedCultures ()) }) }); + Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 895228942..9949c337f 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -169,7 +169,7 @@ namespace UICatalog { _menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask) + new MenuItem ("_Quit", "Quit UI Catalog", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask) }), new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()), new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), @@ -178,7 +178,7 @@ namespace UICatalog { new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), new MenuItem ("_About...", "About UI Catalog", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A), - }) + }), }); _leftPane = new FrameView ("Categories") { @@ -318,7 +318,7 @@ namespace UICatalog { { List menuItems = new List (); var item = new MenuItem (); - item.Title = "_Disable/Enable Mouse"; + item.Title = "_Disable Mouse"; item.Shortcut = Key.CtrlMask | Key.AltMask | (Key)item.Title.ToString ().Substring (1, 1) [0]; item.CheckType |= MenuItemCheckStyle.Checked; item.Checked = Application.IsMouseDisabled; @@ -334,7 +334,8 @@ namespace UICatalog { List menuItems = new List (); var item = new MenuItem (); - item.Title = "Keybindings"; + item.Title = "_Key Bindings"; + item.Help = "Change which keys do what"; item.Action += () => { var dlg = new KeyBindingsDialog (); Application.Run (dlg); diff --git a/UnitTests/ContextMenuTests.cs b/UnitTests/ContextMenuTests.cs index 1cae21537..ad01177cc 100644 --- a/UnitTests/ContextMenuTests.cs +++ b/UnitTests/ContextMenuTests.cs @@ -592,7 +592,7 @@ namespace Terminal.Gui.Core { Assert.Equal (new Point (9, 3), tf.ContextMenu.Position); Application.Top.Redraw (Application.Top.Bounds); var expected = @" - File Edit + File Edit Label: TextField @@ -612,7 +612,7 @@ namespace Terminal.Gui.Core { "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 32, 17), pos); + Assert.Equal (new Rect (1, 0, 32, 17), pos); } [Fact, AutoInitShutdown] @@ -656,7 +656,7 @@ namespace Terminal.Gui.Core { Assert.Equal (new Point (10, 5), tf.ContextMenu.Position); Application.Top.Redraw (Application.Top.Bounds); var expected = @" - File Edit + File Edit ┌ Window ──────────────────────────────────┐ │ │ │ │ @@ -676,7 +676,7 @@ namespace Terminal.Gui.Core { "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 44, 17), pos); + Assert.Equal (new Rect (1, 0, 44, 17), pos); } [Fact, AutoInitShutdown] diff --git a/UnitTests/MenuTests.cs b/UnitTests/MenuTests.cs index a8687c491..0f10e48a9 100644 --- a/UnitTests/MenuTests.cs +++ b/UnitTests/MenuTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; using Xunit.Abstractions; +using static Terminal.Gui.Views.MenuTests; namespace Terminal.Gui.Views { public class MenuTests { @@ -705,16 +707,15 @@ Edit Application.Top.Redraw (Application.Top.Bounds); var expected = @" - Numbers + Numbers "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -723,12 +724,11 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│┌─────────────┐ @@ -738,12 +738,11 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 25, 7), pos); Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.CursorLeft, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -752,16 +751,14 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); } [Fact, AutoInitShutdown] @@ -785,11 +782,11 @@ Edit Application.Top.Redraw (Application.Top.Bounds); var expected = @" - Numbers + Numbers "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, @@ -799,7 +796,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -808,7 +805,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 1, @@ -818,7 +815,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│┌─────────────┐ @@ -828,7 +825,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 25, 7), pos); + Assert.Equal (new Rect (1, 0, 25, 7), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 1, @@ -838,7 +835,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -847,7 +844,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 70, @@ -857,11 +854,11 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); } [Fact, AutoInitShutdown] @@ -887,16 +884,16 @@ Edit Application.Top.Redraw (Application.Top.Bounds); var expected = @" - Numbers + Numbers "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -905,13 +902,13 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null))); Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌─────────────┐ │◄ Two │ ├─────────────┤ @@ -921,12 +918,12 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 15, 7), pos); + Assert.Equal (new Rect (1, 0, 15, 7), pos); Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.Enter, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -935,16 +932,16 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null))); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); } [Fact, AutoInitShutdown] @@ -970,11 +967,11 @@ Edit Application.Top.Redraw (Application.Top.Bounds); var expected = @" - Numbers + Numbers "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, @@ -984,7 +981,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -993,7 +990,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 1, @@ -1003,7 +1000,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌─────────────┐ │◄ Two │ ├─────────────┤ @@ -1013,7 +1010,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 15, 7), pos); + Assert.Equal (new Rect (1, 0, 15, 7), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 1, @@ -1023,7 +1020,7 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers ┌────────┐ │ One │ │ Two ►│ @@ -1032,7 +1029,7 @@ Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 10, 6), pos); + Assert.Equal (new Rect (1, 0, 10, 6), pos); Assert.False (menu.MouseEvent (new MouseEvent () { X = 70, @@ -1042,11 +1039,11 @@ Edit })); Application.Top.Redraw (Application.Top.Bounds); expected = @" - Numbers + Numbers "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 9, 1), pos); + Assert.Equal (new Rect (1, 0, 8, 1), pos); } [Fact, AutoInitShutdown] @@ -1074,11 +1071,11 @@ Edit Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); var expected = @" - File Edit + File Edit "; var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 1), pos); + Assert.Equal (new Rect (1, 0, 11, 1), pos); Assert.True (menu.ProcessKey (new (Key.N, null))); Application.MainLoop.MainIteration (); @@ -1088,11 +1085,11 @@ Edit Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); expected = @" - File Edit + File Edit "; pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 1), pos); + Assert.Equal (new Rect (1, 0, 11, 1), pos); Assert.True (menu.ProcessKey (new (Key.CursorRight, null))); Assert.True (menu.ProcessKey (new (Key.C, null))); @@ -1100,21 +1097,150 @@ Edit Assert.True (copyAction); } + // Defines the expected strings for a Menu. Currently supports + // - MenuBar with any number of MenuItems + // - Each top-level MenuItem can have a SINGLE sub-menu + // + // TODO: Enable multiple sub-menus + // TODO: Enable checked sub-menus + // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) + // + // E.g: + // + // File Edit + // New Copy + public class ExpectedMenuBar : MenuBar { + FakeDriver d = ((FakeDriver)Application.Driver); + + // Each MenuBar title has a 1 space pad on each side + // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs + public string MenuBarText { + get { + string txt = string.Empty; + foreach (var m in Menus) { + + txt += " " + m.Title.ToString () + " "; + } + return txt; + } + } + + // The expected strings when the menu is closed + public string ClosedMenuText => MenuBarText + "\n"; + + // Padding for the X of the sub menu Frane + // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created + string padding (int i) + { + int n = 0; + while (i > 0){ + n += Menus [i-1].TitleLength + 2; + i--; + } + return new string (' ', n); + } + + // Define expected menu frame + // "┌──────┐" + // "│ New │" + // "└──────┘" + // + // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated + // 1 space before the Title and 2 spaces after the Title/Check/Help + public string expectedTopRow (int i) => $"{d.ULCorner}{new String (d.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{d.URCorner} \n"; + // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) + public string expectedMenuItemRow (int i) => $"{d.VLine} {Menus [i].Children [0].Title} {d.VLine} \n"; + public string expectedBottomRow (int i) => $"{d.LLCorner}{new String (d.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{d.LRCorner} \n"; + + // The fulll expected string for an open sub menu + public string expectedSubMenuOpen (int i) => ClosedMenuText + + (Menus [i].Children.Length > 0 ? + padding (i) + expectedTopRow (i) + + padding (i) + expectedMenuItemRow (i) + + padding (i) + expectedBottomRow (i) + : + ""); + + public ExpectedMenuBar (MenuBarItem [] menus) : base (menus) + { + } + } + + [Fact, AutoInitShutdown] + public void MenuBar_Submenus_Alignment_Correct () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { + new MenuBarItem ("File", new MenuItem [] { + new MenuItem ("Really Long Sub Menu", "", null) + }), + new MenuBarItem ("123", new MenuItem [] { + new MenuItem ("Copy", "", null) + }), + new MenuBarItem ("Format", new MenuItem [] { + new MenuItem ("Word Wrap", "", null) + }), + new MenuBarItem ("Help", new MenuItem [] { + new MenuItem ("About", "", null) + }), + new MenuBarItem ("1", new MenuItem [] { + new MenuItem ("2", "", null) + }), + new MenuBarItem ("3", new MenuItem [] { + new MenuItem ("2", "", null) + }), + new MenuBarItem ("Last one", new MenuItem [] { + new MenuItem ("Test", "", null) + }) + }); + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + for (var i = 0; i < expectedMenu.Menus.Length; i++) { + items [i] = new MenuBarItem (expectedMenu.Menus [i].Title, new MenuItem [] { + new MenuItem (expectedMenu.Menus [i].Children [0].Title, "", null) + }); + } + var menu = new MenuBar (items); + + Application.Top.Add (menu); + + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + for (var i = 0; i < expectedMenu.Menus.Length; i++) { + menu.OpenMenu (i); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (i), output); + } + } + [Fact, AutoInitShutdown] public void HotKey_MenuBar_ProcessHotKey_Menu_ProcessKey () { var newAction = false; var copyAction = false; - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_New", "", () => newAction = true) + // Define the expected menu + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { + new MenuBarItem ("File", new MenuItem [] { + new MenuItem ("New", "", null) }), - new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", () => copyAction = true) + new MenuBarItem ("Edit", new MenuItem [] { + new MenuItem ("Copy", "", null) }) }); + // The real menu + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "", () => newAction = true) + }), + new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "", () => copyAction = true) + }), + }); + Application.Top.Add (menu); Assert.False (newAction); @@ -1123,15 +1249,7 @@ Edit Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.F, new KeyModifiers () { Alt = true }))); Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - var expected = @" - File Edit -┌──────┐ -│ New │ -└──────┘ -"; - - var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.N, null))); Application.MainLoop.MainIteration (); @@ -1140,15 +1258,7 @@ Edit Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.E, new KeyModifiers () { Alt = true }))); Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit - ┌───────┐ - │ Copy │ - └───────┘ -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 16, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.C, null))); Application.MainLoop.MainIteration (); @@ -1158,127 +1268,114 @@ Edit [Fact, AutoInitShutdown] public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () { - // With HotKeys - var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_New", "", null) - }), - new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", null) - }) - }); - - Application.Top.Add (menu); - - Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); - Assert.True (menu.IsMenuOpen); - Application.Top.Redraw (Application.Top.Bounds); - var expected = @" - File Edit -┌──────┐ -│ New │ -└──────┘ -"; - - var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 4), pos); - - Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.CursorRight, null))); - Assert.True (menu.IsMenuOpen); - Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit - ┌───────┐ - │ Copy │ - └───────┘ -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 16, 4), pos); - - Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); - Assert.False (menu.IsMenuOpen); - Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 1), pos); - - // Without HotKeys - menu = new MenuBar (new MenuBarItem [] { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { new MenuBarItem ("File", new MenuItem [] { - new MenuItem ("New", "", null) + new MenuItem ("12", "", null) }), new MenuBarItem ("Edit", new MenuItem [] { new MenuItem ("Copy", "", null) }) }); + // Test without HotKeys first + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem (expectedMenu.Menus[0].Title, new MenuItem [] { + new MenuItem (expectedMenu.Menus[0].Children[0].Title, "", null) + }), + new MenuBarItem (expectedMenu.Menus[1].Title, new MenuItem [] { + new MenuItem (expectedMenu.Menus[1].Children[0].Title, "", null) + }) + }); + + Application.Top.Add (menu); + + // Open first Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit -┌──────┐ -│ New │ -└──────┘ -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); + // Open second Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.CursorRight, null))); Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit - ┌───────┐ - │ Copy │ - └───────┘ -"; + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 16, 4), pos); + // Close menu + Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); + Assert.False (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + Application.Top.Remove (menu); + + // Now test WITH HotKeys + menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "", null) + }), + new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "", null) + }), + }); + + Application.Top.Add (menu); + + // Open first + Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); + + // Open second + Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.CursorRight, null))); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); + + // Close menu + Assert.True (menu.ProcessHotKey (new (Key.F9, new KeyModifiers ()))); + Assert.False (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); } [Fact, AutoInitShutdown] public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () { - var menu = new MenuBar (new MenuBarItem [] { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { new MenuBarItem ("File", new MenuItem [] { - new MenuItem ("New", "", null) + new MenuItem ("Open", "", null) }), new MenuBarItem ("Edit", new MenuItem [] { new MenuItem ("Copy", "", null) }) }); + // Test without HotKeys first + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_" + expectedMenu.Menus[0].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[0].Children[0].Title, "", null) + }), + new MenuBarItem ("_" + expectedMenu.Menus[1].Title, new MenuItem [] { + new MenuItem ("_" + expectedMenu.Menus[1].Children[0].Title, "", null) + }), + }); + Application.Top.Add (menu); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu })); Assert.True (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - var expected = @" - File Edit -┌──────┐ -│ New │ -└──────┘ -"; - var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 13, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); } [Fact] @@ -1300,110 +1397,98 @@ Edit [Fact, AutoInitShutdown] public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () { - var menu = new MenuBar (new MenuBarItem [] { + // File Edit Format + //┌──────┐ ┌───────┐ + //│ New │ │ Wrap │ + //└──────┘ └───────┘ + + // Define the expected menu + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { new MenuBarItem ("File", new MenuItem [] { - new MenuItem ("New", "", null) - }), - new MenuBarItem ("Edit", new MenuItem [] { + new MenuItem ("New", "", null) }), + new MenuBarItem ("Edit", new MenuItem [] {}), new MenuBarItem ("Format", new MenuItem [] { new MenuItem ("Wrap", "", null) }) }); + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem (expectedMenu.Menus[0].Title, new MenuItem [] { + new MenuItem (expectedMenu.Menus[0].Children[0].Title, "", null) + }), + new MenuBarItem (expectedMenu.Menus[1].Title, new MenuItem [] {}), + new MenuBarItem (expectedMenu.Menus[2].Title, new MenuItem [] { + new MenuItem (expectedMenu.Menus[2].Children[0].Title, "", null) + }) + }); + var tf = new TextField () { Y = 2, Width = 10 }; Application.Top.Add (menu, tf); - Application.Begin (Application.Top); + Assert.True (tf.HasFocus); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - var expected = @" - File Edit Format -┌──────┐ -│ New │ -└──────┘ -"; - - var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); Assert.True (menu.MouseEvent (new MouseEvent () { X = 15, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format - ┌───────┐ - │ Wrap │ - └───────┘ -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 23, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (2), output); Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); Assert.True (menu.MouseEvent (new MouseEvent () { X = 1, Y = 0, Flags = MouseFlags.ReportMousePosition, View = menu })); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -┌──────┐ -│ New │ -└──────┘ -"; + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 4), pos); Assert.True (menu.MouseEvent (new MouseEvent () { X = 8, Y = 0, Flags = MouseFlags.Button1Pressed, View = menu })); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); } [Fact, AutoInitShutdown] public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () { - var menu = new MenuBar (new MenuBarItem [] { + var expectedMenu = new ExpectedMenuBar (new MenuBarItem [] { new MenuBarItem ("File", new MenuItem [] { new MenuItem ("New", "", null) }), - new MenuBarItem ("Edit", new MenuItem [] { - }), + new MenuBarItem ("Edit", Array.Empty ()), new MenuBarItem ("Format", new MenuItem [] { new MenuItem ("Wrap", "", null) }) }); + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + for (var i = 0; i < expectedMenu.Menus.Length; i++) { + items [i] = new MenuBarItem (expectedMenu.Menus [i].Title, expectedMenu.Menus [i].Children.Length > 0 + ? new MenuItem [] { + new MenuItem (expectedMenu.Menus [i].Children [0].Title, "", null), + } + : Array.Empty ()); + } + var menu = new MenuBar (items); + var tf = new TextField () { Y = 2, Width = 10 }; Application.Top.Add (menu, tf); @@ -1413,76 +1498,40 @@ Edit Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - var expected = @" - File Edit Format -┌──────┐ -│ New │ -└──────┘ -"; - - var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen(0), output); + // Right - Edit has no sub menu; this tests that no sub menu shows Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); + // Right - Format Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format - ┌───────┐ - │ Wrap │ - └───────┘ -"; + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (2), output); - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 23, 4), pos); + // Left - Edit + Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + Application.Top.Redraw (Application.Top.Bounds); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (1), output); Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); Assert.True (menu.IsMenuOpen); Assert.False (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); - - Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -┌──────┐ -│ New │ -└──────┘ -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 4), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.expectedSubMenuOpen (0), output); Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ()))); Assert.False (menu.IsMenuOpen); Assert.True (tf.HasFocus); Application.Top.Redraw (Application.Top.Bounds); - expected = @" - File Edit Format -"; - - pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rect (2, 0, 22, 1), pos); + GraphViewTests.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); } [Fact, AutoInitShutdown] diff --git a/UnitTests/PosTests.cs b/UnitTests/PosTests.cs index b3efecf0b..528a722eb 100644 --- a/UnitTests/PosTests.cs +++ b/UnitTests/PosTests.cs @@ -247,7 +247,7 @@ namespace Terminal.Gui.Core { win.Frame.Right, win.Frame.Bottom)); Assert.Equal (new Rect (0, 20, 78, 1), label.Frame); var expected = @" - Menu + Menu ┌──────────────────────────────────────────────────────────────────────────────┐ │ │ │ │ @@ -310,7 +310,7 @@ namespace Terminal.Gui.Core { win.Frame.Right, win.Frame.Bottom)); Assert.Equal (new Rect (0, 20, 78, 1), label.Frame); var expected = @" - Menu + Menu ┌──────────────────────────────────────────────────────────────────────────────┐ │ │ │ │