浏览代码

Feature to display the sub-menus on a single frame instead multiple frames.

BDisp 3 年之前
父节点
当前提交
484e98998a
共有 4 个文件被更改,包括 608 次插入73 次删除
  1. 18 12
      Example/demo.cs
  2. 2 4
      Terminal.Gui/Core/Toplevel.cs
  3. 123 57
      Terminal.Gui/Views/Menu.cs
  4. 465 0
      UnitTests/MenuTests.cs

+ 18 - 12
Example/demo.cs

@@ -119,10 +119,10 @@ static class Demo {
 		int i = 0;
 		string txt = "Hello world, how are you doing today?";
 		container.Add (
-				new Label ($"{i+1}-{txt}") { TextAlignment = TextAlignment.Left,      Y = 3, Width = Dim.Fill () },
-				new Label ($"{i+2}-{txt}") { TextAlignment = TextAlignment.Right,     Y = 5, Width = Dim.Fill () },
-				new Label ($"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered,  Y = 7, Width = Dim.Fill () },
-				new Label ($"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified, Y = 9, Width = Dim.Fill () }
+				new Label ($"{i + 1}-{txt}") { TextAlignment = TextAlignment.Left, Y = 3, Width = Dim.Fill () },
+				new Label ($"{i + 2}-{txt}") { TextAlignment = TextAlignment.Right, Y = 5, Width = Dim.Fill () },
+				new Label ($"{i + 3}-{txt}") { TextAlignment = TextAlignment.Centered, Y = 7, Width = Dim.Fill () },
+				new Label ($"{i + 4}-{txt}") { TextAlignment = TextAlignment.Justified, Y = 9, Width = Dim.Fill () }
 			);
 
 		Application.Run (container);
@@ -487,15 +487,15 @@ static class Demo {
 					.OrderBy (x => x).Select (x => ustring.Make (x)).ToList ();
 			}
 		}
-		var list = new ComboBox () { Width = Dim.Fill(), Height = Dim.Fill() };
-		list.SetSource(items);
+		var list = new ComboBox () { Width = Dim.Fill (), Height = Dim.Fill () };
+		list.SetSource (items);
 		list.OpenSelectedItem += (ListViewItemEventArgs text) => { Application.RequestStop (); };
 
 		var d = new Dialog () { Title = "Select source file", Width = Dim.Percent (50), Height = Dim.Percent (50) };
 		d.Add (list);
 		Application.Run (d);
 
-		MessageBox.Query (60, 10, "Selected file", list.Text.ToString() == "" ? "Nothing selected" : list.Text.ToString(), "Ok");
+		MessageBox.Query (60, 10, "Selected file", list.Text.ToString () == "" ? "Nothing selected" : list.Text.ToString (), "Ok");
 	}
 	#endregion
 
@@ -564,10 +564,9 @@ static class Demo {
 	#endregion
 
 	public static Action running = MainApp;
-	static void Main(string[] args)
+	static void Main (string [] args)
 	{
-		if (args.Length > 0 && args.Contains("-usc"))
-		{
+		if (args.Length > 0 && args.Contains ("-usc")) {
 			Application.UseSystemConsole = true;
 		}
 
@@ -588,7 +587,7 @@ static class Demo {
 		if (Debugger.IsAttached)
 			CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
 
-		Application.Init();
+		Application.Init ();
 		Application.HeightAsBuffer = true;
 		//ConsoleDriver.Diagnostics = ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler;
 
@@ -621,6 +620,9 @@ static class Demo {
 		menuItems [2].Action = () => ShowMenuItem (menuItems [2]);
 		menuItems [3].Action = () => ShowMenuItem (menuItems [3]);
 
+		MenuItem miUseSubMenusSingleFrame = null;
+		var useSubMenusSingleFrame = false;
+
 		menu = new MenuBar (new MenuBarItem [] {
 			new MenuBarItem ("_File", new MenuItem [] {
 				new MenuItem ("Text _Editor Demo", "", () => { running = Editor; Application.RequestStop (); }, null, null, Key.AltMask | Key.CtrlMask | Key.D),
@@ -638,7 +640,11 @@ static class Demo {
 				new MenuItem ("_Paste", "", Paste, null, null, Key.AltMask | Key.CtrlMask| Key.V),
 				new MenuBarItem ("_Find and Replace",
 					new MenuItem [] { menuItems [0], menuItems [1] }),
-				menuItems[3]
+				menuItems[3],
+				miUseSubMenusSingleFrame = new MenuItem ("Use_SubMenusSingleFrame", "",
+				() => menu.UseSubMenusSingleFrame = miUseSubMenusSingleFrame.Checked = useSubMenusSingleFrame = !useSubMenusSingleFrame) {
+					CheckType = MenuItemCheckStyle.Checked, Checked = useSubMenusSingleFrame
+				}
 			}),
 			new MenuBarItem ("_List Demos", new MenuItem [] {
 				new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true), null, null, Key.AltMask + 0.ToString () [0]),

+ 2 - 4
Terminal.Gui/Core/Toplevel.cs

@@ -347,10 +347,8 @@ namespace Terminal.Gui {
 			case Key.AltMask:
 			case Key.AltMask | Key.Space:
 			case Key.CtrlMask | Key.Space:
-				if (MenuBar != null && MenuBar.OnKeyDown (keyEvent)) {
-					return true;
-				}
-				break;
+			case Key _ when (keyEvent.Key & Key.AltMask) == Key.AltMask:
+				return MenuBar != null && MenuBar.OnKeyDown (keyEvent);
 			}
 
 			return false;

+ 123 - 57
Terminal.Gui/Views/Menu.cs

@@ -180,7 +180,7 @@ namespace Terminal.Gui {
 		{
 			bool nextIsHot = false;
 			foreach (var x in title) {
-				if (x == '_') {
+				if (x == MenuBar.HotKeySpecifier) {
 					nextIsHot = true;
 				} else {
 					if (nextIsHot) {
@@ -349,7 +349,7 @@ namespace Terminal.Gui {
 		{
 			int len = 0;
 			foreach (var ch in title) {
-				if (ch == '_')
+				if (ch == MenuBar.HotKeySpecifier)
 					continue;
 				len++;
 			}
@@ -419,8 +419,9 @@ namespace Terminal.Gui {
 			AddCommand (Command.Left, () => { this.host.PreviousMenu (true); return true; });
 			AddCommand (Command.Right, () => {
 				this.host.NextMenu (this.barItems.IsTopLevel || (this.barItems.Children != null
-					&& current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu)
-					? true : false); return true;
+					&& current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu),
+					current > -1 && host.UseSubMenusSingleFrame && this.barItems.SubMenu (this.barItems.Children [current]) != null);
+				return true;
 			});
 			AddCommand (Command.Cancel, () => { CloseAllMenus (); return true; });
 			AddCommand (Command.Accept, () => { RunSelected (); return true; });
@@ -466,6 +467,8 @@ namespace Terminal.Gui {
 						continue;
 					if (item == null)
 						Driver.AddRune (Driver.HLine);
+					else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null)
+						Driver.AddRune (Driver.LeftArrow);
 					else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null)
 						Driver.AddRune (Driver.RightArrow);
 					else
@@ -501,12 +504,22 @@ namespace Terminal.Gui {
 				ViewToScreen (2, i + 1, out int vtsCol, out _, false);
 				if (vtsCol < Driver.Cols) {
 					Move (2, i + 1);
-					if (!item.IsEnabled ())
+					if (!item.IsEnabled ()) {
 						DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled);
-					else
+					} else if (i == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) {
+						var tf = new TextFormatter () {
+							Alignment = TextAlignment.Centered,
+							HotKeySpecifier = MenuBar.HotKeySpecifier,
+							Text = textToDraw
+						};
+						tf.Draw (ViewToScreen (new Rect (2, i + 1, Frame.Width - 3, 1)),
+							i == current ? ColorScheme.Focus : GetNormalColor (),
+							i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+					} else {
 						DrawHotString (textToDraw,
-						       i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal,
-						       i == current ? ColorScheme.Focus : GetNormalColor ());
+							i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal,
+							i == current ? ColorScheme.Focus : GetNormalColor ());
+					}
 
 					// The help string
 					var l = item.ShortcutTag.RuneCount == 0 ? item.Help.RuneCount : item.Help.RuneCount + item.ShortcutTag.RuneCount + 2;
@@ -590,12 +603,13 @@ namespace Terminal.Gui {
 			// TODO: rune-ify
 			if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) {
 				var x = Char.ToUpper ((char)kb.KeyValue);
+				var idx = -1;
 				foreach (var item in barItems.Children) {
+					idx++;
 					if (item == null) continue;
 					if (item.IsEnabled () && item.HotKey == x) {
-						if (host.CloseMenu ()) {
-							Run (item.Action);
-						}
+						current = idx;
+						RunSelected ();
 						return true;
 					}
 				}
@@ -607,8 +621,15 @@ namespace Terminal.Gui {
 		{
 			if (barItems.IsTopLevel) {
 				Run (barItems.Action);
-			} else if (current > -1) {
+			} else if (current > -1 && barItems.Children [current].Action != null) {
 				Run (barItems.Children [current].Action);
+			} else if (current == 0 && host.UseSubMenusSingleFrame
+				&& barItems.Children [current].Parent.Parent != null) {
+
+				host.PreviousMenu (barItems.Children [current].Parent.IsFromSubMenu, true);
+			} else if (current > -1 && barItems.SubMenu (barItems.Children [current]) != null) {
+
+				CheckSubMenu ();
 			}
 		}
 
@@ -640,7 +661,7 @@ namespace Terminal.Gui {
 				} else {
 					disabled = false;
 				}
-				if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
+				if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
 					!disabled && host.IsMenuOpen) {
 					if (!CheckSubMenu ())
 						return false;
@@ -651,7 +672,8 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
-			host.OnMenuOpened ();
+			if (!host.UseSubMenusSingleFrame)
+				host.OnMenuOpened ();
 			return true;
 		}
 
@@ -663,7 +685,7 @@ namespace Terminal.Gui {
 			bool disabled;
 			do {
 				current--;
-				if (host.UseKeysUpDownAsKeysLeftRight) {
+				if (host.UseKeysUpDownAsKeysLeftRight && !host.UseSubMenusSingleFrame) {
 					if ((current == -1 || this != host.openCurrentMenu) && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) {
 						current++;
 						host.PreviousMenu (true);
@@ -678,7 +700,7 @@ namespace Terminal.Gui {
 					current = barItems.Children.Length - 1;
 				if (!host.SelectEnabledItem (barItems.Children, current, out current, false)) {
 					current = 0;
-					if (!host.SelectEnabledItem (barItems.Children, current, out current) && !host.CloseMenu ()) {
+					if (!host.SelectEnabledItem (barItems.Children, current, out current) && !host.CloseMenu (false)) {
 						return false;
 					}
 					break;
@@ -689,7 +711,7 @@ namespace Terminal.Gui {
 				} else {
 					disabled = false;
 				}
-				if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
+				if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
 					!disabled && host.IsMenuOpen) {
 					if (!CheckSubMenu ())
 						return false;
@@ -697,7 +719,8 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
-			host.OnMenuOpened ();
+			if (!host.UseSubMenusSingleFrame)
+				host.OnMenuOpened ();
 			return true;
 		}
 
@@ -717,12 +740,14 @@ namespace Terminal.Gui {
 					return true;
 				var item = barItems.Children [meY];
 				if (item == null || !item.IsEnabled ()) disabled = true;
+				current = meY;
 				if (item != null && !disabled)
-					Run (barItems.Children [meY].Action);
+					RunSelected ();
 				return true;
 			} else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked ||
 				me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition ||
 				me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
+
 				disabled = false;
 				if (me.Y < 1 || me.Y - 1 >= barItems.Children.Length) {
 					return true;
@@ -732,7 +757,7 @@ namespace Terminal.Gui {
 				if (item == null || !item.IsEnabled ()) disabled = true;
 				if (item != null && !disabled)
 					current = me.Y - 1;
-				if (!CheckSubMenu ())
+				if (host.UseSubMenusSingleFrame || !CheckSubMenu ())
 					return true;
 				host.OnMenuOpened ();
 				return true;
@@ -803,15 +828,14 @@ namespace Terminal.Gui {
 	///	</para>
 	/// </remarks>
 	public class MenuBar : View {
+		internal int selected;
+		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 vislble.
+		/// Gets or sets the array of <see cref="MenuBarItem"/>s for the menu. Only set this when the <see cref="MenuBar"/> is visible.
 		/// </summary>
 		/// <value>The menu array.</value>
 		public MenuBarItem [] Menus { get; set; }
-		internal int selected;
-		internal int selectedSub;
-
-		Action action;
 
 		/// <summary>
 		/// Used for change the navigation key style.
@@ -831,6 +855,16 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// The specifier character for the hotkey to all menus.
+		/// </summary>
+		new public static Rune HotKeySpecifier => '_';
+
+		/// <summary>
+		/// Gets or sets if the sub-menus must be displayed in a single or multiple frames.
+		/// </summary>
+		public bool UseSubMenusSingleFrame { get; set; }
+
 		/// <summary>
 		/// Initializes a new instance of the <see cref="MenuBar"/>.
 		/// </summary>
@@ -897,7 +931,7 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool OnKeyUp (KeyEvent keyEvent)
 		{
-			if (keyEvent.IsAlt || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) {
+			if (keyEvent.IsAlt || keyEvent.Key == Key.AltMask || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) {
 				// User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F)
 				if (openedByAltKey && !IsMenuOpen && openMenu == null && (((uint)keyEvent.Key & (uint)Key.CharMask) == 0
 					|| ((uint)keyEvent.Key & (uint)Key.CharMask) == (uint)Key.Space)) {
@@ -1004,13 +1038,23 @@ namespace Terminal.Gui {
 					pos += 2 + Menus [i].TitleLength + (Menus [i].Help.Length > 0 ? Menus [i].Help.Length + 2 : 0) + 1;
 				}
 			}
-			//Move (0, 0);
 		}
 
 		void Selected (MenuItem item)
 		{
-			// TODO: Running = false;
-			action = item.Action;
+			var action = item.Action;
+
+			if (action == null)
+				return;
+
+			Application.UngrabMouse ();
+			CloseAllMenus ();
+			Application.Refresh ();
+
+			Application.MainLoop.AddIdle (() => {
+				action ();
+				return false;
+			});
 		}
 
 		/// <summary>
@@ -1154,7 +1198,21 @@ namespace Terminal.Gui {
 					RemoveSubMenu (sIndex);
 				} else {
 					var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu;
-					openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
+					if (!UseSubMenusSingleFrame) {
+						openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
+					} else {
+						var first = openSubMenu.Count > 0 ? openSubMenu.First () : openMenu;
+						var mbi = new MenuItem [2 + subMenu.Children.Length];
+						mbi [0] = new MenuItem () { Title = subMenu.Title, Parent = subMenu };
+						mbi [1] = null;
+						for (int j = 0; j < subMenu.Children.Length; j++) {
+							mbi [j + 2] = subMenu.Children [j];
+						}
+						var newSubMenu = new MenuBarItem (mbi);
+						openCurrentMenu = new Menu (this, first.Frame.Left, first.Frame.Top, newSubMenu);
+						last.Visible = false;
+						Application.GrabMouse (openCurrentMenu);
+					}
 					openCurrentMenu.previousSubFocused = last.previousSubFocused;
 					openSubMenu.Add (openCurrentMenu);
 					if (SuperView == null) {
@@ -1190,7 +1248,7 @@ namespace Terminal.Gui {
 
 			previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused;
 			OpenMenu (selected);
-			if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu ()) {
+			if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) {
 				return;
 			}
 			if (!openCurrentMenu.CheckSubMenu ())
@@ -1209,7 +1267,7 @@ namespace Terminal.Gui {
 
 			OpenMenu (idx, sIdx, subMenu);
 			if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)
-				&& subMenu == null && !CloseMenu ()) {
+				&& subMenu == null && !CloseMenu (false)) {
 
 				return;
 			}
@@ -1263,19 +1321,23 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Closes the current Menu programatically, if open and not canceled.
 		/// </summary>
-		public bool CloseMenu ()
+		public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false)
 		{
-			return CloseMenu (false, false);
+			return CloseMenu (false, false, ignoreUseSubMenusSingleFrame);
 		}
 
 		bool reopen;
 
-		internal bool CloseMenu (bool reopen = false, bool isSubMenu = false)
+		internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false)
 		{
+			var mbi = isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems;
+			if (UseSubMenusSingleFrame && mbi != null &&
+				!ignoreUseSubMenusSingleFrame && mbi.Parent != null) {
+				return false;
+			}
 			isMenuClosing = true;
 			this.reopen = reopen;
-			var args = OnMenuClosing (
-				isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems, reopen, isSubMenu);
+			var args = OnMenuClosing (mbi, reopen, isSubMenu);
 			if (args.Cancel) {
 				isMenuClosing = false;
 				if (args.CurrentMenu.Parent != null)
@@ -1327,9 +1389,9 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		void RemoveSubMenu (int index)
+		void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false)
 		{
-			if (openSubMenu == null)
+			if (openSubMenu == null || (UseSubMenusSingleFrame && !ignoreUseSubMenusSingleFrame && openSubMenu.Count > 0))
 				return;
 			for (int i = openSubMenu.Count - 1; i > index; i--) {
 				isMenuClosing = true;
@@ -1338,6 +1400,8 @@ namespace Terminal.Gui {
 					menu = openSubMenu [i - 1];
 				else
 					menu = openMenu;
+				if (!menu.Visible)
+					menu.Visible = true;
 				openCurrentMenu = menu;
 				openCurrentMenu.SetFocus ();
 				if (openSubMenu != null) {
@@ -1350,7 +1414,7 @@ namespace Terminal.Gui {
 					openSubMenu.Remove (menu);
 					menu.Dispose ();
 				}
-				RemoveSubMenu (i);
+				RemoveSubMenu (i, ignoreUseSubMenusSingleFrame);
 			}
 			if (openSubMenu.Count > 0)
 				openCurrentMenu = openSubMenu.Last ();
@@ -1397,7 +1461,7 @@ namespace Terminal.Gui {
 			if (!isMenuOpening && !isMenuClosing) {
 				if (openSubMenu != null && !CloseMenu (false, true))
 					return;
-				if (!CloseMenu ())
+				if (!CloseMenu (false))
 					return;
 				if (LastFocused != null && LastFocused != this)
 					selected = -1;
@@ -1420,7 +1484,7 @@ namespace Terminal.Gui {
 			return view;
 		}
 
-		internal void PreviousMenu (bool isSubMenu = false)
+		internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false)
 		{
 			switch (isSubMenu) {
 			case false:
@@ -1429,20 +1493,20 @@ namespace Terminal.Gui {
 				else
 					selected--;
 
-				if (selected > -1 && !CloseMenu (true, false))
+				if (selected > -1 && !CloseMenu (true, false, ignoreUseSubMenusSingleFrame))
 					return;
 				OpenMenu (selected);
 				if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current, false)) {
 					openCurrentMenu.current = 0;
 					if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
-						CloseMenu ();
+						CloseMenu (ignoreUseSubMenusSingleFrame);
 					}
 				}
 				break;
 			case true:
 				if (selectedSub > -1) {
 					selectedSub--;
-					RemoveSubMenu (selectedSub);
+					RemoveSubMenu (selectedSub, ignoreUseSubMenusSingleFrame);
 					SetNeedsDisplay ();
 				} else
 					PreviousMenu ();
@@ -1451,7 +1515,7 @@ namespace Terminal.Gui {
 			}
 		}
 
-		internal void NextMenu (bool isSubMenu = false)
+		internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false)
 		{
 			switch (isSubMenu) {
 			case false:
@@ -1462,22 +1526,22 @@ namespace Terminal.Gui {
 				else
 					selected++;
 
-				if (selected > -1 && !CloseMenu (true))
+				if (selected > -1 && !CloseMenu (true, ignoreUseSubMenusSingleFrame))
 					return;
 				OpenMenu (selected);
 				SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current);
 				break;
 			case true:
 				if (UseKeysUpDownAsKeysLeftRight) {
-					if (CloseMenu (false, true)) {
-						NextMenu ();
+					if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) {
+						NextMenu (false, ignoreUseSubMenusSingleFrame);
 					}
 				} else {
 					var subMenu = openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current]);
 					if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) {
 						if (openSubMenu != null && !CloseMenu (false, true))
 							return;
-						NextMenu ();
+						NextMenu (false, ignoreUseSubMenusSingleFrame);
 					} else if (subMenu != null ||
 						!openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu)
 						selectedSub++;
@@ -1498,7 +1562,7 @@ namespace Terminal.Gui {
 			for (int i = 0; i < Menus.Length; i++) {
 				// TODO: this code is duplicated, hotkey should be part of the MenuBarItem
 				var mi = Menus [i];
-				int p = mi.Title.IndexOf ('_');
+				int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier);
 				if (p != -1 && p + 1 < mi.Title.RuneCount) {
 					if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) {
 						ProcessMenu (i, mi);
@@ -1543,7 +1607,7 @@ namespace Terminal.Gui {
 
 		private void ProcessMenu (int i, MenuBarItem mi)
 		{
-			if (selected < 0) {
+			if (selected < 0 && IsMenuOpen) {
 				return;
 			}
 
@@ -1556,7 +1620,7 @@ namespace Terminal.Gui {
 				Application.GrabMouse (this);
 				selected = i;
 				OpenMenu (i);
-				if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu ()) {
+				if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) {
 					return;
 				}
 				if (!openCurrentMenu.CheckSubMenu ())
@@ -1577,7 +1641,7 @@ namespace Terminal.Gui {
 			}
 
 			// To ncurses simulate a AltMask key pressing Alt+Space because
-			// it cant detect an alone special key down was pressed.
+			// it can't detect an alone special key down was pressed.
 			if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) {
 				OnKeyDown (kb);
 				OnKeyUp (kb);
@@ -1606,7 +1670,7 @@ namespace Terminal.Gui {
 				foreach (var mi in Menus [selected].Children) {
 					if (mi == null)
 						continue;
-					int p = mi.Title.IndexOf ('_');
+					int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier);
 					if (p != -1 && p + 1 < mi.Title.RuneCount) {
 						if (mi.Title [p + 1] == c) {
 							Selected (mi);
@@ -1621,7 +1685,7 @@ namespace Terminal.Gui {
 
 		void CloseMenuBar ()
 		{
-			if (!CloseMenu ())
+			if (!CloseMenu (false))
 				return;
 			if (openedByAltKey) {
 				openedByAltKey = false;
@@ -1758,7 +1822,9 @@ namespace Terminal.Gui {
 					isContextMenuLoading = false;
 					return false;
 				}
-			} else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
+			} else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked
+				|| me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
+
 				Application.GrabMouse (current);
 			} else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) {
 				Application.GrabMouse (me.View);

+ 465 - 0
UnitTests/MenuTests.cs

@@ -589,5 +589,470 @@ Edit
 			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
 			Assert.Equal (new Point (0, 1), pos);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void UseSubMenusSingleFrame_False_By_Keyboard ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("Numbers", new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuBarItem ("Two", new MenuItem [] {
+						new MenuItem ("Sub-Menu 1", "", null),
+						new MenuItem ("Sub-Menu 2", "", null)
+					}),
+					new MenuItem ("Three", "", null),
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y));
+			Assert.False (menu.UseSubMenusSingleFrame);
+
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  Numbers
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│┌─────────────┐
+│ Three  ││ Sub-Menu 1  │
+└────────┘│ Sub-Menu 2  │
+          └─────────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.CursorLeft, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void UseSubMenusSingleFrame_False_By_Mouse ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("Numbers", new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuBarItem ("Two", new MenuItem [] {
+						new MenuItem ("Sub-Menu 1", "", null),
+						new MenuItem ("Sub-Menu 2", "", null)
+					}),
+					new MenuItem ("Three", "", null),
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y));
+			Assert.False (menu.UseSubMenusSingleFrame);
+
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  Numbers
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed,
+				View = menu
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 3,
+				Flags = MouseFlags.ReportMousePosition,
+				View = Application.Top.Subviews [1]
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│┌─────────────┐
+│ Three  ││ Sub-Menu 1  │
+└────────┘│ Sub-Menu 2  │
+          └─────────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 2,
+				Flags = MouseFlags.ReportMousePosition,
+				View = Application.Top.Subviews [1]
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 70,
+				Y = 2,
+				Flags = MouseFlags.Button1Clicked,
+				View = Application.Top
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void UseSubMenusSingleFrame_True_By_Keyboard ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("Numbers", new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuBarItem ("Two", new MenuItem [] {
+						new MenuItem ("Sub-Menu 1", "", null),
+						new MenuItem ("Sub-Menu 2", "", null)
+					}),
+					new MenuItem ("Three", "", null),
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y));
+			Assert.False (menu.UseSubMenusSingleFrame);
+			menu.UseSubMenusSingleFrame = true;
+			Assert.True (menu.UseSubMenusSingleFrame);
+
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  Numbers
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), 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
+┌─────────────┐
+│◄    Two     │
+├─────────────┤
+│ Sub-Menu 1  │
+│ Sub-Menu 2  │
+└─────────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.Enter, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null)));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void UseSubMenusSingleFrame_True_By_Mouse ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("Numbers", new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuBarItem ("Two", new MenuItem [] {
+						new MenuItem ("Sub-Menu 1", "", null),
+						new MenuItem ("Sub-Menu 2", "", null)
+					}),
+					new MenuItem ("Three", "", null),
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y));
+			Assert.False (menu.UseSubMenusSingleFrame);
+			menu.UseSubMenusSingleFrame = true;
+			Assert.True (menu.UseSubMenusSingleFrame);
+
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  Numbers
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed,
+				View = menu
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 3,
+				Flags = MouseFlags.Button1Clicked,
+				View = Application.Top.Subviews [1]
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌─────────────┐
+│◄    Two     │
+├─────────────┤
+│ Sub-Menu 1  │
+│ Sub-Menu 2  │
+└─────────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 1,
+				Y = 2,
+				Flags = MouseFlags.Button1Clicked,
+				View = Application.Top.Subviews [2]
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.False (menu.MouseEvent (new MouseEvent () {
+				X = 70,
+				Y = 2,
+				Flags = MouseFlags.Button1Clicked,
+				View = Application.Top
+			}));
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+  Numbers
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKey ()
+		{
+			var newAction = false;
+			var copyAction = false;
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_File", new MenuItem [] {
+					new MenuItem ("_New", "", () => newAction = true)
+				}),
+				new MenuBarItem ("_Edit", new MenuItem [] {
+					new MenuItem ("_Copy", "", () => copyAction = true)
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.False (newAction);
+			Assert.False (copyAction);
+
+			Assert.False (menu.OnKeyDown (new (Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (menu.OnKeyUp (new (Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (menu.IsMenuOpen);
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  File   Edit
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (menu.ProcessKey (new (Key.N, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (newAction);
+
+			Assert.False (menu.OnKeyDown (new (Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (menu.OnKeyUp (new (Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (menu.IsMenuOpen);
+
+			Assert.True (menu.ProcessKey (new (Key.CursorRight, null)));
+			Assert.True (menu.ProcessKey (new (Key.C, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (copyAction);
+		}
+
+		[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)
+				}),
+				new MenuBarItem ("_Edit", new MenuItem [] {
+					new MenuItem ("_Copy", "", () => copyAction = true)
+				})
+			});
+
+			Application.Top.Add (menu);
+
+			Assert.False (newAction);
+			Assert.False (copyAction);
+
+			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.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.N, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (newAction);
+
+			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.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (2, 0), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.C, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (copyAction);
+		}
 	}
 }