浏览代码

Adds a popup ContextMenu feature. Implements ContextMenu for TextField. (#1615)

* Implementing ContextMenu feature to being popup by keyboard or mouse.

* Implements ContextMenu into TextField.

* Ensures the context menu's right and bottom frame from being greater than the container.

* Added Host property. Improving scenario and unit tests.

* Only draw the RightTee if it is at the end of the menu.

* Implements cursor visibility on TextField.

* Fixes the sub-menu not showing.

* Avoids draw the menu help and shortcut if there no available space.

* Remove reference for the MenuClosing event.

* UpdateCursor must only run after the ScreenBuffer is initialized to use the cursor visibility.

* Implements Resized event on Toplevel class.

* Prevents writing overlay on the menu.

* Covering more unit tests.

* Changing from Views to Core namespace.

* Implementing MenuClosingEventArgs and MenuAllClosed event.

* Only close the menu if it's open.

* Implementing localization for en-US and pt-PT to the FileDialog.

* Implementing localization for en-US and pt-PT on the TextField context menu.

* Fixes a bug where DeleteSelectedText is updating the Text before operation completion.

* Added a method to get all the supported cultures from the Terminal.Gui.

* Improving context menu and adding more unit tests.
BDisp 3 年之前
父节点
当前提交
a822e1afa9

+ 11 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -1565,14 +1565,24 @@ namespace Terminal.Gui {
 			//	Bottom = (short)Clip.Bottom
 			//};
 
-			UpdateCursor ();
 			WinConsole.WriteToConsole (new Size (Cols, Rows), OutputBuffer, bufferCoords, damageRegion);
+			UpdateCursor ();
 			// System.Diagnostics.Debugger.Log (0, "debug", $"Region={damageRegion.Right - damageRegion.Left},{damageRegion.Bottom - damageRegion.Top}\n");
 			WindowsConsole.SmallRect.MakeEmpty (ref damageRegion);
 		}
 
+		CursorVisibility savedCursorVisibility;
+
 		public override void UpdateCursor ()
 		{
+			if (ccol < 0 || crow < 0 || ccol > Cols || crow > Rows) {
+				GetCursorVisibility (out CursorVisibility cursorVisibility);
+				savedCursorVisibility = cursorVisibility;
+				SetCursorVisibility (CursorVisibility.Invisible);
+				return;
+			}
+
+			SetCursorVisibility (savedCursorVisibility);
 			var position = new WindowsConsole.Coord () {
 				X = (short)ccol,
 				Y = (short)crow

+ 1 - 0
Terminal.Gui/Core/Application.cs

@@ -1227,6 +1227,7 @@ namespace Terminal.Gui {
 				t.SetRelativeLayout (full);
 				t.LayoutSubviews ();
 				t.PositionToplevels ();
+				t.OnResized (full.Size);
 			}
 			Refresh ();
 		}

+ 6 - 1
Terminal.Gui/Core/Command.cs

@@ -241,10 +241,15 @@ namespace Terminal.Gui {
 		DeleteCharLeft,
 
 		/// <summary>
-		/// Selects all objects in the control
+		/// Selects all objects in the control.
 		/// </summary>
 		SelectAll,
 
+		/// <summary>
+		/// Deletes all objects in the control.
+		/// </summary>
+		DeleteAll,
+
 		/// <summary>
 		/// Moves the cursor to the start of line.
 		/// </summary>

+ 200 - 0
Terminal.Gui/Core/ContextMenu.cs

@@ -0,0 +1,200 @@
+using System;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// A context menu window derived from <see cref="MenuBar"/> containing menu items
+	/// which can be opened in any position.
+	/// </summary>
+	public sealed class ContextMenu : IDisposable {
+		private static MenuBar menuBar;
+		private Key key = Key.F10 | Key.ShiftMask;
+		private MouseFlags mouseFlags = MouseFlags.Button3Clicked;
+		private Toplevel container;
+
+		/// <summary>
+		/// Initialize a context menu with empty menu items.
+		/// </summary>
+		public ContextMenu () : this (0, 0, new MenuBarItem ()) { }
+
+		/// <summary>
+		/// Initialize a context menu with menu items from a host <see cref="View"/>.
+		/// </summary>
+		/// <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)
+		{
+			Host = host;
+		}
+
+		/// <summary>
+		/// Initialize a context menu with menu items.
+		/// </summary>
+		/// <param name="x">The left position.</param>
+		/// <param name="y">The top position.</param>
+		/// <param name="menuItems">The menu items.</param>
+		public ContextMenu (int x, int y, MenuBarItem menuItems)
+		{
+			if (IsShow) {
+				Hide ();
+			}
+			MenuItens = menuItems;
+			Position = new Point (x, y);
+		}
+
+		private void MenuBar_MenuAllClosed ()
+		{
+			Dispose ();
+		}
+
+		/// <inheritdoc/>
+		public void Dispose ()
+		{
+			if (IsShow) {
+				menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
+				menuBar.Dispose ();
+				menuBar = null;
+				IsShow = false;
+			}
+			if (container != null) {
+				container.Closing -= Container_Closing;
+				container.Resized -= Container_Resized;
+			}
+		}
+
+		/// <summary>
+		/// Open the <see cref="MenuItens"/> menu items.
+		/// </summary>
+		public void Show ()
+		{
+			if (menuBar != null) {
+				Hide ();
+			}
+			container = Application.Current;
+			container.Closing += Container_Closing;
+			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);
+			}
+			var rect = Menu.MakeFrame (position.X, position.Y, MenuItens.Children);
+			if (rect.Right >= frame.Right) {
+				if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero) {
+					position.X = frame.Right - rect.Width;
+				} else if (ForceMinimumPosToZero) {
+					position.X = 0;
+				}
+			} else if (ForceMinimumPosToZero && position.X < 0) {
+				position.X = 0;
+			}
+			if (rect.Bottom >= frame.Bottom) {
+				if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero) {
+					if (Host == null) {
+						position.Y = frame.Bottom - rect.Height - 1;
+					} else {
+						position.Y = Host.Frame.Y - rect.Height;
+					}
+				} else if (ForceMinimumPosToZero) {
+					position.Y = 0;
+				}
+			} else if (ForceMinimumPosToZero && position.Y < 0) {
+				position.Y = 0;
+			}
+
+			menuBar = new MenuBar (new [] { MenuItens }) {
+				X = position.X,
+				Y = position.Y,
+				Width = 0,
+				Height = 0
+			};
+
+			menuBar.isContextMenuLoading = true;
+			menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
+			IsShow = true;
+			menuBar.OpenMenu ();
+		}
+
+		private void Container_Resized (Size obj)
+		{
+			if (IsShow) {
+				Show ();
+			}
+		}
+
+		private void Container_Closing (ToplevelClosingEventArgs obj)
+		{
+			Hide ();
+		}
+
+		/// <summary>
+		/// Close the <see cref="MenuItens"/> menu items.
+		/// </summary>
+		public void Hide ()
+		{
+			menuBar.CloseAllMenus ();
+			Dispose ();
+		}
+
+		/// <summary>
+		/// Event invoked when the <see cref="ContextMenu.Key"/> is changed.
+		/// </summary>
+		public event Action<Key> KeyChanged;
+
+		/// <summary>
+		/// Event invoked when the <see cref="ContextMenu.MouseFlags"/> is changed.
+		/// </summary>
+		public event Action<MouseFlags> MouseFlagsChanged;
+
+		/// <summary>
+		/// Gets or set the menu position.
+		/// </summary>
+		public Point Position { get; set; }
+
+		/// <summary>
+		/// Gets or sets the menu items for this context menu.
+		/// </summary>
+		public MenuBarItem MenuItens { get; set; }
+
+		/// <summary>
+		/// The <see cref="Gui.Key"/> used to activate the context menu by keyboard.
+		/// </summary>
+		public Key Key {
+			get => key;
+			set {
+				var oldKey = key;
+				key = value;
+				KeyChanged?.Invoke (oldKey);
+			}
+		}
+
+		/// <summary>
+		/// The <see cref="Gui.MouseFlags"/> used to activate the context menu by mouse.
+		/// </summary>
+		public MouseFlags MouseFlags {
+			get => mouseFlags;
+			set {
+				var oldFlags = mouseFlags;
+				mouseFlags = value;
+				MouseFlagsChanged?.Invoke (oldFlags);
+			}
+		}
+
+		/// <summary>
+		/// Gets information whether menu is showing or not.
+		/// </summary>
+		public static bool IsShow { get; private set; }
+
+		/// <summary>
+		/// The host <see cref="View "/> which position will be used,
+		/// otherwise if it's null the container will be used.
+		/// </summary>
+		public View Host { get; set; }
+
+		/// <summary>
+		/// Gets or sets whether forces the minimum position to zero
+		/// if the left or right position are negative.
+		/// </summary>
+		public bool ForceMinimumPosToZero { get; set; } = true;
+	}
+}

+ 10 - 0
Terminal.Gui/Core/Toplevel.cs

@@ -107,6 +107,16 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action<Toplevel> ChildUnloaded;
 
+		/// <summary>
+		/// Invoked when the terminal was resized. The new <see cref="Size"/> of the terminal is provided.
+		/// </summary>
+		public event Action<Size> Resized;
+
+		internal virtual void OnResized (Size size)
+		{
+			Resized?.Invoke (size);
+		}
+
 		internal virtual void OnChildUnloaded (Toplevel top)
 		{
 			ChildUnloaded?.Invoke (top);

+ 14 - 3
Terminal.Gui/Core/View.cs

@@ -1179,13 +1179,15 @@ namespace Terminal.Gui {
 		/// <returns>The move.</returns>
 		/// <param name="col">Col.</param>
 		/// <param name="row">Row.</param>
-		public void Move (int col, int row)
+		/// <param name="clipped">Whether to clip the result of the ViewToScreen method,
+		///  if set to <c>true</c>, the col, row values are clamped to the screen (terminal) dimensions (0..TerminalDim-1).</param>
+		public void Move (int col, int row, bool clipped = true)
 		{
 			if (Driver.Rows == 0) {
 				return;
 			}
 
-			ViewToScreen (col, row, out var rcol, out var rrow);
+			ViewToScreen (col, row, out var rcol, out var rrow, clipped);
 			Driver.Move (rcol, rrow);
 		}
 
@@ -1689,7 +1691,6 @@ namespace Terminal.Gui {
 			foreach (var kvp in KeyBindings.Where (kvp => kvp.Value == command).ToArray ()) {
 				KeyBindings.Remove (kvp.Key);
 			}
-
 		}
 
 		/// <summary>
@@ -1722,6 +1723,16 @@ namespace Terminal.Gui {
 			return CommandImplementations.Keys;
 		}
 
+		/// <summary>
+		/// Gets the key used by a command.
+		/// </summary>
+		/// <param name="command">The command to search.</param>
+		/// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
+		public Key GetKeyFromCommand(Command command)
+		{
+			return KeyBindings.First (x => x.Value == command).Key;
+		}
+
 		/// <inheritdoc/>
 		public override bool ProcessHotKey (KeyEvent keyEvent)
 		{

+ 2 - 2
Terminal.Gui/Core/Window.cs

@@ -97,9 +97,9 @@ namespace Terminal.Gui {
 				base.OnCanFocusChanged ();
 			}
 
-			public override bool MouseEvent (MouseEvent mouseEvent)
+			public override bool OnMouseEvent (MouseEvent mouseEvent)
 			{
-				return instance.MouseEvent (mouseEvent);
+				return instance.OnMouseEvent (mouseEvent);
 			}
 		}
 

+ 211 - 88
Terminal.Gui/Views/Menu.cs

@@ -381,7 +381,7 @@ namespace Terminal.Gui {
 		internal int current;
 		internal View previousSubFocused;
 
-		static Rect MakeFrame (int x, int y, MenuItem [] items)
+		internal static Rect MakeFrame (int x, int y, MenuItem [] items)
 		{
 			if (items == null || items.Length == 0) {
 				return new Rect ();
@@ -446,30 +446,37 @@ namespace Terminal.Gui {
 		public override void Redraw (Rect bounds)
 		{
 			Driver.SetAttribute (GetNormalColor ());
-			DrawFrame (bounds, padding: 0, fill: true);
+			DrawFrame (Bounds, padding: 0, fill: true);
 
-			for (int i = 0; i < barItems.Children.Length; i++) {
+			for (int i = Bounds.Y; i < barItems.Children.Length; i++) {
+				if (i < 0)
+					continue;
 				var item = barItems.Children [i];
 				Driver.SetAttribute (item == null ? GetNormalColor ()
 					: i == current ? ColorScheme.Focus : GetNormalColor ());
 				if (item == null) {
 					Move (0, i + 1);
 					Driver.AddRune (Driver.LeftTee);
-				} else
+				} else if (Frame.X + 1 < Driver.Cols)
 					Move (1, i + 1);
 
 				Driver.SetAttribute (DetermineColorSchemeFor (item, i));
-				for (int p = 0; p < Frame.Width - 2; p++)
+				for (int p = Bounds.X; p < Frame.Width - 2; p++) {
+					if (p < 0)
+						continue;
 					if (item == null)
 						Driver.AddRune (Driver.HLine);
 					else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null)
 						Driver.AddRune (Driver.RightArrow);
 					else
 						Driver.AddRune (' ');
+				}
 
 				if (item == null) {
-					Move (Frame.Width - 1, i + 1);
-					Driver.AddRune (Driver.RightTee);
+					if (SuperView?.Frame.Right - Frame.X > Frame.Width - 1) {
+						Move (Frame.Width - 1, i + 1);
+						Driver.AddRune (Driver.RightTee);
+					}
 					continue;
 				}
 
@@ -491,24 +498,31 @@ namespace Terminal.Gui {
 					textToDraw = item.Title;
 				}
 
-				Move (2, i + 1);
-				if (!item.IsEnabled ())
-					DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled);
-				else
-					DrawHotString (textToDraw,
-					       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;
-				Move (Frame.Width - l - 2, 1 + i);
-				Driver.AddStr (item.Help);
-
-				// The shortcut tag string
-				if (!item.ShortcutTag.IsEmpty) {
-					l = item.ShortcutTag.RuneCount;
-					Move (Frame.Width - l - 2, 1 + i);
-					Driver.AddStr (item.ShortcutTag);
+				ViewToScreen (2, i + 1, out int vtsCol, out _, false);
+				if (vtsCol < Driver.Cols) {
+					Move (2, i + 1);
+					if (!item.IsEnabled ())
+						DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled);
+					else
+						DrawHotString (textToDraw,
+						       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;
+					var col = Frame.Width - l - 2;
+					ViewToScreen (col, i + 1, out vtsCol, out _, false);
+					if (vtsCol < Driver.Cols) {
+						Move (col, 1 + i);
+						Driver.AddStr (item.Help);
+
+						// The shortcut tag string
+						if (!item.ShortcutTag.IsEmpty) {
+							l = item.ShortcutTag.RuneCount;
+							Move (Frame.Width - l - 2, 1 + i);
+							Driver.AddStr (item.ShortcutTag);
+						}
+					}
 				}
 			}
 			PositionCursor ();
@@ -579,8 +593,9 @@ namespace Terminal.Gui {
 				foreach (var item in barItems.Children) {
 					if (item == null) continue;
 					if (item.IsEnabled () && item.HotKey == x) {
-						host.CloseMenu ();
-						Run (item.Action);
+						if (host.CloseMenu ()) {
+							Run (item.Action);
+						}
 						return true;
 					}
 				}
@@ -627,7 +642,8 @@ namespace Terminal.Gui {
 				}
 				if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
 					!disabled && host.IsMenuOpen) {
-					CheckSubMenu ();
+					if (!CheckSubMenu ())
+						return false;
 					break;
 				}
 				if (!host.IsMenuOpen) {
@@ -662,8 +678,8 @@ 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 ()) {
+						return false;
 					}
 					break;
 				}
@@ -675,7 +691,8 @@ namespace Terminal.Gui {
 				}
 				if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null &&
 					!disabled && host.IsMenuOpen) {
-					CheckSubMenu ();
+					if (!CheckSubMenu ())
+						return false;
 					break;
 				}
 			} while (barItems.Children [current] == null || disabled);
@@ -711,20 +728,22 @@ namespace Terminal.Gui {
 					return true;
 				}
 				var item = barItems.Children [me.Y - 1];
+				if (item == null) return true;
 				if (item == null || !item.IsEnabled ()) disabled = true;
 				if (item != null && !disabled)
 					current = me.Y - 1;
-				CheckSubMenu ();
+				if (!CheckSubMenu ())
+					return true;
 				host.OnMenuOpened ();
 				return true;
 			}
 			return false;
 		}
 
-		internal void CheckSubMenu ()
+		internal bool CheckSubMenu ()
 		{
 			if (current == -1 || barItems.Children [current] == null) {
-				return;
+				return true;
 			}
 			var subMenu = barItems.SubMenu (barItems.Children [current]);
 			if (subMenu != null) {
@@ -732,15 +751,17 @@ namespace Terminal.Gui {
 				if (host.openSubMenu != null) {
 					pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu);
 				}
-				if (pos == -1 && this != host.openCurrentMenu && subMenu.Children != host.openCurrentMenu.barItems.Children) {
-					host.CloseMenu (false, true);
+				if (pos == -1 && this != host.openCurrentMenu && subMenu.Children != host.openCurrentMenu.barItems.Children
+					&& !host.CloseMenu (false, true)) {
+					return false;
 				}
 				host.Activate (host.selected, pos, subMenu);
-			} else if (host.openSubMenu?.Last ().barItems.IsSubMenuOf (barItems.Children [current]) == false) {
-				host.CloseMenu (false, true);
+			} else if (host.openSubMenu?.Count == 0 || host.openSubMenu?.Last ().barItems.IsSubMenuOf (barItems.Children [current]) == false) {
+				return host.CloseMenu (false, true);
 			} else {
 				SetNeedsDisplay ();
 			}
+			return true;
 		}
 
 		int GetSubMenuIndex (MenuBarItem subMenu)
@@ -889,7 +910,7 @@ namespace Terminal.Gui {
 					IsMenuOpen = true;
 					selected = 0;
 					CanFocus = true;
-					lastFocused = SuperView.MostFocused;
+					lastFocused = SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused;
 					SetFocus ();
 					SetNeedsDisplay ();
 					Application.GrabMouse (this);
@@ -998,9 +1019,14 @@ namespace Terminal.Gui {
 		public event Action<MenuItem> MenuOpened;
 
 		/// <summary>
-		/// Raised when a menu is closing.
+		/// Raised when a menu is closing passing <see cref="MenuClosingEventArgs"/>.
 		/// </summary>
-		public event Action MenuClosing;
+		public event Action<MenuClosingEventArgs> MenuClosing;
+
+		/// <summary>
+		/// Raised when all the menu are closed.
+		/// </summary>
+		public event Action MenuAllClosed;
 
 		internal Menu openMenu;
 		Menu ocm;
@@ -1009,7 +1035,9 @@ namespace Terminal.Gui {
 			set {
 				if (ocm != value) {
 					ocm = value;
-					OnMenuOpened ();
+					if (ocm.current > -1) {
+						OnMenuOpened ();
+					}
 				}
 			}
 		}
@@ -1050,9 +1078,22 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Virtual method that will invoke the <see cref="MenuClosing"/>
 		/// </summary>
-		public virtual void OnMenuClosing ()
+		/// <param name="currentMenu">The current menu to be closed.</param>
+		/// <param name="reopen">Whether the current menu will be reopen.</param>
+		/// <param name="isSubMenu">Whether is a sub-menu or not.</param>
+		public virtual MenuClosingEventArgs OnMenuClosing (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
 		{
-			MenuClosing?.Invoke ();
+			var ev = new MenuClosingEventArgs (currentMenu, reopen, isSubMenu);
+			MenuClosing?.Invoke (ev);
+			return ev;
+		}
+
+		/// <summary>
+		/// Virtual method that will invoke the <see cref="MenuAllClosed"/>
+		/// </summary>
+		public virtual void OnMenuAllClosed ()
+		{
+			MenuAllClosed?.Invoke ();
 		}
 
 		View lastFocused;
@@ -1067,6 +1108,7 @@ namespace Terminal.Gui {
 			isMenuOpening = true;
 			var newMenu = OnMenuOpening (Menus [index]);
 			if (newMenu.Cancel) {
+				isMenuOpening = false;
 				return;
 			}
 			if (newMenu.NewMenuBarItem != null) {
@@ -1075,21 +1117,29 @@ namespace Terminal.Gui {
 			int pos = 0;
 			switch (subMenu) {
 			case null:
-				lastFocused = lastFocused ?? SuperView?.MostFocused;
-				if (openSubMenu != null)
-					CloseMenu (false, true);
+				lastFocused = lastFocused ?? (SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused);
+				if (openSubMenu != null && !CloseMenu (false, true))
+					return;
 				if (openMenu != null) {
-					SuperView.Remove (openMenu);
+					if (SuperView == null) {
+						Application.Current.Remove (openMenu);
+					} else {
+						SuperView.Remove (openMenu);
+					}
 					openMenu.Dispose ();
 				}
 
 				for (int i = 0; i < index; i++)
 					pos += Menus [i].Title.RuneCount + (Menus [i].Help.RuneCount > 0 ? Menus [i].Help.RuneCount + 2 : 0) + 2;
-				openMenu = new Menu (this, pos, 1, Menus [index]);
+				openMenu = new Menu (this, Frame.X + pos, Frame.Y + 1, Menus [index]);
 				openCurrentMenu = openMenu;
 				openCurrentMenu.previousSubFocused = openMenu;
 
-				SuperView.Add (openMenu);
+				if (SuperView == null) {
+					Application.Current.Add (openMenu);
+				} else {
+					SuperView.Add (openMenu);
+				}
 				openMenu.SetFocus ();
 				break;
 			default:
@@ -1102,7 +1152,11 @@ namespace Terminal.Gui {
 					openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
 					openCurrentMenu.previousSubFocused = last.previousSubFocused;
 					openSubMenu.Add (openCurrentMenu);
-					SuperView.Add (openCurrentMenu);
+					if (SuperView == null) {
+						Application.Current.Add (openCurrentMenu);
+					} else {
+						SuperView.Add (openCurrentMenu);
+					}
 				}
 				selectedSub = openSubMenu.Count - 1;
 				if (selectedSub > -1 && SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
@@ -1124,12 +1178,13 @@ namespace Terminal.Gui {
 			selected = 0;
 			SetNeedsDisplay ();
 
-			previousFocused = SuperView.Focused;
+			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 ()) {
+				return;
 			}
-			openCurrentMenu.CheckSubMenu ();
+			if (!openCurrentMenu.CheckSubMenu ())
+				return;
 			Application.GrabMouse (this);
 		}
 
@@ -1140,13 +1195,13 @@ namespace Terminal.Gui {
 			selected = idx;
 			selectedSub = sIdx;
 			if (openMenu == null)
-				previousFocused = SuperView.Focused;
+				previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused;
 
 			OpenMenu (idx, sIdx, subMenu);
-			if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) {
-				if (subMenu == null) {
-					CloseMenu ();
-				}
+			if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)
+				&& subMenu == null && !CloseMenu ()) {
+
+				return;
 			}
 			SetNeedsDisplay ();
 		}
@@ -1196,24 +1251,35 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Closes the current Menu programatically, if open.
+		/// Closes the current Menu programatically, if open and not canceled.
 		/// </summary>
-		public void CloseMenu ()
+		public bool CloseMenu ()
 		{
-			CloseMenu (false, false);
+			return CloseMenu (false, false);
 		}
 
 		bool reopen;
 
-		internal void CloseMenu (bool reopen = false, bool isSubMenu = false)
+		internal bool CloseMenu (bool reopen = false, bool isSubMenu = false)
 		{
 			isMenuClosing = true;
 			this.reopen = reopen;
-			OnMenuClosing ();
+			var args = OnMenuClosing (
+				isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems, reopen, isSubMenu);
+			if (args.Cancel) {
+				isMenuClosing = false;
+				if (args.CurrentMenu.Parent != null)
+					openMenu.current = ((MenuBarItem)args.CurrentMenu.Parent).Children.IndexOf (args.CurrentMenu);
+				return false;
+			}
 			switch (isSubMenu) {
 			case false:
 				if (openMenu != null) {
-					SuperView?.Remove (openMenu);
+					if (SuperView == null) {
+						Application.Current.Remove (openMenu);
+					} else {
+						SuperView?.Remove (openMenu);
+					}
 				}
 				SetNeedsDisplay ();
 				if (previousFocused != null && previousFocused is Menu && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ())
@@ -1248,6 +1314,7 @@ namespace Terminal.Gui {
 			}
 			this.reopen = false;
 			isMenuClosing = false;
+			return true;
 		}
 
 		void RemoveSubMenu (int index)
@@ -1265,7 +1332,11 @@ namespace Terminal.Gui {
 				openCurrentMenu.SetFocus ();
 				if (openSubMenu != null) {
 					menu = openSubMenu [i];
-					SuperView.Remove (menu);
+					if (SuperView == null) {
+						Application.Current.Remove (menu);
+					} else {
+						SuperView.Remove (menu);
+					}
 					openSubMenu.Remove (menu);
 					menu.Dispose ();
 				}
@@ -1301,7 +1372,11 @@ namespace Terminal.Gui {
 		{
 			if (openSubMenu != null) {
 				foreach (var item in openSubMenu) {
-					SuperView.Remove (item);
+					if (SuperView == null) {
+						Application.Current.Remove (item);
+					} else {
+						SuperView.Remove (item);
+					}
 					item.Dispose ();
 				}
 			}
@@ -1310,15 +1385,17 @@ namespace Terminal.Gui {
 		internal void CloseAllMenus ()
 		{
 			if (!isMenuOpening && !isMenuClosing) {
-				if (openSubMenu != null)
-					CloseMenu (false, true);
-				CloseMenu ();
+				if (openSubMenu != null && !CloseMenu (false, true))
+					return;
+				if (!CloseMenu ())
+					return;
 				if (LastFocused != null && LastFocused != this)
 					selected = -1;
 			}
 			IsMenuOpen = false;
 			openedByHotKey = false;
 			openedByAltKey = false;
+			OnMenuAllClosed ();
 		}
 
 		View FindDeepestMenu (View view, ref int count)
@@ -1342,15 +1419,14 @@ namespace Terminal.Gui {
 				else
 					selected--;
 
-				if (selected > -1)
-					CloseMenu (true, false);
+				if (selected > -1 && !CloseMenu (true, false))
+					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 ();
 					}
-					break;
 				}
 				break;
 			case true:
@@ -1376,20 +1452,21 @@ namespace Terminal.Gui {
 				else
 					selected++;
 
-				if (selected > -1)
-					CloseMenu (true);
+				if (selected > -1 && !CloseMenu (true))
+					return;
 				OpenMenu (selected);
 				SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current);
 				break;
 			case true:
 				if (UseKeysUpDownAsKeysLeftRight) {
-					CloseMenu (false, true);
-					NextMenu ();
+					if (CloseMenu (false, true)) {
+						NextMenu ();
+					}
 				} 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);
+						if (openSubMenu != null && !CloseMenu (false, true))
+							return;
 						NextMenu ();
 					} else if (subMenu != null ||
 						!openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu)
@@ -1469,10 +1546,11 @@ 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 ()) {
+					return;
 				}
-				openCurrentMenu.CheckSubMenu ();
+				if (!openCurrentMenu.CheckSubMenu ())
+					return;
 			}
 			SetNeedsDisplay ();
 		}
@@ -1533,7 +1611,8 @@ namespace Terminal.Gui {
 
 		void CloseMenuBar ()
 		{
-			CloseMenu ();
+			if (!CloseMenu ())
+				return;
 			if (openedByAltKey) {
 				openedByAltKey = false;
 				LastFocused?.SetFocus ();
@@ -1593,7 +1672,9 @@ namespace Terminal.Gui {
 						} else if (selected != i && selected > -1 && (me.Flags == MouseFlags.ReportMousePosition ||
 							me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition)) {
 							if (IsMenuOpen) {
-								CloseMenu (true, false);
+								if (!CloseMenu (true, false)) {
+									return true;
+								}
 								Activate (i);
 							}
 						} else {
@@ -1609,6 +1690,7 @@ namespace Terminal.Gui {
 		}
 
 		internal bool handled;
+		internal bool isContextMenuLoading;
 
 		internal bool HandleGrabView (MouseEvent me, View current)
 		{
@@ -1641,14 +1723,17 @@ namespace Terminal.Gui {
 						v.MouseEvent (nme);
 						return false;
 					}
-				} else if (!(me.View is MenuBar || me.View is Menu) && (me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
-					me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked)) {
+				} else if (!isContextMenuLoading && !(me.View is MenuBar || me.View is Menu)
+					&& me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) {
+
 					Application.UngrabMouse ();
-					CloseAllMenus ();
+					if (IsMenuOpen)
+						CloseAllMenus ();
 					handled = false;
 					return false;
 				} else {
 					handled = false;
+					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))) {
@@ -1742,4 +1827,42 @@ namespace Terminal.Gui {
 			CurrentMenu = currentMenu;
 		}
 	}
+
+	/// <summary>
+	/// An <see cref="EventArgs"/> which allows passing a cancelable menu closing event.
+	/// </summary>
+	public class MenuClosingEventArgs : EventArgs {
+		/// <summary>
+		/// The current <see cref="MenuBarItem"/> parent.
+		/// </summary>
+		public MenuBarItem CurrentMenu { get; }
+
+		/// <summary>
+		/// Indicates whether the current menu will be reopen.
+		/// </summary>
+		public bool Reopen { get; }
+
+		/// <summary>
+		/// Indicates whether the current menu is a sub-menu.
+		/// </summary>
+		public bool IsSubMenu { get; }
+
+		/// <summary>
+		/// Flag that allows you to cancel the opening of the menu.
+		/// </summary>
+		public bool Cancel { get; set; }
+
+		/// <summary>
+		/// Initializes a new instance of <see cref="MenuClosingEventArgs"/>
+		/// </summary>
+		/// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
+		/// <param name="reopen">Whether the current menu will be reopen.</param>
+		/// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
+		public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
+		{
+			CurrentMenu = currentMenu;
+			Reopen = reopen;
+			IsSubMenu = isSubMenu;
+		}
+	}
 }

+ 123 - 16
Terminal.Gui/Views/TextField.cs

@@ -9,6 +9,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using NStack;
+using Terminal.Gui.Resources;
 using Rune = System.Rune;
 
 namespace Terminal.Gui {
@@ -96,6 +97,7 @@ namespace Terminal.Gui {
 			CanFocus = true;
 			Used = true;
 			WantMousePositionReports = true;
+			savedCursorVisibility = desiredCursorVisibility;
 
 			historyText.ChangeText += HistoryText_ChangeText;
 
@@ -128,6 +130,9 @@ namespace Terminal.Gui {
 			AddCommand (Command.Copy, () => { Copy (); return true; });
 			AddCommand (Command.Cut, () => { Cut (); return true; });
 			AddCommand (Command.Paste, () => { Paste (); return true; });
+			AddCommand (Command.SelectAll, () => { SelectAll (); return true; });
+			AddCommand (Command.DeleteAll, () => { DeleteAll (); return true; });
+			AddCommand (Command.Accept, () => { ShowContextMenu (); return true; });
 
 			// Default keybindings for this view
 			AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
@@ -194,6 +199,28 @@ namespace Terminal.Gui {
 			AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy);
 			AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut);
 			AddKeyBinding (Key.V | Key.CtrlMask, Command.Paste);
+			AddKeyBinding (Key.T | Key.CtrlMask, Command.SelectAll);
+			AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll);
+
+			ContextMenu = new ContextMenu (this,
+				new MenuBarItem (new MenuItem [] {
+								new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, GetKeyFromCommand (Command.SelectAll)),
+								new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, GetKeyFromCommand (Command.DeleteAll)),
+								new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, GetKeyFromCommand (Command.Copy)),
+								new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, GetKeyFromCommand (Command.Cut)),
+								new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, GetKeyFromCommand (Command.Paste)),
+								new MenuItem (Strings.ctxUndo, "", () => UndoChanges (), null, null, GetKeyFromCommand (Command.Undo)),
+								new MenuItem (Strings.ctxRedo, "", () => RedoChanges (), null, null, GetKeyFromCommand (Command.Redo)),
+				})
+			);
+			ContextMenu.KeyChanged += ContextMenu_KeyChanged;
+
+			AddKeyBinding (ContextMenu.Key, Command.Accept);
+		}
+
+		private void ContextMenu_KeyChanged (Key obj)
+		{
+			ReplaceKeyBinding (obj, ContextMenu.Key);
 		}
 
 		private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj)
@@ -320,6 +347,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		public bool HasHistoryChanges => historyText.HasHistoryChanges;
 
+		/// <summary>
+		/// Get the <see cref="ContextMenu"/> for this view.
+		/// </summary>
+		public ContextMenu ContextMenu { get; private set; }
+
 		/// <summary>
 		///   Sets the cursor position.
 		/// </summary>
@@ -332,7 +364,35 @@ namespace Terminal.Gui {
 				var cols = Rune.ColumnWidth (text [idx]);
 				TextModel.SetCol (ref col, Frame.Width - 1, cols);
 			}
-			Move (col, 0);
+			var pos = point - first + Math.Min (Frame.X, 0);
+			var offB = OffSetBackground ();
+			if (pos > -1 && col >= pos && pos < Frame.Width + offB) {
+				RestoreCursorVisibility ();
+				Move (col, 0);
+			} else {
+				HideCursorVisibility ();
+				if (pos < 0) {
+					Move (pos, 0, false);
+				} else {
+					Move (pos - offB, 0, false);
+				}
+			}
+		}
+
+		CursorVisibility savedCursorVisibility;
+
+		void HideCursorVisibility ()
+		{
+			if (desiredCursorVisibility != CursorVisibility.Invisible) {
+				DesiredCursorVisibility = CursorVisibility.Invisible;
+			}
+		}
+
+		void RestoreCursorVisibility ()
+		{
+			if (desiredCursorVisibility != savedCursorVisibility) {
+				DesiredCursorVisibility = savedCursorVisibility;
+			}
 		}
 
 		///<inheritdoc/>
@@ -500,8 +560,9 @@ namespace Terminal.Gui {
 		{
 			historyText.Add (new List<List<Rune>> () { text }, new Point (point, 0));
 
+			List<Rune> newText = text;
 			if (length > 0) {
-				DeleteSelectedText ();
+				newText = DeleteSelectedText ();
 				oldCursorPos = point;
 			}
 			if (!useOldCursorPos) {
@@ -510,16 +571,16 @@ namespace Terminal.Gui {
 			var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key));
 			if (Used) {
 				point++;
-				if (point == text.Count + 1) {
-					SetText (text.Concat (kbstr).ToList ());
+				if (point == newText.Count + 1) {
+					SetText (newText.Concat (kbstr).ToList ());
 				} else {
-					if (oldCursorPos > text.Count) {
-						oldCursorPos = text.Count;
+					if (oldCursorPos > newText.Count) {
+						oldCursorPos = newText.Count;
 					}
-					SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count))));
+					SetText (newText.GetRange (0, oldCursorPos).Concat (kbstr).Concat (newText.GetRange (oldCursorPos, Math.Min (newText.Count - oldCursorPos, newText.Count))));
 				}
 			} else {
-				SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0))));
+				SetText (newText.GetRange (0, oldCursorPos).Concat (kbstr).Concat (newText.GetRange (Math.Min (oldCursorPos + 1, newText.Count), Math.Max (newText.Count - oldCursorPos - 1, 0))));
 				point++;
 			}
 			Adjust ();
@@ -747,7 +808,9 @@ namespace Terminal.Gui {
 				}
 				Adjust ();
 			} else {
-				DeleteSelectedText ();
+				var newText = DeleteSelectedText ();
+				Text = ustring.Make (newText);
+				Adjust ();
 			}
 		}
 
@@ -768,7 +831,9 @@ namespace Terminal.Gui {
 				SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1))));
 				Adjust ();
 			} else {
-				DeleteSelectedText ();
+				var newText = DeleteSelectedText ();
+				Text = ustring.Make (newText);
+				Adjust ();
 			}
 		}
 
@@ -854,6 +919,40 @@ namespace Terminal.Gui {
 			return -1;
 		}
 
+		void ShowContextMenu ()
+		{
+			ContextMenu.Show ();
+		}
+
+		/// <summary>
+		/// Selects all text.
+		/// </summary>
+		public void SelectAll ()
+		{
+			if (text.Count == 0) {
+				return;
+			}
+
+			selectedStart = 0;
+			MoveEndExtend ();
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Deletes all text.
+		/// </summary>
+		public void DeleteAll ()
+		{
+			if (text.Count == 0) {
+				return;
+			}
+
+			selectedStart = 0;
+			MoveEndExtend ();
+			DeleteCharLeft ();
+			SetNeedsDisplay ();
+		}
+
 		/// <summary>
 		/// Start position of the selected text.
 		/// </summary>
@@ -893,7 +992,7 @@ namespace Terminal.Gui {
 		{
 			if (!ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) &&
 				!ev.Flags.HasFlag (MouseFlags.Button1Released) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
-				!ev.Flags.HasFlag (MouseFlags.Button1TripleClicked)) {
+				!ev.Flags.HasFlag (MouseFlags.Button1TripleClicked) && !ev.Flags.HasFlag (ContextMenu.MouseFlags)) {
 				return false;
 			}
 
@@ -901,6 +1000,10 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			if (!HasFocus && ev.Flags != MouseFlags.ReportMousePosition) {
+				SetFocus ();
+			}
+
 			// Give autocomplete first opportunity to respond to mouse clicks
 			if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) {
 				return true;
@@ -949,6 +1052,8 @@ namespace Terminal.Gui {
 				PositionCursor (0);
 				ClearAllSelection ();
 				PrepareSelection (0, text.Count);
+			} else if (ev.Flags == ContextMenu.MouseFlags) {
+				ShowContextMenu ();
 			}
 
 			SetNeedsDisplay ();
@@ -1059,10 +1164,12 @@ namespace Terminal.Gui {
 				return;
 
 			Clipboard.Contents = SelectedText;
-			DeleteSelectedText ();
+			var newText = DeleteSelectedText ();
+			Text = ustring.Make (newText);
+			Adjust ();
 		}
 
-		void DeleteSelectedText ()
+		List<Rune> DeleteSelectedText ()
 		{
 			ustring actualText = Text;
 			SetSelectedStartSelectedLength ();
@@ -1070,11 +1177,11 @@ namespace Terminal.Gui {
 			(var _, var len) = TextModel.DisplaySize (text, 0, selStart, false);
 			(var _, var len2) = TextModel.DisplaySize (text, selStart, selStart + length, false);
 			(var _, var len3) = TextModel.DisplaySize (text, selStart + length, actualText.RuneCount, false);
-			Text = actualText [0, len] +
+			var newText = actualText [0, len] +
 				actualText [len + len2, len + len2 + len3];
 			ClearAllSelection ();
-			point = selStart >= Text.RuneCount ? Text.RuneCount : selStart;
-			Adjust ();
+			point = selStart >= newText.RuneCount ? newText.RuneCount : selStart;
+			return newText.ToRuneList ();
 		}
 
 		/// <summary>

+ 146 - 0
UICatalog/Scenarios/ContextMenus.cs

@@ -0,0 +1,146 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "ContextMenus", Description: "Context Menu Sample")]
+	[ScenarioCategory ("Controls")]
+	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;
+
+		public override void Setup ()
+		{
+			var text = "Context Menu";
+			var width = 20;
+
+			tfTopLeft = new TextField (text) {
+				Width = width
+			};
+			Win.Add (tfTopLeft);
+
+			tfTopRight = new TextField (text) {
+				X = Pos.AnchorEnd (width),
+				Width = width
+			};
+			Win.Add (tfTopRight);
+
+			tfMiddle = new TextField (text) {
+				X = Pos.Center (),
+				Y = Pos.Center (),
+				Width = width
+			};
+			Win.Add (tfMiddle);
+
+			tfBottomLeft = new TextField (text) {
+				Y = Pos.AnchorEnd (1),
+				Width = width
+			};
+			Win.Add (tfBottomLeft);
+
+			tfBottomRight = new TextField (text) {
+				X = Pos.AnchorEnd (width),
+				Y = Pos.AnchorEnd (1),
+				Width = width
+			};
+			Win.Add (tfBottomRight);
+
+			Point mousePos = default;
+
+			Win.KeyPress += (e) => {
+				if (e.KeyEvent.Key == (Key.Space | Key.CtrlMask) && !ContextMenu.IsShow) {
+					ShowContextMenu (mousePos.X, mousePos.Y);
+					e.Handled = true;
+				}
+			};
+
+			Win.MouseClick += (e) => {
+				if (e.MouseEvent.Flags == contextMenu.MouseFlags) {
+					ShowContextMenu (e.MouseEvent.X, e.MouseEvent.Y);
+					e.Handled = true;
+				}
+				mousePos = new Point (e.MouseEvent.X, e.MouseEvent.Y);
+			};
+
+			Win.WantMousePositionReports = true;
+
+			Top.Closed += (_) => Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US");
+		}
+
+		private void ShowContextMenu (int x, int y)
+		{
+			contextMenu = new ContextMenu (x, y,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("_Configuration", "Show configuration", () => MessageBox.Query (50, 5, "Info", "This would open settings dialog", "Ok")),
+					new MenuBarItem ("More options", new MenuItem [] {
+						new MenuItem ("_Setup", "Change settings", () => MessageBox.Query (50, 5, "Info", "This would open setup dialog", "Ok")),
+						new MenuItem ("_Maintenance", "Maintenance mode", () => MessageBox.Query (50, 5, "Info", "This would open maintenance dialog", "Ok")),
+					}),
+					new MenuBarItem ("_Languages", GetSupportedCultures ()),
+					miForceMinimumPosToZero = new MenuItem ("ForceMinimumPosToZero", "", () => {
+						miForceMinimumPosToZero.Checked = forceMinimumPosToZero = !forceMinimumPosToZero;
+						tfTopLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+						tfTopRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+						tfMiddle.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+						tfBottomLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+						tfBottomRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+					}) { CheckType = MenuItemCheckStyle.Checked, Checked = forceMinimumPosToZero },
+					null,
+					new MenuItem ("_Quit", "", () => Application.RequestStop ())
+				})
+			) { ForceMinimumPosToZero = forceMinimumPosToZero };
+
+			tfTopLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+			tfTopRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+			tfMiddle.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+			tfBottomLeft.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+			tfBottomRight.ContextMenu.ForceMinimumPosToZero = forceMinimumPosToZero;
+
+			contextMenu.Show ();
+		}
+
+		private MenuItem [] GetSupportedCultures ()
+		{
+			List<MenuItem> supportedCultures = new List<MenuItem> ();
+			var index = -1;
+
+			foreach (var c in cultureInfos) {
+				var culture = new MenuItem {
+					CheckType = MenuItemCheckStyle.Checked
+				};
+				if (index == -1) {
+					culture.Title = "_English";
+					culture.Help = "en-US";
+					culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == "en-US";
+					CreateAction (supportedCultures, culture);
+					supportedCultures.Add (culture);
+					index++;
+					culture = new MenuItem {
+						CheckType = MenuItemCheckStyle.Checked
+					};
+				}
+				culture.Title = $"_{c.Parent.EnglishName}";
+				culture.Help = c.Name;
+				culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == c.Name;
+				CreateAction (supportedCultures, culture);
+				supportedCultures.Add (culture);
+			}
+			return supportedCultures.ToArray ();
+
+			void CreateAction (List<MenuItem> supportedCultures, MenuItem culture)
+			{
+				culture.Action += () => {
+					Thread.CurrentThread.CurrentUICulture = new CultureInfo (culture.Help.ToString ());
+					culture.Checked = true;
+					foreach (var item in supportedCultures) {
+						item.Checked = item.Help.ToString () == Thread.CurrentThread.CurrentUICulture.Name;
+					}
+				};
+			}
+		}
+	}
+}

+ 467 - 0
UnitTests/ContextMenuTests.cs

@@ -0,0 +1,467 @@
+using Xunit;
+using Xunit.Abstractions;
+using GraphViewTests = Terminal.Gui.Views.GraphViewTests;
+
+namespace Terminal.Gui.Core {
+	public class ContextMenuTests {
+		readonly ITestOutputHelper output;
+
+		public ContextMenuTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ContextMenu_Constructors ()
+		{
+			var cm = new ContextMenu ();
+			Assert.Equal (new Point (0, 0), cm.Position);
+			Assert.Empty (cm.MenuItens.Children);
+			Assert.Null (cm.Host);
+			cm.Position = new Point (20, 10);
+			cm.MenuItens = new MenuBarItem (new MenuItem [] {
+				new MenuItem ("First", "", null)
+			});
+			Assert.Equal (new Point (20, 10), cm.Position);
+			Assert.Single (cm.MenuItens.Children);
+
+			cm = new ContextMenu (5, 10,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+			Assert.Equal (new Point (5, 10), cm.Position);
+			Assert.Equal (2, cm.MenuItens.Children.Length);
+			Assert.Null (cm.Host);
+
+			cm = new ContextMenu (new View () { X = 5, Y = 10 },
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+			Assert.Equal (new Point (6, 10), cm.Position);
+			Assert.Equal (2, cm.MenuItens.Children.Length);
+			Assert.NotNull (cm.Host);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Show_Hide_IsShow ()
+		{
+			var cm = new ContextMenu (10, 5,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			cm.Show ();
+			Assert.True (ContextMenu.IsShow);
+
+			Application.Begin (Application.Top);
+
+			var expected = @"
+          ┌──────┐
+          │ One  │
+          │ Two  │
+          └──────┘
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			cm.Hide ();
+			Assert.False (ContextMenu.IsShow);
+
+			Application.Refresh ();
+
+			expected = "";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Position_Changing ()
+		{
+			var cm = new ContextMenu (10, 5,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			cm.Show ();
+			Application.Begin (Application.Top);
+
+			var expected = @"
+          ┌──────┐
+          │ One  │
+          │ Two  │
+          └──────┘
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			cm.Position = new Point (5, 10);
+
+			cm.Show ();
+			Application.Refresh ();
+
+			expected = @"
+     ┌──────┐
+     │ One  │
+     │ Two  │
+     └──────┘
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void MenuItens_Changing ()
+		{
+			var cm = new ContextMenu (10, 5,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			cm.Show ();
+			Application.Begin (Application.Top);
+
+			var expected = @"
+          ┌──────┐
+          │ One  │
+          │ Two  │
+          └──────┘
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			cm.MenuItens = new MenuBarItem (new MenuItem [] {
+				new MenuItem ("First", "", null),
+				new MenuItem ("Second", "", null),
+				new MenuItem ("Third", "", null)
+			});
+
+
+			cm.Show ();
+			Application.Refresh ();
+
+			expected = @"
+          ┌─────────┐
+          │ First   │
+          │ Second  │
+          │ Third   │
+          └─────────┘
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Key_Changing ()
+		{
+			var lbl = new Label ("Original");
+
+			var cm = new ContextMenu ();
+
+			lbl.KeyPress += (e) => {
+				if (e.KeyEvent.Key == cm.Key) {
+					lbl.Text = "Replaced";
+					e.Handled = true;
+				}
+			};
+
+			var top = Application.Top;
+			top.Add (lbl);
+			Application.Begin (top);
+
+			Assert.True (lbl.ProcessKey (new KeyEvent (cm.Key, new KeyModifiers ())));
+			Assert.Equal ("Replaced", lbl.Text);
+
+			lbl.Text = "Original";
+			cm.Key = Key.Space | Key.CtrlMask;
+			Assert.True (lbl.ProcessKey (new KeyEvent (cm.Key, new KeyModifiers ())));
+			Assert.Equal ("Replaced", lbl.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void MouseFlags_Changing ()
+		{
+			var lbl = new Label ("Original");
+
+			var cm = new ContextMenu ();
+
+			lbl.MouseClick += (e) => {
+				if (e.MouseEvent.Flags == cm.MouseFlags) {
+					lbl.Text = "Replaced";
+					e.Handled = true;
+				}
+			};
+
+			var top = Application.Top;
+			top.Add (lbl);
+			Application.Begin (top);
+
+			Assert.True (lbl.OnMouseEvent (new MouseEvent () { Flags = cm.MouseFlags }));
+			Assert.Equal ("Replaced", lbl.Text);
+
+			lbl.Text = "Original";
+			cm.MouseFlags = MouseFlags.Button2Clicked;
+			Assert.True (lbl.OnMouseEvent (new MouseEvent () { Flags = cm.MouseFlags }));
+			Assert.Equal ("Replaced", lbl.Text);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void KeyChanged_Event ()
+		{
+			var oldKey = Key.Null;
+			var cm = new ContextMenu ();
+
+			cm.KeyChanged += (e) => oldKey = e;
+
+			cm.Key = Key.Space | Key.CtrlMask;
+			Assert.Equal (Key.Space | Key.CtrlMask, cm.Key);
+			Assert.Equal (Key.F10 | Key.ShiftMask, oldKey);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void MouseFlagsChanged_Event ()
+		{
+			var oldMouseFlags = new MouseFlags ();
+			var cm = new ContextMenu ();
+
+			cm.MouseFlagsChanged += (e) => oldMouseFlags = e;
+
+			cm.MouseFlags = MouseFlags.Button2Clicked;
+			Assert.Equal (MouseFlags.Button2Clicked, cm.MouseFlags);
+			Assert.Equal (MouseFlags.Button3Clicked, oldMouseFlags);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Show_Ensures_Display_Inside_The_Container_But_Preserves_Position ()
+		{
+			var cm = new ContextMenu (80, 25,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (80, 25), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (80, 25), cm.Position);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+                                                                        ┌──────┐
+                                                                        │ One  │
+                                                                        │ Two  │
+                                                                        └──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (72, 21), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (80, 25), cm.Position);
+		}
+
+		[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 },
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (70, 25), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (70, 25), cm.Position);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+                                                                      ┌──────┐
+                                                                      │ One  │
+                                                                      │ Two  │
+                                                                      └──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (70, 21), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (70, 25), 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 },
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (11, 6), cm.Position);
+
+			cm.Host.X = 5;
+			cm.Host.Y = 10;
+
+			cm.Show ();
+			Assert.Equal (new Point (6, 11), cm.Position);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+      ┌──────┐
+      │ One  │
+      │ Two  │
+      └──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (6, 12), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (6, 11), cm.Position);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Show_Display_At_Zero_If_The_Toplevel_Width_Is_Less_Than_The_Menu_Width ()
+		{
+			var cm = new ContextMenu (0, 0,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (0, 0), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (0, 0), cm.Position);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (5, 25);
+
+			var expected = @"
+┌────
+│ One
+│ Two
+└────
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (0, 0), cm.Position);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Show_Display_At_Zero_If_The_Toplevel_Height_Is_Less_Than_The_Menu_Height ()
+		{
+			var cm = new ContextMenu (0, 0,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (0, 0), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (0, 0), cm.Position);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (80, 4);
+
+			var expected = @"
+┌──────┐
+│ One  │
+│ Two  │
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+
+			cm.Hide ();
+			Assert.Equal (new Point (0, 0), cm.Position);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Hide_Is_Invoke_At_Container_Closing ()
+		{
+			var cm = new ContextMenu (80, 25,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			var top = Application.Top;
+			Application.Begin (top);
+			top.Running = true;
+
+			Assert.False (ContextMenu.IsShow);
+
+			cm.Show ();
+			Assert.True (ContextMenu.IsShow);
+
+			top.RequestStop ();
+			Assert.False (ContextMenu.IsShow);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ForceMinimumPosToZero_True_False ()
+		{
+			var cm = new ContextMenu (-1, -2,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (-1, -2), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (-1, -2), cm.Position);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+┌──────┐
+│ One  │
+│ Two  │
+└──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+
+			cm.ForceMinimumPosToZero = false;
+			cm.Show ();
+			Assert.Equal (new Point (-1, -2), cm.Position);
+			Application.Refresh ();
+
+			expected = @"
+ One  │
+ Two  │
+──────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (1, 0), pos);
+		}
+	}
+}

+ 44 - 0
UnitTests/GraphViewTests.cs

@@ -116,6 +116,50 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
+		public static Point AssertDriverContentsWithPosAre (string expectedLook, ITestOutputHelper output)
+		{
+			var sb = new StringBuilder ();
+			var driver = ((FakeDriver)Application.Driver);
+			var x = -1;
+			var y = -1;
+
+			var contents = driver.Contents;
+
+			for (int r = 0; r < driver.Rows; r++) {
+				for (int c = 0; c < driver.Cols; c++) {
+					var rune = (char)contents [r, c, 0];
+					if (x == -1 && rune != ' ') {
+						x = c;
+						y = r;
+					}
+					sb.Append (rune);
+				}
+				sb.AppendLine ();
+			}
+
+			var actualLook = sb.ToString ();
+
+			if (!string.Equals (expectedLook, actualLook)) {
+
+				// ignore trailing whitespace on each line
+				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 ();
+				actualLook = trailingWhitespace.Replace (actualLook, "").Trim ();
+
+				// standardise line endings for the comparison
+				expectedLook = expectedLook.Replace ("\r\n", "\n");
+				actualLook = actualLook.Replace ("\r\n", "\n");
+
+				output?.WriteLine ("Expected:" + Environment.NewLine + expectedLook);
+				output?.WriteLine ("But Was:" + Environment.NewLine + actualLook);
+
+				Assert.Equal (expectedLook, actualLook);
+			}
+			return new Point (x, y);
+		}
+
 #pragma warning disable xUnit1013 // Public method should be marked as test
 		/// <summary>
 		/// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.

+ 146 - 7
UnitTests/MenuTests.cs

@@ -1,12 +1,17 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.Views {
 	public class MenuTests {
+		readonly ITestOutputHelper output;
+
+		public MenuTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
 		[Fact]
 		public void Constuctors_Defaults ()
 		{
@@ -95,6 +100,8 @@ namespace Terminal.Gui.Views {
 		{
 			var miAction = "";
 			var isMenuClosed = true;
+			var cancelClosing = false;
+
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_New", "Creates new file.", New)
@@ -119,9 +126,14 @@ namespace Terminal.Gui.Views {
 				e.Action ();
 				Assert.Equal ("Copy", miAction);
 			};
-			menu.MenuClosing += () => {
+			menu.MenuClosing += (e) => {
 				Assert.False (isMenuClosed);
-				isMenuClosed = true;
+				if (cancelClosing) {
+					e.Cancel = true;
+					isMenuClosed = false;
+				} else {
+					isMenuClosed = true;
+				}
 			};
 			Application.Top.Add (menu);
 
@@ -129,10 +141,37 @@ namespace Terminal.Gui.Views {
 			Assert.True (menu.IsMenuOpen);
 			isMenuClosed = !menu.IsMenuOpen;
 			Assert.False (isMenuClosed);
-
+			Application.Top.Redraw (Application.Top.Bounds);
+			var expected = @"
+Edit
+┌─────────────────────────────┐
+│ Copy  Copies the selection. │
+└─────────────────────────────┘
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			cancelClosing = true;
+			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			Assert.True (menu.IsMenuOpen);
+			Assert.False (isMenuClosed);
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+Edit
+┌─────────────────────────────┐
+│ Copy  Copies the selection. │
+└─────────────────────────────┘
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			cancelClosing = false;
 			Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
 			Assert.False (menu.IsMenuOpen);
 			Assert.True (isMenuClosed);
+			Application.Top.Redraw (Application.Top.Bounds);
+			expected = @"
+Edit
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
 
 			void New () => miAction = "New";
 			void Copy () => miAction = "Copy";
@@ -249,7 +288,7 @@ namespace Terminal.Gui.Views {
 				miCurrent = e;
 				mCurrent = menu.openCurrentMenu;
 			};
-			menu.MenuClosing += () => {
+			menu.MenuClosing += (_) => {
 				mbiCurrent = null;
 				miCurrent = null;
 				mCurrent = null;
@@ -450,5 +489,105 @@ namespace Terminal.Gui.Views {
 				return miCurrent != null ? miCurrent.Title.ToString () : "None";
 			}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void DrawFrame_With_Positive_Positions ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			});
+
+			Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y));
+
+			menu.OpenMenu ();
+			Application.Begin (Application.Top);
+
+			var expected = @"
+┌──────┐
+│ One  │
+│ Two  │
+└──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void DrawFrame_With_Negative_Positions ()
+		{
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null)
+				})
+			}) {
+				X = -1,
+				Y = -1
+			};
+
+			Assert.Equal (new Point (-1, -1), new Point (menu.Frame.X, menu.Frame.Y));
+
+			menu.OpenMenu ();
+			Application.Begin (Application.Top);
+
+			var expected = @"
+──────┐
+ One  │
+ Two  │
+──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 0), pos);
+
+			menu.CloseAllMenus ();
+			menu.Frame = new Rect (-1, -2, menu.Frame.Width, menu.Frame.Height);
+			menu.OpenMenu ();
+			Application.Refresh ();
+
+			expected = @"
+ One  │
+ Two  │
+──────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (1, 0), pos);
+
+			menu.CloseAllMenus ();
+			menu.Frame = new Rect (0, 0, menu.Frame.Width, menu.Frame.Height);
+			((FakeDriver)Application.Driver).SetBufferSize (7, 5);
+			menu.OpenMenu ();
+			Application.Refresh ();
+
+			expected = @"
+┌──────
+│ One  
+│ Two  
+└──────
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+
+			menu.CloseAllMenus ();
+			menu.Frame = new Rect (0, 0, menu.Frame.Width, menu.Frame.Height);
+			((FakeDriver)Application.Driver).SetBufferSize (7, 4);
+			menu.OpenMenu ();
+			Application.Refresh ();
+
+			expected = @"
+┌──────
+│ One  
+│ Two  
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 1), pos);
+		}
 	}
 }

+ 66 - 3
UnitTests/TextFieldTests.cs

@@ -1092,15 +1092,20 @@ namespace Terminal.Gui.Views {
 			tf.CursorPosition = tf.Text.Length;
 			Assert.True (tf.ProcessKey (new KeyEvent (Key.Backspace | Key.CtrlMask, new KeyModifiers ())));
 			Assert.Equal ("to jump between text ", tf.Text);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.T | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ("to jump between text ", tf.SelectedText);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ("", tf.Text);
 		}
 
 		[Fact]
 		[AutoInitShutdown]
 		public void Adjust_First ()
 		{
-			TextField tf = new TextField ();
-			tf.Width = Dim.Fill ();
-			tf.Text = "This is a test.";
+			TextField tf = new TextField () {
+				Width = Dim.Fill (),
+				Text = "This is a test."
+			};
 			Application.Top.Add (tf);
 			Application.Begin (Application.Top);
 
@@ -1115,5 +1120,63 @@ namespace Terminal.Gui.Views {
 				return item;
 			}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void DeleteSelectedText_InsertText_DeleteCharLeft_DeleteCharRight_Cut ()
+		{
+			var newText = "";
+			var oldText = "";
+			var tf = new TextField () { Width = 10, Text = "-1" };
+
+			tf.TextChanging += (e) => newText = e.NewText.ToString ();
+			tf.TextChanged += (e) => oldText = e.ToString ();
+
+			Application.Top.Add (tf);
+			Application.Begin (Application.Top);
+
+			Assert.Equal ("-1", tf.Text);
+
+			// InsertText
+			tf.SelectedStart = 1;
+			tf.CursorPosition = 2;
+			Assert.Equal (1, tf.SelectedLength);
+			Assert.Equal ("1", tf.SelectedText);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.D2, new KeyModifiers ())));
+			Assert.Equal ("-2", newText);
+			Assert.Equal ("-1", oldText);
+			Assert.Equal ("-2", tf.Text);
+
+			// DeleteCharLeft
+			tf.SelectedStart = 1;
+			tf.CursorPosition = 2;
+			Assert.Equal (1, tf.SelectedLength);
+			Assert.Equal ("2", tf.SelectedText);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Assert.Equal ("-", newText);
+			Assert.Equal ("-2", oldText);
+			Assert.Equal ("-", tf.Text);
+
+			// DeleteCharRight
+			tf.Text = "-1";
+			tf.SelectedStart = 1;
+			tf.CursorPosition = 2;
+			Assert.Equal (1, tf.SelectedLength);
+			Assert.Equal ("1", tf.SelectedText);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			Assert.Equal ("-", newText);
+			Assert.Equal ("-1", oldText);
+			Assert.Equal ("-", tf.Text);
+
+			// Cut
+			tf.Text = "-1";
+			tf.SelectedStart = 1;
+			tf.CursorPosition = 2;
+			Assert.Equal (1, tf.SelectedLength);
+			Assert.Equal ("1", tf.SelectedText);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.X | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ("-", newText);
+			Assert.Equal ("-1", oldText);
+			Assert.Equal ("-", tf.Text);
+		}
 	}
 }

+ 102 - 13
UnitTests/ViewTests.cs

@@ -1,16 +1,20 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using Terminal.Gui;
+using System;
 using Xunit;
+using Xunit.Abstractions;
+using GraphViewTests = Terminal.Gui.Views.GraphViewTests;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
-namespace Terminal.Gui.Views {
+namespace Terminal.Gui.Core {
 	public class ViewTests {
+		readonly ITestOutputHelper output;
+
+		public ViewTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
 		[Fact]
 		public void New_Initializes ()
 		{
@@ -1658,7 +1662,7 @@ namespace Terminal.Gui.Views {
 			var win1 = new Window () { Width = Dim.Percent (50), Height = Dim.Fill () };
 			win1.Add (view1);
 			var view2 = new View () { Width = 20, Height = 2, CanFocus = true };
-			var win2 = new Window () { X = Pos.Right (win1),  Width = Dim.Fill (), Height = Dim.Fill () };
+			var win2 = new Window () { X = Pos.Right (win1), Width = Dim.Fill (), Height = Dim.Fill () };
 			win2.Add (view2);
 			Application.Top.Add (win1, win2);
 			Application.Begin (Application.Top);
@@ -1828,28 +1832,113 @@ namespace Terminal.Gui.Views {
 			Assert.True (sbQuiting);
 			Assert.False (tfQuiting);
 		}
-		
+
 		[Fact]
 		[AutoInitShutdown]
 		public void WindowDispose_CanFocusProblem ()
 		{
 			// Arrange
-			Application.Init();
-			using var top = Toplevel.Create();
+			Application.Init ();
+			using var top = Toplevel.Create ();
 			using var view = new View (
 				x: 0,
 				y: 1,
 				text: nameof (WindowDispose_CanFocusProblem));
 			using var window = new Window ();
-			top.Add(window);
+			top.Add (window);
 			window.Add (view);
 
 			// Act
-			Application.Begin(top);
+			Application.Begin (top);
 			Application.Shutdown ();
 
 			// Assert does Not throw NullReferenceException
 			top.SetFocus ();
 		}
+
+		[Fact, AutoInitShutdown]
+		public void DrawFrame_With_Positive_Positions ()
+		{
+			var view = new View (new Rect (0, 0, 8, 4));
+
+			view.DrawContent += (_) => view.DrawFrame (view.Bounds, 0, true);
+
+			Assert.Equal (Point.Empty, new Point (view.Frame.X, view.Frame.Y));
+			Assert.Equal (new Size (8, 4), new Size (view.Frame.Width, view.Frame.Height));
+
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+┌──────┐
+│      │
+│      │
+└──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 0), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void DrawFrame_With_Negative_Positions ()
+		{
+			var view = new View (new Rect (-1, 0, 8, 4));
+
+			view.DrawContent += (_) => view.DrawFrame (view.Bounds, 0, true);
+
+			Assert.Equal (new Point (-1, 0), new Point (view.Frame.X, view.Frame.Y));
+			Assert.Equal (new Size (8, 4), new Size (view.Frame.Width, view.Frame.Height));
+
+			Application.Top.Add (view);
+			Application.Begin (Application.Top);
+
+			var expected = @"
+──────┐
+      │
+      │
+──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (0, 0), pos);
+
+			view.Frame = new Rect (-1, -1, 8, 4);
+			Application.Refresh ();
+
+			expected = @"
+      │
+      │
+──────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output);
+			Assert.Equal (new Point (6, 0), 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);
+
+			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);
+		}
 	}
 }