Merge pull request #2117 from tig/fix_2109_menubar_spacing

Fixes #2109. MenuBar has extra space on left. Refactors MenuTests. Better Menu API docs.
This commit is contained in:
Tig
2022-10-21 15:20:31 -07:00
committed by GitHub
8 changed files with 476 additions and 435 deletions

View File

@@ -2,8 +2,24 @@
namespace Terminal.Gui {
/// <summary>
/// A context menu window derived from <see cref="MenuBar"/> containing menu items
/// which can be opened in any position.
/// ContextMenu provides a pop-up menu that can be positioned anywhere within a <see cref="View"/>.
/// ContextMenu is analogous to <see cref="MenuBar"/> and, once activated, works like a sub-menu
/// of a <see cref="MenuBarItem"/> (but can be positioned anywhere).
/// <para>
/// 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
/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-menus are
/// drawn within the ContextMenu frame.
/// </para>
/// <para>
/// ContextMenus can be activated using the Shift-F10 key (by default; use the <see cref="Key"/> to change to another key).
/// </para>
/// <para>
/// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling <see cref="Show()"/>.
/// </para>
/// <para>
/// ContextMenus are located using screen using screen coordinates and appear above all other Views.
/// </para>
/// </summary>
public sealed class ContextMenu : IDisposable {
private static MenuBar menuBar;
@@ -12,15 +28,15 @@ namespace Terminal.Gui {
private Toplevel container;
/// <summary>
/// Initialize a context menu with empty menu items.
/// Initializes a context menu with no menu items.
/// </summary>
public ContextMenu () : this (0, 0, new MenuBarItem ()) { }
/// <summary>
/// Initialize a context menu with menu items from a host <see cref="View"/>.
/// Initializes a context menu, with a <see cref="View"/> specifiying the parent/hose of the menu.
/// </summary>
/// <param name="host">The host view.</param>
/// <param name="menuItems">The menu items.</param>
/// <param name="menuItems">The menu items for the context menu.</param>
public ContextMenu (View host, MenuBarItem menuItems) :
this (host.Frame.X, host.Frame.Y, menuItems)
{
@@ -28,10 +44,10 @@ namespace Terminal.Gui {
}
/// <summary>
/// Initialize a context menu with menu items.
/// Initializes a context menu with menu items at a specific screen location.
/// </summary>
/// <param name="x">The left position.</param>
/// <param name="y">The top position.</param>
/// <param name="x">The left position (screen relative).</param>
/// <param name="y">The top position (screen relative).</param>
/// <param name="menuItems">The menu items.</param>
public ContextMenu (int x, int y, MenuBarItem menuItems)
{
@@ -48,7 +64,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Disposes the all the context menu objects instances.
/// Disposes the context menu object.
/// </summary>
public void Dispose ()
{
@@ -65,7 +81,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Open the <see cref="MenuItems"/> menu items.
/// Shows (opens) the ContextMenu, displaying the <see cref="MenuItem"/>s it contains.
/// </summary>
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 {
}
/// <summary>
/// Close the <see cref="MenuItems"/> menu items.
/// Hides (closes) the ContextMenu.
/// </summary>
public void Hide ()
{
@@ -158,7 +174,7 @@ namespace Terminal.Gui {
public event Action<MouseFlags> MouseFlagsChanged;
/// <summary>
/// Gets or set the menu position.
/// Gets or sets the menu position.
/// </summary>
public Point Position { get; set; }
@@ -168,7 +184,7 @@ namespace Terminal.Gui {
public MenuBarItem MenuItems { get; set; }
/// <summary>
/// The <see cref="Gui.Key"/> used to activate the context menu by keyboard.
/// <see cref="Gui.Key"/> specifies they keyboard key that will activate the context menu with the keyboard.
/// </summary>
public Key Key {
get => key;
@@ -180,7 +196,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// The <see cref="Gui.MouseFlags"/> used to activate the context menu by mouse.
/// <see cref="Gui.MouseFlags"/> specifies the mouse action used to activate the context menu by mouse.
/// </summary>
public MouseFlags MouseFlags {
get => mouseFlags;
@@ -192,7 +208,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Gets information whether menu is showing or not.
/// Gets whether the ContextMenu is showing or not.
/// </summary>
public static bool IsShow { get; private set; }
@@ -203,8 +219,9 @@ namespace Terminal.Gui {
public View Host { get; set; }
/// <summary>
/// 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 <see langword="true"/> which means the context menu will be forced to the right.
/// If set to <see langword="false"/>, the context menu will be clipped on the left if x is less than zero.
/// </summary>
public bool ForceMinimumPosToZero { get; set; } = true;
@@ -214,7 +231,9 @@ namespace Terminal.Gui {
public MenuBar MenuBar { get => menuBar; }
/// <summary>
/// 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 <see langword="true"/>, the ContextMenu
/// and any sub-menus that would normally cascade will be displayed within a single frame. If <see langword="false"/> (the default),
/// sub-menus will cascade using separate frames for each level of the menu hierarchy.
/// </summary>
public bool UseSubMenusSingleFrame { get; set; }
}

View File

@@ -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 {
/// <summary>
/// The menu item will be shown normally, with no check indicator.
/// The menu item will be shown normally, with no check indicator. The default.
/// </summary>
NoCheck = 0b_0000_0000,
/// <summary>
/// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>.
/// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).
/// </summary>
Checked = 0b_0000_0001,
/// <summary>
/// The menu item is part of a menu radio group (see <see cref="Checked"/> and will indicate selected state.
/// The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.
/// </summary>
Radio = 0b_0000_0010,
};
/// <summary>
/// A <see cref="MenuItem"/> has a title, an associated help text, and an action to execute on activation.
/// A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation.
/// MenuItems can also have a checked indicator (see <see cref="Checked"/>).
/// </summary>
public class MenuItem {
ustring title;
@@ -78,14 +69,28 @@ namespace Terminal.Gui {
}
/// <summary>
/// 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 <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the <see cref="Title"/>
/// of a MenuItem with an underscore ('_').
/// <para>
/// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// See also <see cref="Shortcut"/> which enable global key-bindings to menu items.
/// </para>
/// </summary>
public Rune HotKey;
/// <summary>
/// This is the global setting that can be used as a global <see cref="ShortcutHelper.Shortcut"/> 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 <see cref="View"/> that is
/// the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this <see cref="MenuItem"/>.
/// <para>
/// The <see cref="Key"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
/// </para>
/// </summary>
public Key Shortcut {
get => shortcutHelper.Shortcut;
@@ -97,12 +102,12 @@ namespace Terminal.Gui {
}
/// <summary>
/// The keystroke combination used in the <see cref="ShortcutHelper.ShortcutTag"/> as string.
/// Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.
/// </summary>
public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (shortcutHelper.Shortcut);
/// <summary>
/// Gets or sets the title.
/// Gets or sets the title of the menu item .
/// </summary>
/// <value>The title.</value>
public ustring Title {
@@ -116,34 +121,46 @@ namespace Terminal.Gui {
}
/// <summary>
/// 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 <see cref="Title"/>.
/// </summary>
/// <value>The help text.</value>
public ustring Help { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <value>Method to invoke.</value>
public Action Action { get; set; }
/// <summary>
/// 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 <see cref="CanExecute"/> returns <see langword="true"/>
/// the menu item will be enabled. Otherwise, it will be disabled.
/// </summary>
/// <value>Function to determine if action is ready to be executed.</value>
/// <value>Function to determine if the action is can be executed or not.</value>
public Func<bool> CanExecute { get; set; }
/// <summary>
/// Shortcut to check if the menu item is enabled
/// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around <see cref="CanExecute"/>.
/// </summary>
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)
/// <summary>
/// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See <see cref="MenuItemCheckStyle"/>.
@@ -151,12 +168,12 @@ namespace Terminal.Gui {
public bool Checked { set; get; }
/// <summary>
/// Sets or gets the type selection indicator the menu item will be displayed with.
/// Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to <see langword="true"/>.
/// </summary>
public MenuItemCheckStyle CheckType { get; set; }
/// <summary>
/// Gets or sets the parent for this <see cref="MenuItem"/>.
/// Gets the parent for this <see cref="MenuItem"/>.
/// </summary>
/// <value>The parent.</value>
public MenuItem Parent { get; internal set; }
@@ -167,7 +184,7 @@ namespace Terminal.Gui {
internal bool IsFromSubMenu { get { return Parent != null; } }
/// <summary>
/// Merely a debugging aid to see the interaction with main
/// Merely a debugging aid to see the interaction with main.
/// </summary>
public MenuItem GetMenuItem ()
{
@@ -175,7 +192,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Merely a debugging aid to see the interaction with main
/// Merely a debugging aid to see the interaction with main.
/// </summary>
public bool GetMenuBarItem ()
{
@@ -213,14 +230,15 @@ namespace Terminal.Gui {
}
/// <summary>
/// A <see cref="MenuBarItem"/> contains <see cref="MenuBarItem"/>s or <see cref="MenuItem"/>s.
/// <see cref="MenuBarItem"/> is a menu item on an app's <see cref="MenuBar"/>.
/// MenuBarItems do not support <see cref="MenuItem.Shortcut"/>.
/// </summary>
public class MenuBarItem : MenuItem {
/// <summary>
/// Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.
/// </summary>
/// <param name="title">Title for the menu item.</param>
/// <param name="help">Help text to display.</param>
/// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
/// <param name="action">Action to invoke when the menu item is activated.</param>
/// <param name="canExecute">Function to determine if the action can currently be executed.</param>
/// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
@@ -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;
}
///// <summary>
///// Gets or sets the title to display.
///// </summary>
///// <value>The title.</value>
//public ustring Title { get; set; }
/// <summary>
/// Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this <see cref="MenuBarItem"/>
/// </summary>
@@ -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 {
}
}
/// <summary>
/// Provides a menu bar with drop-down and cascading menus.
/// <para>
/// Provides a menu bar that spans the top of a <see cref="Toplevel"/> View with drop-down and cascading menus.
/// </para>
/// <para>
/// By default, any sub-sub-menus (sub-menus of the <see cref="MenuItem"/>s added to <see cref="MenuBarItem"/>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
/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-sub-menus are
/// drawn within a single frame below the MenuBar.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="MenuBar"/> appears on the first row of the terminal.
/// The <see cref="MenuBar"/> appears on the first row of the parent <see cref="Toplevel"/> View and uses the full width.
/// </para>
/// <para>
/// The <see cref="MenuBar"/> provides global hotkeys for the application.
/// The <see cref="MenuBar"/> provides global hotkeys for the application. See <see cref="MenuItem.HotKey"/>.
/// </para>
/// <para>
/// See also: <see cref="ContextMenu"/>
/// </para>
/// </remarks>
public class MenuBar : View {
@@ -850,7 +862,7 @@ namespace Terminal.Gui {
internal int selectedSub;
/// <summary>
/// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this when the <see cref="MenuBar"/> is visible.
/// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this after the <see cref="MenuBar"/> is visible.
/// </summary>
/// <value>The menu array.</value>
public MenuBarItem [] Menus { get; set; }
@@ -873,7 +885,7 @@ namespace Terminal.Gui {
static ustring shortcutDelimiter = "+";
/// <summary>
/// Used for change the shortcut delimiter separator.
/// Sets or gets the shortcut delimiter separator. The default is "+".
/// </summary>
public static ustring ShortcutDelimiter {
get => shortcutDelimiter;
@@ -893,6 +905,13 @@ namespace Terminal.Gui {
/// <summary>
/// Gets or sets if the sub-menus must be displayed in a single or multiple frames.
/// <para>
/// By default any sub-sub-menus (sub-menus of the main <see cref="MenuItem"/>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
/// <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be changed such that all sub-sub-menus are
/// drawn within a single frame below the MenuBar.
/// </para>
/// </summary>
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;
///<inheritdoc/>
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<MenuClosingEventArgs> MenuClosing;
/// <summary>
/// Raised when all the menu are closed.
/// Raised when all the menu is closed.
/// </summary>
public event Action MenuAllClosed;
@@ -1135,7 +1159,7 @@ namespace Terminal.Gui {
internal bool isMenuClosing;
/// <summary>
/// True if the menu is open; otherwise false.
/// <see langword="true"/> if the menu is open; otherwise <see langword="true"/>.
/// </summary>
public bool IsMenuOpen { get; protected set; }
@@ -1168,7 +1192,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Virtual method that will invoke the <see cref="MenuClosing"/>
/// Virtual method that will invoke the <see cref="MenuClosing"/>.
/// </summary>
/// <param name="currentMenu">The current menu to be closed.</param>
/// <param name="reopen">Whether the current menu will be reopen.</param>
@@ -1181,7 +1205,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Virtual method that will invoke the <see cref="MenuAllClosed"/>
/// Virtual method that will invoke the <see cref="MenuAllClosed"/>.
/// </summary>
public virtual void OnMenuAllClosed ()
{
@@ -1191,7 +1215,7 @@ namespace Terminal.Gui {
View lastFocused;
/// <summary>
/// Get the lasted focused view before open the menu.
/// Gets the view that was last focused before opening the menu.
/// </summary>
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<Menu> ();
if (sIndex > -1) {
@@ -1275,7 +1303,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Opens the current Menu programatically.
/// Opens the Menu programatically, as though the F9 key were pressed.
/// </summary>
public void OpenMenu ()
{
@@ -1357,7 +1385,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Closes the current Menu programatically, if open and not canceled.
/// Closes the Menu programmatically if open and not canceled (as though F9 were pressed).
/// </summary>
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 {
/// </summary>
public MenuBarItem NewMenuBarItem { get; set; }
/// <summary>
/// Flag that allows you to cancel the opening of the menu.
/// Flag that allows the cancellation of the event. If set to <see langword="true"/> in the
/// event handler, the event will be canceled.
/// </summary>
public bool Cancel { get; set; }
/// <summary>
/// Initializes a new instance of <see cref="MenuOpeningEventArgs"/>
/// Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.
/// </summary>
/// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
public MenuOpeningEventArgs (MenuBarItem currentMenu)
@@ -1997,7 +1965,7 @@ namespace Terminal.Gui {
public MenuBarItem CurrentMenu { get; }
/// <summary>
/// Indicates whether the current menu will be reopen.
/// Indicates whether the current menu will reopen.
/// </summary>
public bool Reopen { get; }
@@ -2007,15 +1975,16 @@ namespace Terminal.Gui {
public bool IsSubMenu { get; }
/// <summary>
/// Flag that allows you to cancel the opening of the menu.
/// Flag that allows the cancellation of the event. If set to <see langword="true"/> in the
/// event handler, the event will be canceled.
/// </summary>
public bool Cancel { get; set; }
/// <summary>
/// Initializes a new instance of <see cref="MenuClosingEventArgs"/>
/// Initializes a new instance of <see cref="MenuClosingEventArgs"/>.
/// </summary>
/// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
/// <param name="reopen">Whether the current menu will be reopen.</param>
/// <param name="reopen">Whether the current menu will reopen.</param>
/// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
{

View File

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

View File

@@ -116,6 +116,7 @@ namespace UICatalog.Scenarios {
new MenuBarItem ("_Languages", GetSupportedCultures ())
})
});
Top.Add (menu);
var statusBar = new StatusBar (new StatusItem [] {

View File

@@ -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<MenuItem> menuItems = new List<MenuItem> ();
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<MenuItem> menuItems = new List<MenuItem> ();
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);

View File

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

View File

@@ -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<MenuItem> ()),
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<MenuItem> ());
}
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]

View File

@@ -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
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ │