Browse Source

Merge pull request #1634 from BDisp/UseSubMenusSingleFrame-feature

Feature to display the sub-menus on a single frame instead multiple frames.
Tig Kindel 3 years ago
parent
commit
1595bbb39a
42 changed files with 1162 additions and 368 deletions
  1. 27 13
      Example/demo.cs
  2. 18 5
      Terminal.Gui/Core/ContextMenu.cs
  3. 2 4
      Terminal.Gui/Core/Toplevel.cs
  4. 125 57
      Terminal.Gui/Views/Menu.cs
  5. 2 2
      UICatalog/Scenario.cs
  6. 1 1
      UICatalog/Scenarios/AllViewsTester.cs
  7. 1 1
      UICatalog/Scenarios/AutoSizeAndDirectionText.cs
  8. 1 1
      UICatalog/Scenarios/CharacterMap.cs
  9. 1 1
      UICatalog/Scenarios/ClassExplorer.cs
  10. 1 1
      UICatalog/Scenarios/ComboBoxIteration.cs
  11. 25 5
      UICatalog/Scenarios/ContextMenus.cs
  12. 1 1
      UICatalog/Scenarios/DynamicMenuBar.cs
  13. 1 1
      UICatalog/Scenarios/DynamicStatusBar.cs
  14. 1 1
      UICatalog/Scenarios/GraphViewExample.cs
  15. 1 1
      UICatalog/Scenarios/HexEditor.cs
  16. 1 1
      UICatalog/Scenarios/InteractiveTree.cs
  17. 1 1
      UICatalog/Scenarios/LineViewExample.cs
  18. 1 1
      UICatalog/Scenarios/ListViewWithSelection.cs
  19. 1 1
      UICatalog/Scenarios/ListsAndCombos.cs
  20. 1 1
      UICatalog/Scenarios/Mouse.cs
  21. 1 1
      UICatalog/Scenarios/MultiColouredTable.cs
  22. 1 1
      UICatalog/Scenarios/Notepad.cs
  23. 1 1
      UICatalog/Scenarios/Progress.cs
  24. 1 1
      UICatalog/Scenarios/ProgressBarStyles.cs
  25. 1 1
      UICatalog/Scenarios/Scrolling.cs
  26. 1 1
      UICatalog/Scenarios/SyntaxHighlighting.cs
  27. 1 1
      UICatalog/Scenarios/TabViewExample.cs
  28. 1 1
      UICatalog/Scenarios/TableEditor.cs
  29. 1 1
      UICatalog/Scenarios/TextAlignments.cs
  30. 1 1
      UICatalog/Scenarios/TextAlignmentsAndDirection.cs
  31. 1 1
      UICatalog/Scenarios/TextFormatterDemo.cs
  32. 1 1
      UICatalog/Scenarios/TextViewAutocompletePopup.cs
  33. 1 1
      UICatalog/Scenarios/TimeAndDate.cs
  34. 1 1
      UICatalog/Scenarios/TopLevelNoWindowBug.cs
  35. 1 1
      UICatalog/Scenarios/TreeUseCases.cs
  36. 1 1
      UICatalog/Scenarios/TreeViewFileSystem.cs
  37. 1 1
      UICatalog/Scenarios/Unicode.cs
  38. 1 1
      UICatalog/UICatalog.cs
  39. 188 30
      UnitTests/ContextMenuTests.cs
  40. 244 192
      UnitTests/GraphViewTests.cs
  41. 485 14
      UnitTests/MenuTests.cs
  42. 14 14
      UnitTests/ViewTests.cs

+ 27 - 13
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);
@@ -239,6 +239,7 @@ static class Demo {
 	static void Editor ()
 	{
 		Application.Init ();
+		Application.HeightAsBuffer = heightAsBuffer;
 
 		var ntop = Application.Top;
 
@@ -487,15 +488,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 +565,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;
 		}
 
@@ -583,13 +583,14 @@ static class Demo {
 	public static MenuBar menu;
 	public static CheckBox menuKeysStyle;
 	public static CheckBox menuAutoMouseNav;
+	private static bool heightAsBuffer = false;
 	static void MainApp ()
 	{
 		if (Debugger.IsAttached)
 			CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
 
-		Application.Init();
-		Application.HeightAsBuffer = true;
+		Application.Init ();
+		Application.HeightAsBuffer = heightAsBuffer;
 		//ConsoleDriver.Diagnostics = ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler;
 
 		var top = Application.Top;
@@ -621,6 +622,11 @@ static class Demo {
 		menuItems [2].Action = () => ShowMenuItem (menuItems [2]);
 		menuItems [3].Action = () => ShowMenuItem (menuItems [3]);
 
+		MenuItem miUseSubMenusSingleFrame = null;
+		var useSubMenusSingleFrame = false;
+
+		MenuItem miHeightAsBuffer = null;
+
 		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 +644,15 @@ 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
+				},
+				miHeightAsBuffer = new MenuItem ("_Height As Buffer", "", () => {
+					miHeightAsBuffer.Checked = heightAsBuffer = !heightAsBuffer;
+					Application.HeightAsBuffer = heightAsBuffer;
+				}) { CheckType = MenuItemCheckStyle.Checked, Checked = heightAsBuffer }
 			}),
 			new MenuBarItem ("_List Demos", new MenuItem [] {
 				new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true), null, null, Key.AltMask + 0.ToString () [0]),

+ 18 - 5
Terminal.Gui/Core/ContextMenu.cs

@@ -22,7 +22,7 @@ namespace Terminal.Gui {
 		/// <param name="host">The host view.</param>
 		/// <param name="menuItems">The menu items.</param>
 		public ContextMenu (View host, MenuBarItem menuItems) :
-			this (host.Frame.X + 1, host.Frame.Bottom, menuItems)
+			this (host.Frame.X, host.Frame.Y, menuItems)
 		{
 			Host = host;
 		}
@@ -75,8 +75,13 @@ namespace Terminal.Gui {
 			container.Resized += Container_Resized;
 			var frame = container.Frame;
 			var position = Position;
-			if (Host != null && position != new Point (Host.Frame.X + 1, Host.Frame.Bottom)) {
-				Position = position = new Point (Host.Frame.X + 1, Host.Frame.Bottom);
+			if (Host != null) {
+				Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
+				var pos = new Point (x, y);
+				pos.Y += Host.Frame.Height - 1;
+				if (position != pos) {
+					Position = position = pos;
+				}
 			}
 			var rect = Menu.MakeFrame (position.X, position.Y, MenuItens.Children);
 			if (rect.Right >= frame.Right) {
@@ -93,7 +98,9 @@ namespace Terminal.Gui {
 					if (Host == null) {
 						position.Y = frame.Bottom - rect.Height - 1;
 					} else {
-						position.Y = Host.Frame.Y - rect.Height;
+						Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
+						var pos = new Point (x, y);
+						position.Y = pos.Y - rect.Height - 1;
 					}
 				} else if (ForceMinimumPosToZero) {
 					position.Y = 0;
@@ -106,7 +113,8 @@ namespace Terminal.Gui {
 				X = position.X,
 				Y = position.Y,
 				Width = 0,
-				Height = 0
+				Height = 0,
+				UseSubMenusSingleFrame = UseSubMenusSingleFrame
 			};
 
 			menuBar.isContextMenuLoading = true;
@@ -201,5 +209,10 @@ namespace Terminal.Gui {
 		/// Gets the <see cref="Gui.MenuBar"/> that is hosting this context menu.
 		/// </summary>
 		public MenuBar MenuBar { get => menuBar; }
+
+		/// <summary>
+		/// Gets or sets if the sub-menus must be displayed in a single or multiple frames.
+		/// </summary>
+		public bool UseSubMenusSingleFrame { get; set; }
 	}
 }

+ 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;

+ 125 - 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,8 +757,10 @@ namespace Terminal.Gui {
 				if (item == null || !item.IsEnabled ()) disabled = true;
 				if (item != null && !disabled)
 					current = me.Y - 1;
-				if (!CheckSubMenu ())
+				if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
+					SetNeedsDisplay ();
 					return true;
+				}
 				host.OnMenuOpened ();
 				return true;
 			}
@@ -803,15 +830,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 +857,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 +933,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 +1040,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 +1200,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 +1250,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 +1269,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 +1323,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 +1391,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 +1402,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 +1416,7 @@ namespace Terminal.Gui {
 					openSubMenu.Remove (menu);
 					menu.Dispose ();
 				}
-				RemoveSubMenu (i);
+				RemoveSubMenu (i, ignoreUseSubMenusSingleFrame);
 			}
 			if (openSubMenu.Count > 0)
 				openCurrentMenu = openSubMenu.Last ();
@@ -1397,7 +1463,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 +1486,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 +1495,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 +1517,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 +1528,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 +1564,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 +1609,7 @@ namespace Terminal.Gui {
 
 		private void ProcessMenu (int i, MenuBarItem mi)
 		{
-			if (selected < 0) {
+			if (selected < 0 && IsMenuOpen) {
 				return;
 			}
 
@@ -1556,7 +1622,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 +1643,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 +1672,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 +1687,7 @@ namespace Terminal.Gui {
 
 		void CloseMenuBar ()
 		{
-			if (!CloseMenu ())
+			if (!CloseMenu (false))
 				return;
 			if (openedByAltKey) {
 				openedByAltKey = false;
@@ -1758,7 +1824,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);

+ 2 - 2
UICatalog/Scenario.cs

@@ -12,7 +12,7 @@ namespace UICatalog {
 	///  <list type="number">
 	///  <item><description>Create a new <c>.cs</c> file in the <cs>Scenarios</cs> directory that derives from <see cref="Scenario"/>.</description></item>
 	///  <item><description>Annotate the <see cref="Scenario"/> derived class with a <see cref="Scenario.ScenarioMetadata"/> attribute specifying the scenario's name and description.</description></item>
-	///  <item><description>Add one or more <see cref="Scenario.ScenarioCategory"/> attributes to the class specifying which categories the scenario belongs to. If you don't specify a category the scenario will show up in "All".</description></item>
+	///  <item><description>Add one or more <see cref="Scenario.ScenarioCategory"/> attributes to the class specifying which categories the scenario belongs to. If you don't specify a category the scenario will show up in "_All".</description></item>
 	///  <item><description>Implement the <see cref="Setup"/> override which will be called when a user selects the scenario to run.</description></item>
 	///  <item><description>Optionally, implement the <see cref="Init(Toplevel, ColorScheme)"/> and/or <see cref="Run"/> overrides to provide a custom implementation.</description></item>
 	///  </list>
@@ -213,7 +213,7 @@ namespace UICatalog {
 		/// </summary>
 		internal static List<string> GetAllCategories ()
 		{
-			List<string> categories = new List<string> () { "All" };
+			List<string> categories = new List<string> () { "_All" };
 			foreach (Type type in typeof (Scenario).Assembly.GetTypes ()
 			 .Where (myType => myType.IsClass && !myType.IsAbstract && myType.IsSubclassOf (typeof (Scenario)))) {
 				List<System.Attribute> attrs = System.Attribute.GetCustomAttributes (type).ToList ();

+ 1 - 1
UICatalog/Scenarios/AllViewsTester.cs

@@ -10,7 +10,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "All Views Tester", Description: "Provides a test UI for all classes derived from View")]
-	[ScenarioCategory ("Layout")]
+	[ScenarioCategory ("Layout"), ScenarioCategory ("StatusBar")]
 	public class AllViewsTester : Scenario {
 		Window _leftPane;
 		ListView _classListView;

+ 1 - 1
UICatalog/Scenarios/AutoSizeAndDirectionText.cs

@@ -2,7 +2,7 @@
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "AutoSize and Direction Text", Description: "Demonstrates the text auto-size and direction manipulation.")]
-	[ScenarioCategory ("Text")]
+	[ScenarioCategory ("Text"), ScenarioCategory ("AutoSize"), ScenarioCategory ("Direction")]
 	public class AutoSizeAndDirectionText : Scenario {
 		public override void Setup ()
 		{

+ 1 - 1
UICatalog/Scenarios/CharacterMap.cs

@@ -17,7 +17,7 @@ namespace UICatalog.Scenarios {
 	/// </summary>
 	[ScenarioMetadata (Name: "Character Map", Description: "Illustrates a custom control and Unicode")]
 	[ScenarioCategory ("Text")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("ScrollView")]
 	public class CharacterMap : Scenario {
 		CharMap _charMap;
 		public override void Setup ()

+ 1 - 1
UICatalog/Scenarios/ClassExplorer.cs

@@ -10,7 +10,7 @@ using Terminal.Gui.Trees;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Class Explorer", Description: "Tree view explorer for classes by namespace based on TreeView")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")]
 	public class ClassExplorer : Scenario {
 		private TreeView<object> treeView;
 		private TextView textView;

+ 1 - 1
UICatalog/Scenarios/ComboBoxIteration.cs

@@ -3,7 +3,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "ComboBoxIteration", Description: "ComboBox iteration.")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("ComboBox")]
 	public class ComboBoxIteration : Scenario {
 		public override void Setup ()
 		{

+ 25 - 5
UICatalog/Scenarios/ContextMenus.cs

@@ -5,19 +5,26 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "ContextMenus", Description: "Context Menu Sample")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Menu"), ScenarioCategory ("ContextMenu")]
 	public class ContextMenus : Scenario {
 		private ContextMenu contextMenu = new ContextMenu ();
 		private readonly List<CultureInfo> cultureInfos = Application.SupportedCultures;
 		private MenuItem miForceMinimumPosToZero;
 		private bool forceMinimumPosToZero = true;
 		private TextField tfTopLeft, tfTopRight, tfMiddle, tfBottomLeft, tfBottomRight;
+		private MenuItem miUseSubMenusSingleFrame;
+		private bool useSubMenusSingleFrame;
 
 		public override void Setup ()
 		{
 			var text = "Context Menu";
 			var width = 20;
 
+			Win.Add (new Label ("Press 'Ctrl + Space' to open the Window context menu.") {
+				X = Pos.Center (),
+				Y = 1
+			});
+
 			tfTopLeft = new TextField (text) {
 				Width = width
 			};
@@ -52,7 +59,7 @@ namespace UICatalog.Scenarios {
 			Point mousePos = default;
 
 			Win.KeyPress += (e) => {
-				if (e.KeyEvent.Key == (Key.Space | Key.CtrlMask) && !ContextMenu.IsShow) {
+				if (e.KeyEvent.Key == (Key.Space | Key.CtrlMask)) {
 					ShowContextMenu (mousePos.X, mousePos.Y);
 					e.Handled = true;
 				}
@@ -63,12 +70,21 @@ namespace UICatalog.Scenarios {
 					ShowContextMenu (e.MouseEvent.X, e.MouseEvent.Y);
 					e.Handled = true;
 				}
-				mousePos = new Point (e.MouseEvent.X, e.MouseEvent.Y);
 			};
 
+			Application.RootMouseEvent += Application_RootMouseEvent;
+
+			void Application_RootMouseEvent (MouseEvent me)
+			{
+				mousePos = new Point (me.X, me.Y);
+			}
+
 			Win.WantMousePositionReports = true;
 
-			Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
+			Top.Closed += (_) => {
+				Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
+				Application.RootMouseEvent -= Application_RootMouseEvent;
+			};
 		}
 
 		private void ShowContextMenu (int x, int y)
@@ -89,10 +105,14 @@ namespace UICatalog.Scenarios {
 						tfBottomLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
 						tfBottomRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
 					}) { CheckType = MenuItemCheckStyle.Checked, Checked = forceMinimumPosToZero },
+					miUseSubMenusSingleFrame = new MenuItem ("Use_SubMenusSingleFrame", "",
+						() => contextMenu.UseSubMenusSingleFrame = miUseSubMenusSingleFrame.Checked = useSubMenusSingleFrame = !useSubMenusSingleFrame) {
+							CheckType = MenuItemCheckStyle.Checked, Checked = useSubMenusSingleFrame
+						},
 					null,
 					new MenuItem ("_Quit", "", () => Application.RequestStop ())
 				})
-			) { ForceMinimumPosToZero = forceMinimumPosToZero };
+			) { ForceMinimumPosToZero = forceMinimumPosToZero, UseSubMenusSingleFrame = useSubMenusSingleFrame };
 
 			tfTopLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
 			tfTopRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;

+ 1 - 1
UICatalog/Scenarios/DynamicMenuBar.cs

@@ -10,7 +10,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Dynamic MenuBar", Description: "Demonstrates how to add and remove a MenuBar, Menus and change titles dynamically.")]
-	[ScenarioCategory ("Dynamic")]
+	[ScenarioCategory ("Menu")]
 	public class DynamicMenuBar : Scenario {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{

+ 1 - 1
UICatalog/Scenarios/DynamicStatusBar.cs

@@ -10,7 +10,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Dynamic StatusBar", Description: "Demonstrates how to add and remove a StatusBar and change items dynamically.")]
-	[ScenarioCategory ("Dynamic")]
+	[ScenarioCategory ("StatusBar")]
 	public class DynamicStatusBar : Scenario {
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{

+ 1 - 1
UICatalog/Scenarios/GraphViewExample.cs

@@ -9,7 +9,7 @@ using Color = Terminal.Gui.Color;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Graph View", Description: "Demos GraphView control")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("Graph")]
 	public class GraphViewExample : Scenario {
 
 		GraphView graphView;

+ 1 - 1
UICatalog/Scenarios/HexEditor.cs

@@ -9,7 +9,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Text")]
 	[ScenarioCategory ("Dialogs")]
-	[ScenarioCategory ("TopLevel")]
+	[ScenarioCategory ("TopLevel"), ScenarioCategory ("IO.Stream")]
 	public class HexEditor : Scenario {
 		private string _fileName = "demo.bin";
 		private HexView _hexView;

+ 1 - 1
UICatalog/Scenarios/InteractiveTree.cs

@@ -10,7 +10,7 @@ using static UICatalog.Scenario;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Interactive Tree", Description: "Create nodes and child nodes in TreeView")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")]
 	public class InteractiveTree : Scenario {
 
 		TreeView treeView;

+ 1 - 1
UICatalog/Scenarios/LineViewExample.cs

@@ -10,7 +10,7 @@ using static UICatalog.Scenario;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Line View", Description: "Demonstrates the LineView control")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("Lines")]
 	public class LineViewExample : Scenario {
 
 		public override void Setup ()

+ 1 - 1
UICatalog/Scenarios/ListViewWithSelection.cs

@@ -8,7 +8,7 @@ using Attribute = Terminal.Gui.Attribute;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "List View With Selection", Description: "ListView with columns and selection")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("ListView")]
 	public class ListViewWithSelection : Scenario {
 
 		public CheckBox _customRenderCB;

+ 1 - 1
UICatalog/Scenarios/ListsAndCombos.cs

@@ -7,7 +7,7 @@ using NStack;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "ListView & ComboBox", Description: "Demonstrates a ListView populating a ComboBox that acts as a filter.")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("ListView"), ScenarioCategory ("ComboBox")]
 	public class ListsAndCombos : Scenario {
 
 		public override void Setup ()

+ 1 - 1
UICatalog/Scenarios/Mouse.cs

@@ -5,7 +5,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Mouse", Description: "Demonstrates how to capture mouse events")]
-	[ScenarioCategory ("Input")]
+	[ScenarioCategory ("Input"), ScenarioCategory ("Mouse")]
 	public class Mouse : Scenario {
 		public override void Setup ()
 		{

+ 1 - 1
UICatalog/Scenarios/MultiColouredTable.cs

@@ -5,7 +5,7 @@ using Terminal.Gui;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "MultiColouredTable", Description: "Demonstrates how to multi color cell contents")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("Colors"), ScenarioCategory ("TableView")]
 	public class MultiColouredTable : Scenario {
 		TableViewColors tableView;
 

+ 1 - 1
UICatalog/Scenarios/Notepad.cs

@@ -10,7 +10,7 @@ using static UICatalog.Scenario;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Notepad", Description: "Multi tab text editor")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TabView")]
 	public class Notepad : Scenario {
 
 		TabView tabView;

+ 1 - 1
UICatalog/Scenarios/Progress.cs

@@ -11,7 +11,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Progress", Description: "Shows off ProgressBar and Threading")]
 	[ScenarioCategory ("Controls")]
 	[ScenarioCategory ("MainLoop")]
-	[ScenarioCategory ("Threading")]
+	[ScenarioCategory ("Threading"), ScenarioCategory ("ProgressBar")]
 	public class Progress : Scenario {
 
 		class ProgressDemo : FrameView {

+ 1 - 1
UICatalog/Scenarios/ProgressBarStyles.cs

@@ -6,7 +6,7 @@ using Terminal.Gui;
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "ProgressBar Styles", Description: "Shows the ProgressBar Styles")]
 	[ScenarioCategory ("Controls")]
-	[ScenarioCategory ("MainLoop")]
+	[ScenarioCategory ("MainLoop"), ScenarioCategory ("ProgressBar")]
 	public class ProgressBarStyles : Scenario {
 		private Timer _fractionTimer;
 		private Timer _pulseTimer;

+ 1 - 1
UICatalog/Scenarios/Scrolling.cs

@@ -5,7 +5,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Scrolling", Description: "Demonstrates ScrollView etc...")]
 	[ScenarioCategory ("Controls")]
 	[ScenarioCategory ("Bug Repro")]
-
+	[ScenarioCategory ("ScrollView")]
 	public class Scrolling : Scenario {
 
 		class Box10x : View {

+ 1 - 1
UICatalog/Scenarios/SyntaxHighlighting.cs

@@ -8,7 +8,7 @@ using Attribute = Terminal.Gui.Attribute;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Syntax Highlighting", Description: "Text editor with keyword highlighting")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("AutoComplete")]
 	public class SyntaxHighlighting : Scenario {
 
 		SqlTextView textView;

+ 1 - 1
UICatalog/Scenarios/TabViewExample.cs

@@ -10,7 +10,7 @@ using static UICatalog.Scenario;
 namespace UICatalog.Scenarios {
 
 	[ScenarioMetadata (Name: "Tab View", Description: "Demos TabView control with limited screen space in Absolute layout")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TabView")]
 	public class TabViewExample : Scenario {
 
 		TabView tabView;

+ 1 - 1
UICatalog/Scenarios/TableEditor.cs

@@ -12,7 +12,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Text")]
 	[ScenarioCategory ("Dialogs")]
-	[ScenarioCategory ("TopLevel")]
+	[ScenarioCategory ("TopLevel"), ScenarioCategory ("TableView")]
 	public class TableEditor : Scenario 
 	{
 		TableView tableView;

+ 1 - 1
UICatalog/Scenarios/TextAlignments.cs

@@ -5,7 +5,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Text Alignment", Description: "Demonstrates text alignment")]
-	[ScenarioCategory ("Text")]
+	[ScenarioCategory ("Text"), ScenarioCategory ("Alignment")]
 	public class TextAlignments : Scenario {
 		public override void Setup ()
 		{

+ 1 - 1
UICatalog/Scenarios/TextAlignmentsAndDirection.cs

@@ -5,7 +5,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Text Alignment and Direction", Description: "Demonstrates text alignment")]
-	[ScenarioCategory ("Text")]
+	[ScenarioCategory ("Text"), ScenarioCategory ("Alignment"), ScenarioCategory ("Direction")]
 	public class TextAlignmentsAndDirections : Scenario {
 
 		public override void Setup ()

+ 1 - 1
UICatalog/Scenarios/TextFormatterDemo.cs

@@ -9,7 +9,7 @@ using Rune = System.Rune;
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "TextFormatter Demo", Description: "Demos and tests the TextFormatter class.")]
 	[ScenarioCategory ("Text")]
-	[ScenarioCategory ("POC")]
+	[ScenarioCategory ("POC"), ScenarioCategory ("TextFormat")]
 	public class TextFormatterDemo : Scenario {
 		public override void Setup ()
 		{

+ 1 - 1
UICatalog/Scenarios/TextViewAutocompletePopup.cs

@@ -4,7 +4,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "TextView Autocomplete Popup", Description: "Show five TextView Autocomplete Popup effects")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("AutoComplete")]
 	public class TextViewAutocompletePopup : Scenario {
 
 		TextView textViewTopLeft;

+ 1 - 1
UICatalog/Scenarios/TimeAndDate.cs

@@ -3,7 +3,7 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Time And Date", Description: "Illustrates TimeField and time & date handling")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("DateTime")]
 	[ScenarioCategory ("Bug Repro")] // Issue #246
 	public class TimeAndDate : Scenario {
 		Label lblOldTime;

+ 1 - 1
UICatalog/Scenarios/TopLevelNoWindowBug.cs

@@ -2,7 +2,7 @@
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "TopLevelNoWindowBug", Description: "Illustrates that not having a Window causes MenuBar to misbehave. #437")]
-	[ScenarioCategory ("Bug Repro")]
+	[ScenarioCategory ("Bug Repro"), ScenarioCategory ("TopLevel")]
 
 	public class TopLevelNoWindowBug : Scenario {
 

+ 1 - 1
UICatalog/Scenarios/TreeUseCases.cs

@@ -7,7 +7,7 @@ using Terminal.Gui.Trees;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Tree View", Description: "Simple tree view examples")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView")]
 	public class TreeUseCases : Scenario {
 
 		View currentTree;

+ 1 - 1
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -7,7 +7,7 @@ using Terminal.Gui.Trees;
 
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("TreeView"), ScenarioCategory ("Files")]
 	public class TreeViewFileSystem : Scenario {
 
 		/// <summary>

+ 1 - 1
UICatalog/Scenarios/Unicode.cs

@@ -6,7 +6,7 @@ using Terminal.Gui;
 namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Unicode", Description: "Tries to test Unicode in all controls (#204)")]
 	[ScenarioCategory ("Text")]
-	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Controls"), ScenarioCategory ("Unicode")]
 	public class UnicodeInMenu : Scenario {
 		public override void Setup ()
 		{

+ 1 - 1
UICatalog/UICatalog.cs

@@ -681,7 +681,7 @@ namespace UICatalog {
 			_categoryListViewItem = _categoryListView.SelectedItem;
 			var item = _categories [_categoryListView.SelectedItem];
 			List<Type> newlist;
-			if (item.Equals ("All")) {
+			if (item.Equals ("_All")) {
 				newlist = _scenarios;
 
 			} else {

+ 188 - 30
UnitTests/ContextMenuTests.cs

@@ -42,7 +42,7 @@ namespace Terminal.Gui.Core {
 					new MenuItem ("Two", "", null)
 				})
 			);
-			Assert.Equal (new Point (6, 10), cm.Position);
+			Assert.Equal (new Point (5, 10), cm.Position);
 			Assert.Equal (2, cm.MenuItens.Children.Length);
 			Assert.NotNull (cm.Host);
 		}
@@ -269,8 +269,8 @@ namespace Terminal.Gui.Core {
                                                                         └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (72, 21), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (72, 21, 80, 4), pos);
 
 			cm.Hide ();
 			Assert.Equal (new Point (80, 25), cm.Position);
@@ -279,64 +279,100 @@ namespace Terminal.Gui.Core {
 		[Fact, AutoInitShutdown]
 		public void Show_Ensures_Display_Inside_The_Container_Without_Overlap_The_Host ()
 		{
-			var cm = new ContextMenu (new View () { X = 69, Y = 24, Width = 10, Height = 1 },
+			var view = new View ("View") {
+				X = Pos.AnchorEnd (10),
+				Y = Pos.AnchorEnd (1),
+				Width = 10,
+				Height = 1
+			};
+			var cm = new ContextMenu (view,
 				new MenuBarItem (new MenuItem [] {
 					new MenuItem ("One", "", null),
 					new MenuItem ("Two", "", null)
 				})
 			);
 
-			Assert.Equal (new Point (70, 25), cm.Position);
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (new Rect (70, 24, 10, 1), view.Frame);
+			Assert.Equal (new Point (0, 0), cm.Position);
 
 			cm.Show ();
-			Assert.Equal (new Point (70, 25), cm.Position);
-			Application.Begin (Application.Top);
+			Assert.Equal (new Point (70, 24), cm.Position);
+			Application.Top.Redraw (Application.Top.Bounds);
 
 			var expected = @"
                                                                       ┌──────┐
                                                                       │ One  │
                                                                       │ Two  │
                                                                       └──────┘
+                                                                      View
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (70, 21), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (70, 20, 78, 5), pos);
 
 			cm.Hide ();
-			Assert.Equal (new Point (70, 25), cm.Position);
+			Assert.Equal (new Point (70, 24), cm.Position);
 		}
 
 		[Fact, AutoInitShutdown]
 		public void Show_Display_Below_The_Bottom_Host_If_Has_Enough_Space ()
 		{
-			var cm = new ContextMenu (new View () { X = 10, Y = 5, Width = 10, Height = 1 },
+			var view = new View ("View") { X = 10, Y = 5, Width = 10, Height = 1 };
+			var cm = new ContextMenu (view,
 				new MenuBarItem (new MenuItem [] {
 					new MenuItem ("One", "", null),
 					new MenuItem ("Two", "", null)
 				})
 			);
 
-			Assert.Equal (new Point (11, 6), cm.Position);
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (new Point (10, 5), cm.Position);
+
+			cm.Show ();
+			Application.Top.Redraw (Application.Top.Bounds);
+			Assert.Equal (new Point (10, 5), cm.Position);
 
+			var expected = @"
+          View
+          ┌──────┐
+          │ One  │
+          │ Two  │
+          └──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (10, 5, 18, 5), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (10, 5), cm.Position);
 			cm.Host.X = 5;
 			cm.Host.Y = 10;
+			cm.Host.Height = 3;
 
 			cm.Show ();
-			Assert.Equal (new Point (6, 11), cm.Position);
-			Application.Begin (Application.Top);
+			Application.Top.Redraw (Application.Top.Bounds);
+			Assert.Equal (new Point (5, 12), cm.Position);
 
-			var expected = @"
-      ┌──────┐
-      │ One  │
-      │ Two  │
-      └──────┘
+			expected = @"
+     View
+
+
+     ┌──────┐
+     │ One  │
+     │ Two  │
+     └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (6, 12), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (5, 10, 13, 7), pos);
 
 			cm.Hide ();
-			Assert.Equal (new Point (6, 11), cm.Position);
+			Assert.Equal (new Point (5, 12), cm.Position);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -363,8 +399,8 @@ namespace Terminal.Gui.Core {
 └────
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 5, 4), pos);
 
 			cm.Hide ();
 			Assert.Equal (new Point (0, 0), cm.Position);
@@ -393,8 +429,8 @@ namespace Terminal.Gui.Core {
 │ Two  │
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 8, 3), pos);
 
 			cm.Hide ();
 			Assert.Equal (new Point (0, 0), cm.Position);
@@ -446,8 +482,8 @@ namespace Terminal.Gui.Core {
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 8, 4), pos);
 
 			cm.ForceMinimumPosToZero = false;
 			cm.Show ();
@@ -460,8 +496,8 @@ namespace Terminal.Gui.Core {
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (1, 0), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 7, 3), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -515,5 +551,127 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (menu, Application.mouseGrabView);
 			Assert.True (menu.IsMenuOpen);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void ContextMenu_On_Toplevel_With_A_MenuBar_TextField_StatusBar ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", "", null),
+				new MenuBarItem ("Edit", "", null)
+			});
+
+			var label = new Label ("Label:") {
+				X = 2,
+				Y = 3
+			};
+
+			var tf = new TextField ("TextField") {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+				Width = 20
+			};
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.F1, "~F1~ Help", null),
+				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", null)
+			});
+
+			Application.Top.Add (menu, label, tf, statusBar);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (45, 17);
+
+			Assert.Equal (new Rect (9, 3, 20, 1), tf.Frame);
+			Assert.True (tf.HasFocus);
+
+			tf.ContextMenu.Show ();
+			Assert.True (ContextMenu.IsShow);
+			Assert.Equal (new Point (9, 3), tf.ContextMenu.Position);
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  File   Edit
+
+
+  Label: TextField
+         ┌────────────────────────────┐
+         │ Select All          Ctrl+T │
+         │ Delete All    Ctrl+Shift+D │
+         │ Copy                Ctrl+C │
+         │ Cut                 Ctrl+X │
+         │ Paste               Ctrl+V │
+         │ Undo                Ctrl+Z │
+         │ Redo                Ctrl+Y │
+         └────────────────────────────┘
+
+
+
+ F1 Help │ ^Q Quit
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 39, 17), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContextMenu_On_Toplevel_With_A_MenuBar_Window_TextField_StatusBar ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", "", null),
+				new MenuBarItem ("Edit", "", null)
+			});
+
+			var label = new Label ("Label:") {
+				X = 2,
+				Y = 3
+			};
+
+			var tf = new TextField ("TextField") {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+				Width = 20
+			};
+
+			var win = new Window ("Window");
+			win.Add (label, tf);
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.F1, "~F1~ Help", null),
+				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", null)
+			});
+
+			Application.Top.Add (menu, win, statusBar);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (45, 17);
+
+
+			Assert.Equal (new Rect (9, 3, 20, 1), tf.Frame);
+			Assert.True (tf.HasFocus);
+
+			tf.ContextMenu.Show ();
+			Assert.True (ContextMenu.IsShow);
+			Assert.Equal (new Point (10, 5), tf.ContextMenu.Position);
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+  File   Edit
+┌ Window ───────────────────────────────────┐
+│                                           │
+│                                           │
+│                                           │
+│  Label: TextField                         │
+│         ┌────────────────────────────┐    │
+│         │ Select All          Ctrl+T │    │
+│         │ Delete All    Ctrl+Shift+D │    │
+│         │ Copy                Ctrl+C │    │
+│         │ Cut                 Ctrl+X │    │
+│         │ Paste               Ctrl+V │    │
+│         │ Undo                Ctrl+Z │    │
+│         │ Redo                Ctrl+Y │    │
+│         └────────────────────────────┘    │
+└───────────────────────────────────────────┘
+ F1 Help │ ^Q Quit
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 45, 17), pos);
+		}
 	}
 }

+ 244 - 192
UnitTests/GraphViewTests.cs

@@ -11,40 +11,40 @@ using System.Text.RegularExpressions;
 using Xunit.Abstractions;
 
 namespace Terminal.Gui.Views {
-		
+
 	#region Helper Classes
 	class FakeHAxis : HorizontalAxis {
 
 		public List<Point> DrawAxisLinePoints = new List<Point> ();
-		public List<int> LabelPoints = new List<int>();
+		public List<int> LabelPoints = new List<int> ();
 
 		protected override void DrawAxisLine (GraphView graph, int x, int y)
 		{
 			base.DrawAxisLine (graph, x, y);
-			DrawAxisLinePoints.Add (new Point(x, y));
+			DrawAxisLinePoints.Add (new Point (x, y));
 		}
 
 		public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
 		{
 			base.DrawAxisLabel (graph, screenPosition, text);
-			LabelPoints.Add(screenPosition);
+			LabelPoints.Add (screenPosition);
 		}
 	}
 
 	class FakeVAxis : VerticalAxis {
 
 		public List<Point> DrawAxisLinePoints = new List<Point> ();
-		public List<int> LabelPoints = new List<int>();
+		public List<int> LabelPoints = new List<int> ();
 
 		protected override void DrawAxisLine (GraphView graph, int x, int y)
 		{
 			base.DrawAxisLine (graph, x, y);
-			DrawAxisLinePoints.Add (new Point(x, y));
+			DrawAxisLinePoints.Add (new Point (x, y));
 		}
 		public override void DrawAxisLabel (GraphView graph, int screenPosition, string text)
 		{
 			base.DrawAxisLabel (graph, screenPosition, text);
-			LabelPoints.Add(screenPosition);
+			LabelPoints.Add (screenPosition);
 		}
 	}
 	#endregion
@@ -99,13 +99,13 @@ namespace Terminal.Gui.Views {
 			if (!string.Equals (expectedLook, actualLook)) {
 
 				// ignore trailing whitespace on each line
-				var trailingWhitespace = new Regex (@"\s+$",RegexOptions.Multiline);
-				
+				var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
+
 				// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
-				expectedLook =  trailingWhitespace.Replace(expectedLook,"").Trim();
+				expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
 				actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
 
-				// standardise line endings for the comparison
+				// standardize line endings for the comparison
 				expectedLook = expectedLook.Replace ("\r\n", "\n");
 				actualLook = actualLook.Replace ("\r\n", "\n");
 
@@ -116,48 +116,92 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
-		public static Point AssertDriverContentsWithPosAre (string expectedLook, ITestOutputHelper output)
+		public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
 		{
+			var lines = new List<List<char>> ();
 			var sb = new StringBuilder ();
 			var driver = ((FakeDriver)Application.Driver);
 			var x = -1;
 			var y = -1;
+			int w = -1;
+			int h = -1;
 
 			var contents = driver.Contents;
 
 			for (int r = 0; r < driver.Rows; r++) {
+				var runes = new List<char> ();
 				for (int c = 0; c < driver.Cols; c++) {
 					var rune = (char)contents [r, c, 0];
-					if (x == -1 && rune != ' ') {
-						x = c;
-						y = r;
+					if (rune != ' ') {
+						if (x == -1) {
+							x = c;
+							y = r;
+							for (int i = 0; i < c; i++) {
+								runes.InsertRange (i, new List<char> () { ' ' });
+							}
+						}
+						if (c > w) {
+							w = c;
+						}
+						h = r - y;
+					}
+					if (x > -1) {
+						runes.Add (rune);
 					}
-					sb.Append (rune);
 				}
-				sb.AppendLine ();
+				if (runes.Count > 0) {
+					lines.Add (runes);
+				}
 			}
 
-			var actualLook = sb.ToString ();
+			// Remove unnecessary empty lines
+			for (int r = lines.Count - 1; r > h; r--) {
+				lines.RemoveAt (r);
+			}
 
-			if (!string.Equals (expectedLook, actualLook)) {
+			// Remove trailing whitespace on each line
+			for (int r = 0; r < lines.Count; r++) {
+				List<char> row = lines [r];
+				for (int c = row.Count - 1; c >= 0; c--) {
+					if (row [c] != ' ') {
+						break;
+					}
+					row.RemoveAt (c);
+				}
+			}
 
-				// ignore trailing whitespace on each line
-				var trailingWhitespace = new Regex (@"\s+$", RegexOptions.Multiline);
+			// Convert char list to string
+			for (int r = 0; r < lines.Count; r++) {
+				var line = new string (lines [r].ToArray ());
+				if (r == lines.Count - 1) {
+					sb.Append (line);
+				} else {
+					sb.AppendLine (line);
+				}
+			}
 
-				// get rid of trailing whitespace on each line (and leading/trailing whitespace of start/end of full string)
-				expectedLook = trailingWhitespace.Replace (expectedLook, "").Trim ();
-				actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
+			var actualLook = sb.ToString ();
 
-				// standardise line endings for the comparison
+			if (!string.Equals (expectedLook, actualLook)) {
+
+				// standardize line endings for the comparison
 				expectedLook = expectedLook.Replace ("\r\n", "\n");
 				actualLook = actualLook.Replace ("\r\n", "\n");
 
+				// Remove the first and the last line ending from the expectedLook
+				if (expectedLook.StartsWith ("\n")) {
+					expectedLook = expectedLook [1..];
+				}
+				if (expectedLook.EndsWith ("\n")) {
+					expectedLook = expectedLook [..^1];
+				}
+
 				output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
 				output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
 
 				Assert.Equal (expectedLook, actualLook);
 			}
-			return new Point (x, y);
+			return new Rect (x, y, w > -1 ? w + 1 : 0, h > -1 ? h + 1 : 0);
 		}
 
 #pragma warning disable xUnit1013 // Public method should be marked as test
@@ -168,11 +212,11 @@ namespace Terminal.Gui.Views {
 		/// </summary>
 		/// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console.  Must be valid indexes of <paramref name="expectedColors"/></param>
 		/// <param name="expectedColors"></param>
-		public static void AssertDriverColorsAre (string expectedLook, Attribute[] expectedColors)
+		public static void AssertDriverColorsAre (string expectedLook, Attribute [] expectedColors)
 		{
 #pragma warning restore xUnit1013 // Public method should be marked as test
 
-			if(expectedColors.Length > 10) {
+			if (expectedColors.Length > 10) {
 				throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
 			}
 
@@ -182,7 +226,7 @@ namespace Terminal.Gui.Views {
 			var contents = driver.Contents;
 
 			int r = 0;
-			foreach(var line in expectedLook.Split ('\n').Select(l=>l.Trim())) {
+			foreach (var line in expectedLook.Split ('\n').Select (l => l.Trim ())) {
 
 				for (int c = 0; c < line.Length; c++) {
 
@@ -195,10 +239,10 @@ namespace Terminal.Gui.Views {
 						throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
 					}
 
-					var colorUsed = Array.IndexOf(expectedColors,match[0]).ToString()[0];
+					var colorUsed = Array.IndexOf (expectedColors, match [0]).ToString () [0];
 					var userExpected = line [c];
 
-					if( colorUsed != userExpected) {
+					if (colorUsed != userExpected) {
 						throw new Exception ($"Colors used did not match expected at row {r} and col {c}.  Color index used was {colorUsed} but test expected {userExpected} (these are indexes into the expectedColors array)");
 					}
 				}
@@ -434,7 +478,7 @@ namespace Terminal.Gui.Views {
 		/// is mutable a sensible place to check this is in redraw.
 		/// </summary>
 		[Fact]
-		public void CellSizeZero()
+		public void CellSizeZero ()
 		{
 			InitFakeDriver ();
 
@@ -442,8 +486,8 @@ namespace Terminal.Gui.Views {
 			gv.ColorScheme = new ColorScheme ();
 			gv.Bounds = new Rect (0, 0, 50, 30);
 			gv.Series.Add (new ScatterSeries () { Points = new List<PointF> { new PointF (1, 1) } });
-			gv.CellSize= new PointF(0,5);
-			var ex = Assert.Throws<Exception>(()=>gv.Redraw (gv.Bounds));
+			gv.CellSize = new PointF (0, 5);
+			var ex = Assert.Throws<Exception> (() => gv.Redraw (gv.Bounds));
 
 			Assert.Equal ("CellSize cannot be 0", ex.Message);
 
@@ -451,7 +495,7 @@ namespace Terminal.Gui.Views {
 			Application.Shutdown ();
 		}
 
-		
+
 
 		/// <summary>
 		/// Tests that each point in the screen space maps to a rectangle of
@@ -488,7 +532,7 @@ namespace Terminal.Gui.Views {
 					Assert.Equal (x, p.X);
 					Assert.Equal (y, p.Y);
 
-					p = gv.GraphSpaceToScreen (new PointF (graphSpace.Right - epsilon , graphSpace.Top + epsilon));
+					p = gv.GraphSpaceToScreen (new PointF (graphSpace.Right - epsilon, graphSpace.Top + epsilon));
 					Assert.Equal (x, p.X);
 					Assert.Equal (y, p.Y);
 
@@ -613,33 +657,35 @@ namespace Terminal.Gui.Views {
 		}
 	}
 
-	public class MultiBarSeriesTests{
+	public class MultiBarSeriesTests {
 
 		readonly ITestOutputHelper output;
 
-		public MultiBarSeriesTests(ITestOutputHelper output)
+		public MultiBarSeriesTests (ITestOutputHelper output)
 		{
 			this.output = output;
 		}
 
 		[Fact]
-		public void MultiBarSeries_BarSpacing(){
-			
+		public void MultiBarSeries_BarSpacing ()
+		{
+
 			// Creates clusters of 5 adjacent bars with 2 spaces between clusters
-			var series = new MultiBarSeries(5,7,1);
+			var series = new MultiBarSeries (5, 7, 1);
 
-			Assert.Equal(5,series.SubSeries.Count);
+			Assert.Equal (5, series.SubSeries.Count);
 
-			Assert.Equal(0,series.SubSeries.ElementAt(0).Offset);
-			Assert.Equal(1,series.SubSeries.ElementAt(1).Offset);
-			Assert.Equal(2,series.SubSeries.ElementAt(2).Offset);
-			Assert.Equal(3,series.SubSeries.ElementAt(3).Offset);
-			Assert.Equal(4,series.SubSeries.ElementAt(4).Offset);
+			Assert.Equal (0, series.SubSeries.ElementAt (0).Offset);
+			Assert.Equal (1, series.SubSeries.ElementAt (1).Offset);
+			Assert.Equal (2, series.SubSeries.ElementAt (2).Offset);
+			Assert.Equal (3, series.SubSeries.ElementAt (3).Offset);
+			Assert.Equal (4, series.SubSeries.ElementAt (4).Offset);
 		}
 
 
 		[Fact]
-		public void MultiBarSeriesColors_WrongNumber(){
+		public void MultiBarSeriesColors_WrongNumber ()
+		{
 
 			var fake = new FakeDriver ();
 
@@ -648,8 +694,8 @@ namespace Terminal.Gui.Views {
 			};
 
 			// user passes 1 color only but asks for 5 bars
-			var ex = Assert.Throws<ArgumentException>(()=>new MultiBarSeries(5,7,1,colors));
-			Assert.Equal("Number of colors must match the number of bars (Parameter 'numberOfBarsPerCategory')",ex.Message);
+			var ex = Assert.Throws<ArgumentException> (() => new MultiBarSeries (5, 7, 1, colors));
+			Assert.Equal ("Number of colors must match the number of bars (Parameter 'numberOfBarsPerCategory')", ex.Message);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -657,7 +703,8 @@ namespace Terminal.Gui.Views {
 
 
 		[Fact]
-		public void MultiBarSeriesColors_RightNumber(){
+		public void MultiBarSeriesColors_RightNumber ()
+		{
 
 			var fake = new FakeDriver ();
 
@@ -668,11 +715,11 @@ namespace Terminal.Gui.Views {
 			};
 
 			// user passes 3 colors and asks for 3 bars
-			var series = new MultiBarSeries(3,7,1,colors);
+			var series = new MultiBarSeries (3, 7, 1, colors);
 
-			Assert.Equal(series.SubSeries.ElementAt(0).OverrideBarColor,colors[0]);
-			Assert.Equal(series.SubSeries.ElementAt(1).OverrideBarColor,colors[1]);
-			Assert.Equal(series.SubSeries.ElementAt(2).OverrideBarColor,colors[2]);
+			Assert.Equal (series.SubSeries.ElementAt (0).OverrideBarColor, colors [0]);
+			Assert.Equal (series.SubSeries.ElementAt (1).OverrideBarColor, colors [1]);
+			Assert.Equal (series.SubSeries.ElementAt (2).OverrideBarColor, colors [2]);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -680,20 +727,22 @@ namespace Terminal.Gui.Views {
 
 
 		[Fact]
-		public void MultiBarSeriesAddValues_WrongNumber(){
-			
+		public void MultiBarSeriesAddValues_WrongNumber ()
+		{
+
 			// user asks for 3 bars per category
-			var series = new MultiBarSeries(3,7,1);
+			var series = new MultiBarSeries (3, 7, 1);
 
-			var ex = Assert.Throws<ArgumentException>(()=>series.AddBars("Cars",'#',1));
+			var ex = Assert.Throws<ArgumentException> (() => series.AddBars ("Cars", '#', 1));
 
-			Assert.Equal("Number of values must match the number of bars per category (Parameter 'values')",ex.Message);
+			Assert.Equal ("Number of values must match the number of bars per category (Parameter 'values')", ex.Message);
 		}
 
 
 
 		[Fact]
-		public void TestRendering_MultibarSeries(){
+		public void TestRendering_MultibarSeries ()
+		{
 
 			GraphViewTests.InitFakeDriver ();
 
@@ -703,14 +752,14 @@ namespace Terminal.Gui.Views {
 			// y axis goes from 0.1 to 1 across 10 console rows
 			// x axis goes from 0 to 20 across 20 console columns
 			gv.Bounds = new Rect (0, 0, 20, 10);
-			gv.CellSize = new PointF(1f,0.1f);
+			gv.CellSize = new PointF (1f, 0.1f);
 			gv.MarginBottom = 1;
 			gv.MarginLeft = 1;
 
-			var multibarSeries = new MultiBarSeries (2,4,1);
-			
+			var multibarSeries = new MultiBarSeries (2, 4, 1);
+
 			//nudge them left to avoid float rounding errors at the boundaries of cells
-			foreach(var sub in multibarSeries.SubSeries) {
+			foreach (var sub in multibarSeries.SubSeries) {
 				sub.Offset -= 0.001f;
 			}
 
@@ -720,28 +769,28 @@ namespace Terminal.Gui.Views {
 
 			// don't show axis labels that means any labels
 			// that appaer are explicitly from the bars
-			gv.AxisX = fakeXAxis = new FakeHAxis(){Increment=0};
-			gv.AxisY = new FakeVAxis(){Increment=0};
+			gv.AxisX = fakeXAxis = new FakeHAxis () { Increment = 0 };
+			gv.AxisY = new FakeVAxis () { Increment = 0 };
 
-			gv.Redraw(gv.Bounds);
+			gv.Redraw (gv.Bounds);
 
 			// Since bar series has no bars yet no labels should be displayed
-			Assert.Empty(fakeXAxis.LabelPoints);
-
-			multibarSeries.AddBars("hey",'M',0.5001f, 0.5001f);
-			fakeXAxis.LabelPoints.Clear();
-			gv.Redraw(gv.Bounds);
-	
-			Assert.Equal(4,fakeXAxis.LabelPoints.Single());
-
-			multibarSeries.AddBars("there",'M',0.24999f,0.74999f);
-			multibarSeries.AddBars("bob",'M',1,2);
-			fakeXAxis.LabelPoints.Clear();
-			gv.Redraw(gv.Bounds);
-
-			Assert.Equal(3,fakeXAxis.LabelPoints.Count);
-			Assert.Equal(4,fakeXAxis.LabelPoints[0]);
-			Assert.Equal(8,fakeXAxis.LabelPoints[1]);
+			Assert.Empty (fakeXAxis.LabelPoints);
+
+			multibarSeries.AddBars ("hey", 'M', 0.5001f, 0.5001f);
+			fakeXAxis.LabelPoints.Clear ();
+			gv.Redraw (gv.Bounds);
+
+			Assert.Equal (4, fakeXAxis.LabelPoints.Single ());
+
+			multibarSeries.AddBars ("there", 'M', 0.24999f, 0.74999f);
+			multibarSeries.AddBars ("bob", 'M', 1, 2);
+			fakeXAxis.LabelPoints.Clear ();
+			gv.Redraw (gv.Bounds);
+
+			Assert.Equal (3, fakeXAxis.LabelPoints.Count);
+			Assert.Equal (4, fakeXAxis.LabelPoints [0]);
+			Assert.Equal (8, fakeXAxis.LabelPoints [1]);
 			Assert.Equal (12, fakeXAxis.LabelPoints [2]);
 
 			string looksLike =
@@ -763,7 +812,7 @@ namespace Terminal.Gui.Views {
 		}
 	}
 
-	public class BarSeriesTests{
+	public class BarSeriesTests {
 
 
 		private GraphView GetGraph (out FakeBarSeries series, out FakeHAxis axisX, out FakeVAxis axisY)
@@ -776,46 +825,47 @@ namespace Terminal.Gui.Views {
 			// y axis goes from 0.1 to 1 across 10 console rows
 			// x axis goes from 0 to 10 across 20 console columns
 			gv.Bounds = new Rect (0, 0, 20, 10);
-			gv.CellSize = new PointF(0.5f,0.1f);
+			gv.CellSize = new PointF (0.5f, 0.1f);
 
 			gv.Series.Add (series = new FakeBarSeries ());
 
 			// don't show axis labels that means any labels
 			// that appaer are explicitly from the bars
-			gv.AxisX = axisX = new FakeHAxis(){Increment=0};
-			gv.AxisY = axisY = new FakeVAxis(){Increment=0};
+			gv.AxisX = axisX = new FakeHAxis () { Increment = 0 };
+			gv.AxisY = axisY = new FakeVAxis () { Increment = 0 };
 
 			return gv;
 		}
 
 		[Fact]
-		public void TestZeroHeightBar_WithName(){
+		public void TestZeroHeightBar_WithName ()
+		{
 
-			var graph = GetGraph(out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
-			graph.Redraw(graph.Bounds);
+			var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
+			graph.Redraw (graph.Bounds);
 
 			// no bars
-			Assert.Empty(barSeries.BarScreenStarts);
-			Assert.Empty(axisX.LabelPoints);
-			Assert.Empty(axisY.LabelPoints);
+			Assert.Empty (barSeries.BarScreenStarts);
+			Assert.Empty (axisX.LabelPoints);
+			Assert.Empty (axisY.LabelPoints);
 
 			// bar of height 0
-			barSeries.Bars.Add(new BarSeries.Bar("hi",new GraphCellToRender('.'),0));
+			barSeries.Bars.Add (new BarSeries.Bar ("hi", new GraphCellToRender ('.'), 0));
 			barSeries.Orientation = Orientation.Vertical;
 
 			// redraw graph
-			graph.Redraw(graph.Bounds);
+			graph.Redraw (graph.Bounds);
 
 			// bar should not be drawn
-			Assert.Empty(barSeries.BarScreenStarts);
+			Assert.Empty (barSeries.BarScreenStarts);
 
-			Assert.NotEmpty(axisX.LabelPoints);
-			Assert.Empty(axisY.LabelPoints);
+			Assert.NotEmpty (axisX.LabelPoints);
+			Assert.Empty (axisY.LabelPoints);
 
 			// but bar name should be
 			// Screen position x=2 because bars are drawn every 1f of
 			// graph space and CellSize.X is 0.5f
-			Assert.Contains(2, axisX.LabelPoints);
+			Assert.Contains (2, axisX.LabelPoints);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -823,71 +873,73 @@ namespace Terminal.Gui.Views {
 
 
 		[Fact]
-		public void TestTwoTallBars_WithOffset(){
+		public void TestTwoTallBars_WithOffset ()
+		{
 
-			var graph = GetGraph(out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
-			graph.Redraw(graph.Bounds);
+			var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
+			graph.Redraw (graph.Bounds);
 
 			// no bars
-			Assert.Empty(barSeries.BarScreenStarts);
-			Assert.Empty(axisX.LabelPoints);
-			Assert.Empty(axisY.LabelPoints);
+			Assert.Empty (barSeries.BarScreenStarts);
+			Assert.Empty (axisX.LabelPoints);
+			Assert.Empty (axisY.LabelPoints);
 
 			// 0.5 units of graph fit every screen cell
 			// so 1 unit of graph space is 2 screen columns
-			graph.CellSize = new PointF(0.5f,0.1f);
+			graph.CellSize = new PointF (0.5f, 0.1f);
 
 			// Start bar 1 screen unit along
 			barSeries.Offset = 0.5f;
 			barSeries.BarEvery = 1f;
 
-			barSeries.Bars.Add(
-				new BarSeries.Bar("hi1",new GraphCellToRender('.'),100));
-			barSeries.Bars.Add(
-				new BarSeries.Bar("hi2",new GraphCellToRender('.'),100));
+			barSeries.Bars.Add (
+				new BarSeries.Bar ("hi1", new GraphCellToRender ('.'), 100));
+			barSeries.Bars.Add (
+				new BarSeries.Bar ("hi2", new GraphCellToRender ('.'), 100));
 
 			barSeries.Orientation = Orientation.Vertical;
 
 			// redraw graph
-			graph.Redraw(graph.Bounds);
+			graph.Redraw (graph.Bounds);
 
 			// bar should be drawn at BarEvery 1f + offset 0.5f = 3 screen units
-			Assert.Equal(3,barSeries.BarScreenStarts[0].X);
-			Assert.Equal(3,barSeries.BarScreenEnds[0].X);
+			Assert.Equal (3, barSeries.BarScreenStarts [0].X);
+			Assert.Equal (3, barSeries.BarScreenEnds [0].X);
 
 			// second bar should be BarEveryx2 = 2f + offset 0.5f = 5 screen units
-			Assert.Equal(5,barSeries.BarScreenStarts[1].X);
-			Assert.Equal(5,barSeries.BarScreenEnds[1].X);
+			Assert.Equal (5, barSeries.BarScreenStarts [1].X);
+			Assert.Equal (5, barSeries.BarScreenEnds [1].X);
 
 			// both bars should have labels
-			Assert.Equal(2,axisX.LabelPoints.Count);
-			Assert.Contains(3, axisX.LabelPoints);
-			Assert.Contains(5, axisX.LabelPoints);
+			Assert.Equal (2, axisX.LabelPoints.Count);
+			Assert.Contains (3, axisX.LabelPoints);
+			Assert.Contains (5, axisX.LabelPoints);
 
 			// bars are very tall but should not draw up off top of screen
-			Assert.Equal(9,barSeries.BarScreenStarts[0].Y);
-			Assert.Equal(0,barSeries.BarScreenEnds[0].Y);
-			Assert.Equal(9,barSeries.BarScreenStarts[1].Y);
-			Assert.Equal(0,barSeries.BarScreenEnds[1].Y);
+			Assert.Equal (9, barSeries.BarScreenStarts [0].Y);
+			Assert.Equal (0, barSeries.BarScreenEnds [0].Y);
+			Assert.Equal (9, barSeries.BarScreenStarts [1].Y);
+			Assert.Equal (0, barSeries.BarScreenEnds [1].Y);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
 		}
 
 		[Fact]
-		public void TestOneLongOneShortHorizontalBars_WithOffset(){
+		public void TestOneLongOneShortHorizontalBars_WithOffset ()
+		{
 
-			var graph = GetGraph(out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
-			graph.Redraw(graph.Bounds);
+			var graph = GetGraph (out FakeBarSeries barSeries, out FakeHAxis axisX, out FakeVAxis axisY);
+			graph.Redraw (graph.Bounds);
 
 			// no bars
-			Assert.Empty(barSeries.BarScreenStarts);
-			Assert.Empty(axisX.LabelPoints);
-			Assert.Empty(axisY.LabelPoints);
+			Assert.Empty (barSeries.BarScreenStarts);
+			Assert.Empty (axisX.LabelPoints);
+			Assert.Empty (axisY.LabelPoints);
 
 			// 0.1 units of graph y fit every screen row
 			// so 1 unit of graph y space is 10 screen rows
-			graph.CellSize = new PointF(0.5f,0.1f);
+			graph.CellSize = new PointF (0.5f, 0.1f);
 
 			// Start bar 3 screen units up (y = height-3)
 			barSeries.Offset = 0.25f;
@@ -896,64 +948,64 @@ namespace Terminal.Gui.Views {
 			barSeries.Orientation = Orientation.Horizontal;
 
 			// 1 bar that is very wide (100 graph units horizontally = screen pos 50 but bounded by screen)
-			barSeries.Bars.Add(
-				new BarSeries.Bar("hi1",new GraphCellToRender('.'),100));
+			barSeries.Bars.Add (
+				new BarSeries.Bar ("hi1", new GraphCellToRender ('.'), 100));
 
 			// 1 bar that is shorter
-			barSeries.Bars.Add(
-				new BarSeries.Bar("hi2",new GraphCellToRender('.'),5));
+			barSeries.Bars.Add (
+				new BarSeries.Bar ("hi2", new GraphCellToRender ('.'), 5));
 
 			// redraw graph
-			graph.Redraw(graph.Bounds);
+			graph.Redraw (graph.Bounds);
 
 			// since bars are horizontal all have the same X start cordinates
-			Assert.Equal(0,barSeries.BarScreenStarts[0].X);
-			Assert.Equal(0,barSeries.BarScreenStarts[1].X);
+			Assert.Equal (0, barSeries.BarScreenStarts [0].X);
+			Assert.Equal (0, barSeries.BarScreenStarts [1].X);
 
 			// bar goes all the way to the end so bumps up against right screen boundary
 			// width of graph is 20
-			Assert.Equal(19,barSeries.BarScreenEnds[0].X);
+			Assert.Equal (19, barSeries.BarScreenEnds [0].X);
 
 			// shorter bar is 5 graph units wide which is 10 screen units
-			Assert.Equal(10,barSeries.BarScreenEnds[1].X);
+			Assert.Equal (10, barSeries.BarScreenEnds [1].X);
 
 			// first  bar should be offset 6 screen units (0.25f + 0.3f graph units)
 			// since height of control is 10 then first bar should be at screen row 4 (10-6)
-			Assert.Equal(4,barSeries.BarScreenStarts[0].Y);
+			Assert.Equal (4, barSeries.BarScreenStarts [0].Y);
 
 			// second  bar should be offset 9 screen units (0.25f + 0.6f graph units)
 			// since height of control is 10 then second bar should be at screen row 1 (10-9)
-			Assert.Equal(1,barSeries.BarScreenStarts[1].Y);
+			Assert.Equal (1, barSeries.BarScreenStarts [1].Y);
 
 			// both bars should have labels but on the y axis
-			Assert.Equal(2,axisY.LabelPoints.Count);
-			Assert.Empty(axisX.LabelPoints);
+			Assert.Equal (2, axisY.LabelPoints.Count);
+			Assert.Empty (axisX.LabelPoints);
 
 			// labels should align with the bars (same screen y axis point)
-			Assert.Contains(4, axisY.LabelPoints);
-			Assert.Contains(1, axisY.LabelPoints);
+			Assert.Contains (4, axisY.LabelPoints);
+			Assert.Contains (1, axisY.LabelPoints);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
 		}
 
-		private class FakeBarSeries : BarSeries{
+		private class FakeBarSeries : BarSeries {
 			public GraphCellToRender FinalColor { get; private set; }
 
-			public List<Point> BarScreenStarts { get; private set; } = new List<Point>();
-			public List<Point> BarScreenEnds { get; private set; } = new List<Point>();
-			
+			public List<Point> BarScreenStarts { get; private set; } = new List<Point> ();
+			public List<Point> BarScreenEnds { get; private set; } = new List<Point> ();
+
 			protected override GraphCellToRender AdjustColor (GraphCellToRender graphCellToRender)
 			{
-				return FinalColor = base.AdjustColor (graphCellToRender);	
+				return FinalColor = base.AdjustColor (graphCellToRender);
 			}
 
 			protected override void DrawBarLine (GraphView graph, Point start, Point end, Bar beingDrawn)
 			{
 				base.DrawBarLine (graph, start, end, beingDrawn);
-				
-				BarScreenStarts.Add(start);
-				BarScreenEnds.Add(end);
+
+				BarScreenStarts.Add (start);
+				BarScreenEnds.Add (end);
 			}
 
 		}
@@ -965,11 +1017,11 @@ namespace Terminal.Gui.Views {
 
 		private GraphView GetGraph (out FakeHAxis axis)
 		{
-			return GetGraph(out axis, out _);
+			return GetGraph (out axis, out _);
 		}
 		private GraphView GetGraph (out FakeVAxis axis)
 		{
-			return GetGraph(out _, out axis);
+			return GetGraph (out _, out axis);
 		}
 		private GraphView GetGraph (out FakeHAxis axisX, out FakeVAxis axisY)
 		{
@@ -1003,15 +1055,15 @@ namespace Terminal.Gui.Views {
 			gv.Redraw (gv.Bounds);
 
 			Assert.DoesNotContain (new Point (-1, 29), axis.DrawAxisLinePoints);
-			Assert.Contains (new Point (0, 29),axis.DrawAxisLinePoints);
+			Assert.Contains (new Point (0, 29), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (1, 29), axis.DrawAxisLinePoints);
-						
+
 			Assert.Contains (new Point (48, 29), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (49, 29), axis.DrawAxisLinePoints);
 			Assert.DoesNotContain (new Point (50, 29), axis.DrawAxisLinePoints);
 
-			Assert.InRange(axis.LabelPoints.Max(),0,49);
-			Assert.InRange(axis.LabelPoints.Min(),0,49);
+			Assert.InRange (axis.LabelPoints.Max (), 0, 49);
+			Assert.InRange (axis.LabelPoints.Min (), 0, 49);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1033,8 +1085,8 @@ namespace Terminal.Gui.Views {
 			Assert.Contains (new Point (49, 19), axis.DrawAxisLinePoints);
 			Assert.DoesNotContain (new Point (50, 19), axis.DrawAxisLinePoints);
 
-			Assert.InRange(axis.LabelPoints.Max(),0,49);
-			Assert.InRange(axis.LabelPoints.Min(),0,49);
+			Assert.InRange (axis.LabelPoints.Max (), 0, 49);
+			Assert.InRange (axis.LabelPoints.Min (), 0, 49);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1057,8 +1109,8 @@ namespace Terminal.Gui.Views {
 			Assert.DoesNotContain (new Point (50, 29), axis.DrawAxisLinePoints);
 
 			// Axis lables should not be drawn in the margin
-			Assert.InRange(axis.LabelPoints.Max(),5,49);
-			Assert.InRange(axis.LabelPoints.Min(),5,49);
+			Assert.InRange (axis.LabelPoints.Max (), 5, 49);
+			Assert.InRange (axis.LabelPoints.Min (), 5, 49);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1081,15 +1133,15 @@ namespace Terminal.Gui.Views {
 			gv.Redraw (gv.Bounds);
 
 			Assert.DoesNotContain (new Point (0, -1), axis.DrawAxisLinePoints);
-			Assert.Contains (new Point (0, 1),axis.DrawAxisLinePoints);
+			Assert.Contains (new Point (0, 1), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (0, 2), axis.DrawAxisLinePoints);
-						
+
 			Assert.Contains (new Point (0, 28), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (0, 29), axis.DrawAxisLinePoints);
 			Assert.DoesNotContain (new Point (0, 30), axis.DrawAxisLinePoints);
 
-			Assert.InRange(axis.LabelPoints.Max(),0,29);
-			Assert.InRange(axis.LabelPoints.Min(),0,29);
+			Assert.InRange (axis.LabelPoints.Max (), 0, 29);
+			Assert.InRange (axis.LabelPoints.Min (), 0, 29);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1104,16 +1156,16 @@ namespace Terminal.Gui.Views {
 			gv.Redraw (gv.Bounds);
 
 			Assert.DoesNotContain (new Point (0, -1), axis.DrawAxisLinePoints);
-			Assert.Contains (new Point (0, 1),axis.DrawAxisLinePoints);
+			Assert.Contains (new Point (0, 1), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (0, 2), axis.DrawAxisLinePoints);
-						
+
 			Assert.Contains (new Point (0, 18), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (0, 19), axis.DrawAxisLinePoints);
 			Assert.DoesNotContain (new Point (0, 20), axis.DrawAxisLinePoints);
 
 			// Labels should not be drawn into the axis
-			Assert.InRange(axis.LabelPoints.Max(),0,19);
-			Assert.InRange(axis.LabelPoints.Min(),0,19);
+			Assert.InRange (axis.LabelPoints.Max (), 0, 19);
+			Assert.InRange (axis.LabelPoints.Min (), 0, 19);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1128,15 +1180,15 @@ namespace Terminal.Gui.Views {
 			gv.Redraw (gv.Bounds);
 
 			Assert.DoesNotContain (new Point (5, -1), axis.DrawAxisLinePoints);
-			Assert.Contains (new Point (5, 1),axis.DrawAxisLinePoints);
+			Assert.Contains (new Point (5, 1), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (5, 2), axis.DrawAxisLinePoints);
-						
+
 			Assert.Contains (new Point (5, 28), axis.DrawAxisLinePoints);
 			Assert.Contains (new Point (5, 29), axis.DrawAxisLinePoints);
 			Assert.DoesNotContain (new Point (5, 30), axis.DrawAxisLinePoints);
 
-			Assert.InRange(axis.LabelPoints.Max(),0,29);
-			Assert.InRange(axis.LabelPoints.Min(),0,29);
+			Assert.InRange (axis.LabelPoints.Max (), 0, 29);
+			Assert.InRange (axis.LabelPoints.Min (), 0, 29);
 
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
@@ -1148,13 +1200,13 @@ namespace Terminal.Gui.Views {
 	public class TextAnnotationTests {
 		readonly ITestOutputHelper output;
 
-		public TextAnnotationTests(ITestOutputHelper output)
+		public TextAnnotationTests (ITestOutputHelper output)
 		{
 			this.output = output;
 		}
 
 		[Fact]
-		public void TestTextAnnotation_ScreenUnits()
+		public void TestTextAnnotation_ScreenUnits ()
 		{
 			var gv = GraphViewTests.GetGraph ();
 
@@ -1177,8 +1229,8 @@ namespace Terminal.Gui.Views {
 
 			// user scrolls up one unit of graph space
 			gv.ScrollOffset = new PointF (0, 1f);
-			gv.Redraw (gv.Bounds); 
-			
+			gv.Redraw (gv.Bounds);
+
 			// we expect no change in the location of the annotation (only the axis label changes)
 			// this is because screen units are constant and do not change as the viewport into
 			// graph space scrolls to different areas of the graph
@@ -1301,7 +1353,7 @@ namespace Terminal.Gui.Views {
 		}
 
 		[Theory]
-		[InlineData(null)]
+		[InlineData (null)]
 		[InlineData ("  ")]
 		[InlineData ("\t\t")]
 		public void TestTextAnnotation_EmptyText (string whitespace)
@@ -1316,7 +1368,7 @@ namespace Terminal.Gui.Views {
 			// add a point a bit further along the graph so if the whitespace were rendered
 			// the test would pick it up (AssertDriverContentsAre ignores trailing whitespace on lines)
 			var points = new ScatterSeries ();
-			points.Points.Add(new PointF(7, 2));
+			points.Points.Add (new PointF (7, 2));
 			gv.Series.Add (points);
 
 			gv.Redraw (gv.Bounds);
@@ -1340,7 +1392,7 @@ namespace Terminal.Gui.Views {
 	public class LegendTests {
 		readonly ITestOutputHelper output;
 
-		public LegendTests(ITestOutputHelper output)
+		public LegendTests (ITestOutputHelper output)
 		{
 			this.output = output;
 		}
@@ -1349,7 +1401,7 @@ namespace Terminal.Gui.Views {
 		public void LegendNormalUsage_WithBorder ()
 		{
 			var gv = GraphViewTests.GetGraph ();
-			var legend = new LegendAnnotation(new Rect(2,0,5,3));
+			var legend = new LegendAnnotation (new Rect (2, 0, 5, 3));
 			legend.AddEntry (new GraphCellToRender ('A'), "Ant");
 			legend.AddEntry (new GraphCellToRender ('B'), "Bat");
 
@@ -1404,13 +1456,13 @@ namespace Terminal.Gui.Views {
 	public class PathAnnotationTests {
 		readonly ITestOutputHelper output;
 
-		public PathAnnotationTests( ITestOutputHelper output)
+		public PathAnnotationTests (ITestOutputHelper output)
 		{
 			this.output = output;
 		}
 
 		[Fact]
-		public void PathAnnotation_Box()
+		public void PathAnnotation_Box ()
 		{
 			var gv = GraphViewTests.GetGraph ();
 
@@ -1480,9 +1532,9 @@ namespace Terminal.Gui.Views {
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
 		}
-			
+
 		[Fact]
-		public void XAxisLabels_With_MarginLeft()
+		public void XAxisLabels_With_MarginLeft ()
 		{
 			GraphViewTests.InitFakeDriver ();
 			var gv = new GraphView {
@@ -1499,7 +1551,7 @@ namespace Terminal.Gui.Views {
 			});
 
 			// reserve 3 cells of the left for the margin
-			gv.MarginLeft = 3; 
+			gv.MarginLeft = 3;
 			gv.MarginBottom = 1;
 
 			gv.Redraw (gv.Bounds);
@@ -1515,7 +1567,7 @@ namespace Terminal.Gui.Views {
    0    5
          
           ";
-				GraphViewTests.AssertDriverContentsAre (expected, output);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1599,7 +1651,7 @@ namespace Terminal.Gui.Views {
 0┼┬┬┬┬┬┬┬┬
  0    5";
 
-			GraphViewTests.AssertDriverContentsAre (expected,output);
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
 
 			// Shutdown must be called to safely clean up Application if Init has been called
@@ -1657,11 +1709,11 @@ namespace Terminal.Gui.Views {
 		}
 	}
 
-		public class AxisIncrementToRenderTests {
+	public class AxisIncrementToRenderTests {
 		[Fact]
 		public void AxisIncrementToRenderTests_Constructor ()
 		{
-			var render = new AxisIncrementToRender (Orientation.Horizontal,1,6.6f);
+			var render = new AxisIncrementToRender (Orientation.Horizontal, 1, 6.6f);
 
 			Assert.Equal (Orientation.Horizontal, render.Orientation);
 			Assert.Equal (1, render.ScreenLocation);

+ 485 - 14
UnitTests/MenuTests.cs

@@ -512,8 +512,8 @@ Edit
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 8, 4), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -541,8 +541,8 @@ Edit
 ──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 0), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			menu.CloseAllMenus ();
 			menu.Frame = new Rect (-1, -2, menu.Frame.Width, menu.Frame.Height);
@@ -555,8 +555,8 @@ Edit
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (1, 0), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (1, 0, 7, 3), pos);
 
 			menu.CloseAllMenus ();
 			menu.Frame = new Rect (0, 0, menu.Frame.Width, menu.Frame.Height);
@@ -566,13 +566,13 @@ Edit
 
 			expected = @"
 ┌──────
-│ One  
-│ Two  
+│ One
+│ Two
 └──────
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 7, 4), pos);
 
 			menu.CloseAllMenus ();
 			menu.Frame = new Rect (0, 0, menu.Frame.Width, menu.Frame.Height);
@@ -582,12 +582,483 @@ Edit
 
 			expected = @"
 ┌──────
-│ One  
-│ Two  
+│ One
+│ Two
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 1), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 1, 7, 3), 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.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
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			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
+┌────────┐
+│ One    │
+│ Two   ►│┌─────────────┐
+│ Three  ││ Sub-Menu 1  │
+└────────┘│ Sub-Menu 2  │
+          └─────────────┘
+";
+
+			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
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			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
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 10, 6), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 25, 7), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 10, 6), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), 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.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
+┌────────┐
+│ One    │
+│ Two   ►│
+│ Three  │
+└────────┘
+";
+
+			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)));
+			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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 15, 7), 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.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
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 10, 6), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 15, 7), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 10, 6), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 9, 1), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessHotKey_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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 13, 1), pos);
+
+			Assert.True (menu.ProcessKey (new (Key.N, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (newAction);
+
+			Assert.True (menu.ProcessHotKey (new (Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (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);
+
+			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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 13, 4), 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.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (2, 0, 17, 4), pos);
+
+			Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.C, null)));
+			Application.MainLoop.MainIteration ();
+			Assert.True (copyAction);
 		}
 	}
 }

+ 14 - 14
UnitTests/ViewTests.cs

@@ -1876,8 +1876,8 @@ namespace Terminal.Gui.Core {
 └──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 0), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 8, 4), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -1900,8 +1900,8 @@ namespace Terminal.Gui.Core {
 ──────┘
 ";
 
-			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 0), pos);
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			view.Frame = new Rect (-1, -1, 8, 4);
 			Application.Refresh ();
@@ -1912,33 +1912,33 @@ namespace Terminal.Gui.Core {
 ──────┘
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (6, 0), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (6, 0, 7, 3), pos);
 
 			view.Frame = new Rect (0, 0, 8, 4);
 			((FakeDriver)Application.Driver).SetBufferSize (7, 4);
 
 			expected = @"
 ┌──────
-│      
-│      
+│
+│
 └──────
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 0), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 7, 4), pos);
 
 			view.Frame = new Rect (0, 0, 8, 4);
 			((FakeDriver)Application.Driver).SetBufferSize (7, 3);
 
 			expected = @"
 ┌──────
-│      
-│      
+│
+│
 ";
 
-			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
-			Assert.Equal (new Point (0, 0), pos);
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 7, 3), pos);
 		}
 	}
 }