Browse Source

Merge branch 'develop' of tig:migueldeicaza/gui.cs into develop

Charlie Kindel 2 years ago
parent
commit
86c0477966

+ 2 - 1
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -663,7 +663,7 @@ namespace Terminal.Gui {
 
 			// Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey
 			if (wch == 27) {
-				Curses.timeout (200);
+				Curses.timeout (10);
 
 				code = Curses.get_wch (out int wch2);
 
@@ -820,6 +820,7 @@ namespace Terminal.Gui {
 				//Console.Out.Flush ();
 
 				window = Curses.initscr ();
+				Curses.set_escdelay (10);
 			} catch (Exception e) {
 				throw new Exception ($"Curses failed to initialize, the exception is: {e.Message}");
 			}

+ 4 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/binding.cs

@@ -330,6 +330,7 @@ namespace Unix.Terminal {
 		static public int reset_shell_mode () => methods.reset_shell_mode ();
 		static public int savetty () => methods.savetty ();
 		static public int resetty () => methods.resetty ();
+		static public int set_escdelay (int size) => methods.set_escdelay (size);
 	}
 
 #pragma warning disable RCS1102 // Make class static.
@@ -405,6 +406,7 @@ namespace Unix.Terminal {
 		public delegate int reset_shell_mode ();
 		public delegate int savetty ();
 		public delegate int resetty ();
+		public delegate int set_escdelay (int size);
 	}
 
 	internal class NativeMethods {
@@ -478,6 +480,7 @@ namespace Unix.Terminal {
 		public readonly Delegates.reset_shell_mode reset_shell_mode;
 		public readonly Delegates.savetty savetty;
 		public readonly Delegates.resetty resetty;
+		public readonly Delegates.set_escdelay set_escdelay;
 		public UnmanagedLibrary UnmanagedLibrary;
 
 		public NativeMethods (UnmanagedLibrary lib)
@@ -553,6 +556,7 @@ namespace Unix.Terminal {
 			reset_shell_mode = lib.GetNativeMethodDelegate<Delegates.reset_shell_mode> ("reset_shell_mode");
 			savetty = lib.GetNativeMethodDelegate<Delegates.savetty> ("savetty");
 			resetty = lib.GetNativeMethodDelegate<Delegates.resetty> ("resetty");
+			set_escdelay = lib.GetNativeMethodDelegate<Delegates.set_escdelay> ("set_escdelay");
 		}
 	}
 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

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

@@ -271,7 +271,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Configurable keybindings supported by the control
 		/// </summary>
-		private Dictionary<Key, Command> KeyBindings { get; set; } = new Dictionary<Key, Command> ();
+		private Dictionary<Key, Command []> KeyBindings { get; set; } = new Dictionary<Key, Command []> ();
 		private Dictionary<Command, Func<bool?>> CommandImplementations { get; set; } = new Dictionary<Command, Func<bool?>> ();
 
 		/// <summary>
@@ -1716,17 +1716,32 @@ namespace Terminal.Gui {
 		/// <param name="keyEvent">The key event passed.</param>
 		protected bool? InvokeKeybindings (KeyEvent keyEvent)
 		{
+			bool? toReturn = null;
+
 			if (KeyBindings.ContainsKey (keyEvent.Key)) {
-				var command = KeyBindings [keyEvent.Key];
 
-				if (!CommandImplementations.ContainsKey (command)) {
-					throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
-				}
+				foreach (var command in KeyBindings [keyEvent.Key]) {
+
+					if (!CommandImplementations.ContainsKey (command)) {
+						throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
+					}
+
+					// each command has its own return value
+					var thisReturn = CommandImplementations [command] ();
+
+					// if we haven't got anything yet, the current command result should be used
+					if (toReturn == null) {
+						toReturn = thisReturn;
+					}
 
-				return CommandImplementations [command] ();
+					// if ever see a true then that's what we will return
+					if (thisReturn ?? false) {
+						toReturn = thisReturn.Value;
+					}
+				}
 			}
 
-			return null;
+			return toReturn;
 		}
 
 
@@ -1736,11 +1751,19 @@ namespace Terminal.Gui {
 		/// </para>
 		/// <para>If the key is already bound to a different <see cref="Command"/> it will be
 		/// rebound to this one</para>
+		/// <remarks>Commands are only ever applied to the current <see cref="View"/>(i.e. this feature
+		/// cannot be used to switch focus to another view and perform multiple commands there)</remarks>
 		/// </summary>
 		/// <param name="key"></param>
-		/// <param name="command"></param>
-		public void AddKeyBinding (Key key, Command command)
+		/// <param name="command">The command(s) to run on the <see cref="View"/> when <paramref name="key"/> is pressed.
+		/// When specifying multiple, all commands will be applied in sequence.  The bound <paramref name="key"/> strike
+		/// will be consumed if any took effect.</param>
+		public void AddKeyBinding (Key key, params Command [] command)
 		{
+			if (command.Length == 0) {
+				throw new ArgumentException ("At least one command must be specified", nameof (command));
+			}
+
 			if (KeyBindings.ContainsKey (key)) {
 				KeyBindings [key] = command;
 			} else {
@@ -1756,7 +1779,7 @@ namespace Terminal.Gui {
 		protected void ReplaceKeyBinding (Key fromKey, Key toKey)
 		{
 			if (KeyBindings.ContainsKey (fromKey)) {
-				Command value = KeyBindings [fromKey];
+				var value = KeyBindings [fromKey];
 				KeyBindings.Remove (fromKey);
 				KeyBindings [toKey] = value;
 			}
@@ -1795,9 +1818,9 @@ namespace Terminal.Gui {
 		/// keys bound to the same command and this method will clear all of them.
 		/// </summary>
 		/// <param name="command"></param>
-		public void ClearKeybinding (Command command)
+		public void ClearKeybinding (params Command [] command)
 		{
-			foreach (var kvp in KeyBindings.Where (kvp => kvp.Value == command).ToArray ()) {
+			foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray()) {
 				KeyBindings.Remove (kvp.Key);
 			}
 		}
@@ -1837,9 +1860,9 @@ namespace Terminal.Gui {
 		/// </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)
+		public Key GetKeyFromCommand (params Command [] command)
 		{
-			return KeyBindings.First (x => x.Value == command).Key;
+			return KeyBindings.First (x => x.Value.SequenceEqual (command)).Key;
 		}
 
 		/// <inheritdoc/>

+ 1 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -16,7 +16,7 @@
     <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
     <PackageReference Include="NStack.Core" Version="0.17.1" />
     <InternalsVisibleTo Include="UnitTests" />
   </ItemGroup>

+ 15 - 6
Terminal.Gui/Views/Menu.cs

@@ -384,17 +384,26 @@ namespace Terminal.Gui {
 		internal int current;
 		internal View previousSubFocused;
 
-		internal static Rect MakeFrame (int x, int y, MenuItem [] items)
+		internal static Rect MakeFrame (int x, int y, MenuItem [] items, Menu parent = null)
 		{
 			if (items == null || items.Length == 0) {
 				return new Rect ();
 			}
-			int maxW = items.Max (z => z?.Width) ?? 0;
-
-			return new Rect (x, y, maxW + 2, items.Length + 2);
+			int minX = x;
+			int minY = y;
+			int maxW = (items.Max (z => z?.Width) ?? 0) + 2;
+			int maxH = items.Length + 2;
+			if (parent != null && x + maxW > Driver.Cols) {
+				minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0);
+			}
+			if (y + maxH > Driver.Rows) {
+				minY = Math.Max (Driver.Rows - maxH, 0);
+			}
+			return new Rect (minX, minY, maxW, maxH);
 		}
 
-		public Menu (MenuBar host, int x, int y, MenuBarItem barItems) : base (MakeFrame (x, y, barItems.Children))
+		public Menu (MenuBar host, int x, int y, MenuBarItem barItems, Menu parent = null)
+			: base (MakeFrame (x, y, barItems.Children, parent))
 		{
 			this.barItems = barItems;
 			this.host = host;
@@ -1232,7 +1241,7 @@ namespace Terminal.Gui {
 				} else {
 					var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu;
 					if (!UseSubMenusSingleFrame) {
-						openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu);
+						openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu, last);
 					} else {
 						var first = openSubMenu.Count > 0 ? openSubMenu.First () : openMenu;
 						var mbi = new MenuItem [2 + subMenu.Children.Length];

+ 2 - 0
Terminal.Gui/Views/TextField.cs

@@ -203,6 +203,8 @@ namespace Terminal.Gui {
 			AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut);
 			AddKeyBinding (Key.V | Key.CtrlMask, Command.Paste);
 			AddKeyBinding (Key.T | Key.CtrlMask, Command.SelectAll);
+
+			AddKeyBinding (Key.R | Key.CtrlMask, Command.DeleteAll);
 			AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll);
 
 			currentCulture = Thread.CurrentThread.CurrentUICulture;

+ 16 - 18
Terminal.Gui/Views/TextView.cs

@@ -1372,6 +1372,8 @@ namespace Terminal.Gui {
 
 			AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo);
 			AddKeyBinding (Key.R | Key.CtrlMask, Command.Redo);
+
+			AddKeyBinding (Key.G | Key.CtrlMask, Command.DeleteAll);
 			AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll);
 
 			currentCulture = Thread.CurrentThread.CurrentUICulture;
@@ -2332,15 +2334,15 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Invoke the <see cref="UnwrappedCursorPosition"/> event with the unwrapped <see cref="CursorPosition"/>.
 		/// </summary>
-		public virtual void OnUnwrappedCursorPosition ()
+		public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null)
 		{
-			var row = currentRow;
-			var col = currentColumn;
-			if (wordWrap) {
+			var row = cRow == null ? currentRow : cRow;
+			var col = cCol == null ? currentColumn : cCol;
+			if (cRow == null && cCol == null && wordWrap) {
 				row = wrapManager.GetModelLineFromWrappedLines (currentRow);
 				col = wrapManager.GetModelColFromWrappedLines (currentRow, currentColumn);
 			}
-			UnwrappedCursorPosition?.Invoke (new Point (col, row));
+			UnwrappedCursorPosition?.Invoke (new Point ((int)col, (int)row));
 		}
 
 		ustring GetSelectedRegion ()
@@ -2357,6 +2359,7 @@ namespace Terminal.Gui {
 				startCol = wrapManager.GetModelColFromWrappedLines (selectionStartRow, selectionStartColumn);
 				model = wrapManager.Model;
 			}
+			OnUnwrappedCursorPosition (cRow, cCol);
 			return GetRegion (startRow, startCol, cRow, cCol, model);
 		}
 
@@ -3177,14 +3180,14 @@ namespace Terminal.Gui {
 			if (newPos.HasValue && currentRow == newPos.Value.row) {
 				var restCount = currentColumn - newPos.Value.col;
 				currentLine.RemoveRange (newPos.Value.col, restCount);
-				if (wordWrap && wrapManager.RemoveRange (currentRow, newPos.Value.col, restCount)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 				currentColumn = newPos.Value.col;
 			} else if (newPos.HasValue) {
 				var restCount = currentLine.Count - currentColumn;
 				currentLine.RemoveRange (currentColumn, restCount);
-				if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 				currentColumn = newPos.Value.col;
@@ -3234,7 +3237,7 @@ namespace Terminal.Gui {
 				restCount = currentLine.Count - currentColumn;
 				currentLine.RemoveRange (currentColumn, restCount);
 			}
-			if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) {
+			if (wordWrap) {
 				wrapNeeded = true;
 			}
 
@@ -3716,7 +3719,7 @@ namespace Terminal.Gui {
 				historyText.Add (new List<List<Rune>> () { new List<Rune> (currentLine) }, CursorPosition,
 					HistoryText.LineStatus.Replaced);
 
-				if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out _)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 				if (wrapNeeded) {
@@ -3733,7 +3736,7 @@ namespace Terminal.Gui {
 				historyText.Add (new List<List<Rune>> () { new List<Rune> (currentLine) }, CursorPosition,
 					HistoryText.LineStatus.Replaced);
 
-				if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 
@@ -3761,7 +3764,7 @@ namespace Terminal.Gui {
 				historyText.Add (new List<List<Rune>> () { new List<Rune> (currentLine) }, CursorPosition);
 
 				currentLine.RemoveAt (currentColumn - 1);
-				if (wordWrap && wrapManager.RemoveAt (currentRow, currentColumn - 1)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 				currentColumn--;
@@ -3793,8 +3796,7 @@ namespace Terminal.Gui {
 				var prevCount = prevRow.Count;
 				model.GetLine (prowIdx).AddRange (GetCurrentLine ());
 				model.RemoveLine (currentRow);
-				bool lineRemoved = false;
-				if (wordWrap && wrapManager.RemoveLine (currentRow, currentColumn, out lineRemoved, false)) {
+				if (wordWrap) {
 					wrapNeeded = true;
 				}
 				currentRow--;
@@ -3802,11 +3804,7 @@ namespace Terminal.Gui {
 				historyText.Add (new List<List<Rune>> () { GetCurrentLine () }, new Point (currentColumn, prowIdx),
 					HistoryText.LineStatus.Replaced);
 
-				if (wrapNeeded && !lineRemoved) {
-					currentColumn = Math.Max (prevCount - 1, 0);
-				} else {
-					currentColumn = prevCount;
-				}
+				currentColumn = prevCount;
 				SetNeedsDisplay ();
 			}
 

+ 1 - 1
Terminal.Gui/Views/TreeView.cs

@@ -907,7 +907,7 @@ namespace Terminal.Gui {
 		{
 			var map = BuildLineMap ();
 			ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1);
-			SelectedObject = map.Last ().Model;
+			SelectedObject = map.LastOrDefault ()?.Model;
 
 			SetNeedsDisplay ();
 		}

+ 1 - 0
Terminal.Gui/Windows/MessageBox.cs

@@ -284,6 +284,7 @@ namespace Terminal.Gui {
 				l.Y = Pos.Center ();
 				l.Width = Dim.Fill (2);
 				l.Height = Dim.Fill (1);
+				l.AutoSize = false;
 				d.Add (l);
 			}
 

+ 9 - 2
UICatalog/KeyBindingsDialog.cs

@@ -132,7 +132,7 @@ namespace UICatalog {
 				Width = Dim.Percent (50),
 				Height = Dim.Percent (100) - 1,
 			};
-			commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged;
+
 			Add (commandsListView);
 
 			keyLabel = new Label () {
@@ -143,7 +143,7 @@ namespace UICatalog {
 			};
 			Add (keyLabel);
 
-			var btnChange = new Button ("Change") {
+			var btnChange = new Button ("Ch_ange") {
 				X = Pos.Percent (50),
 				Y = 1,
 			};
@@ -160,6 +160,13 @@ namespace UICatalog {
 			var cancel = new Button ("Cancel");
 			cancel.Clicked += ()=>Application.RequestStop();
 			AddButton (cancel);
+
+			// Register event handler as the last thing in constructor to prevent early calls
+			// before it is even shown (e.g. OnEnter)
+			commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged;
+
+			// Setup to show first ListView entry
+			SetTextBoxToShowBinding (commands.First());
 		}
 
 		private void RemapKey ()

+ 239 - 30
UnitTests/ContextMenuTests.cs

@@ -423,7 +423,7 @@ namespace Terminal.Gui.Core {
 			cm.Show ();
 			Assert.Equal (new Point (0, 0), cm.Position);
 			Application.Begin (Application.Top);
-			((FakeDriver)Application.Driver).SetBufferSize (80, 4);
+			((FakeDriver)Application.Driver).SetBufferSize (80, 3);
 
 			var expected = @"
 ┌──────┐
@@ -432,7 +432,7 @@ namespace Terminal.Gui.Core {
 ";
 
 			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (0, 1, 8, 3), pos);
+			Assert.Equal (new Rect (0, 0, 8, 3), pos);
 
 			cm.Hide ();
 			Assert.Equal (new Point (0, 0), cm.Position);
@@ -592,27 +592,27 @@ namespace Terminal.Gui.Core {
 			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                    
+  File   Edit                   
+                                
+                                
+  Label: TextField              
+         ┌─────────────────────┐
+         │ Select All   Ctrl+T │
+         │ Delete All   Ctrl+R
+         │ 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, 38, 17), pos);
+			Assert.Equal (new Rect (2, 0, 32, 17), pos);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -648,7 +648,6 @@ namespace Terminal.Gui.Core {
 			Application.Begin (Application.Top);
 			((FakeDriver)Application.Driver).SetBufferSize (44, 17);
 
-
 			Assert.Equal (new Rect (9, 3, 20, 1), tf.Frame);
 			Assert.True (tf.HasFocus);
 
@@ -663,15 +662,15 @@ namespace Terminal.Gui.Core {
 │                                          │
 │                                          │
 │  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 │    │
-│         └───────────────────────────┘    │
+│         ┌─────────────────────┐      
+│         │ Select All   Ctrl+T │      
+│         │ Delete All   Ctrl+R │      
+│         │ Copy         Ctrl+C │      
+│         │ Cut          Ctrl+X │      
+│         │ Paste        Ctrl+V │      
+│         │ Undo         Ctrl+Z │      
+│         │ Redo         Ctrl+Y │      
+│         └─────────────────────┘      
 └──────────────────────────────────────────┘
  F1 Help │ ^Q Quit                          
 ";
@@ -679,5 +678,215 @@ namespace Terminal.Gui.Core {
 			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (new Rect (2, 0, 44, 17), pos);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Menus_And_SubMenus_Always_Try_To_Be_On_Screen ()
+		{
+			var cm = new ContextMenu (-1, -2,
+				new MenuBarItem (new MenuItem [] {
+					new MenuItem ("One", "", null),
+					new MenuItem ("Two", "", null),
+					new MenuItem ("Three", "", null),
+					new MenuBarItem ("Four", new MenuItem [] {
+						new MenuItem ("SubMenu1", "", null),
+						new MenuItem ("SubMenu2", "", null),
+						new MenuItem ("SubMenu3", "", null),
+						new MenuItem ("SubMenu4", "", null),
+						new MenuItem ("SubMenu5", "", null),
+						new MenuItem ("SubMenu6", "", null),
+						new MenuItem ("SubMenu7", "", null)
+					}),
+					new MenuItem ("Five", "", null),
+					new MenuItem ("Six", "", null)
+				})
+			);
+
+			Assert.Equal (new Point (-1, -2), cm.Position);
+
+			cm.Show ();
+			Assert.Equal (new Point (-1, -2), cm.Position);
+			var top = Application.Top;
+			Application.Begin (top);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+┌────────┐
+│ One    │
+│ Two    │
+│ Three  │
+│ Four  ►│
+│ Five   │
+│ Six    │
+└────────┘
+", output);
+
+			Assert.True (top.Subviews [0].MouseEvent (new MouseEvent {
+				X = 0,
+				Y = 4,
+				Flags = MouseFlags.ReportMousePosition,
+				View = top.Subviews [0]
+			}));
+			Application.Refresh ();
+			Assert.Equal (new Point (-1, -2), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+┌────────┐             
+│ One    │             
+│ Two    │             
+│ Three  │             
+│ Four  ►│┌───────────┐
+│ Five   ││ SubMenu1  │
+│ Six    ││ SubMenu2  │
+└────────┘│ SubMenu3  │
+          │ SubMenu4  │
+          │ SubMenu5  │
+          │ SubMenu6  │
+          │ SubMenu7  │
+          └───────────┘
+", output);
+
+			((FakeDriver)Application.Driver).SetBufferSize (40, 20);
+			cm.Position = new Point (41, -2);
+			cm.Show ();
+			Application.Refresh ();
+			Assert.Equal (new Point (41, -2), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                              ┌────────┐
+                              │ One    │
+                              │ Two    │
+                              │ Three  │
+                              │ Four  ►│
+                              │ Five   │
+                              │ Six    │
+                              └────────┘
+", output);
+
+			Assert.True (top.Subviews [0].MouseEvent (new MouseEvent {
+				X = 30,
+				Y = 4,
+				Flags = MouseFlags.ReportMousePosition,
+				View = top.Subviews [0]
+			}));
+			Application.Refresh ();
+			Assert.Equal (new Point (41, -2), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                              ┌────────┐
+                              │ One    │
+                              │ Two    │
+                              │ Three  │
+                 ┌───────────┐│ Four  ►│
+                 │ SubMenu1  ││ Five   │
+                 │ SubMenu2  ││ Six    │
+                 │ SubMenu3  │└────────┘
+                 │ SubMenu4  │          
+                 │ SubMenu5  │          
+                 │ SubMenu6  │          
+                 │ SubMenu7  │          
+                 └───────────┘          
+", output);
+
+			cm.Position = new Point (41, 9);
+			cm.Show ();
+			Application.Refresh ();
+			Assert.Equal (new Point (41, 9), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                              ┌────────┐
+                              │ One    │
+                              │ Two    │
+                              │ Three  │
+                              │ Four  ►│
+                              │ Five   │
+                              │ Six    │
+                              └────────┘
+", output);
+
+			Assert.True (top.Subviews [0].MouseEvent (new MouseEvent {
+				X = 30,
+				Y = 4,
+				Flags = MouseFlags.ReportMousePosition,
+				View = top.Subviews [0]
+			}));
+			Application.Refresh ();
+			Assert.Equal (new Point (41, 9), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                              ┌────────┐
+                 ┌───────────┐│ One    │
+                 │ SubMenu1  ││ Two    │
+                 │ SubMenu2  ││ Three  │
+                 │ SubMenu3  ││ Four  ►│
+                 │ SubMenu4  ││ Five   │
+                 │ SubMenu5  ││ Six    │
+                 │ SubMenu6  │└────────┘
+                 │ SubMenu7  │          
+                 └───────────┘          
+", output);
+
+			cm.Position = new Point (41, 22);
+			cm.Show ();
+			Application.Refresh ();
+			Assert.Equal (new Point (41, 22), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                              ┌────────┐
+                              │ One    │
+                              │ Two    │
+                              │ Three  │
+                              │ Four  ►│
+                              │ Five   │
+                              │ Six    │
+                              └────────┘
+", output);
+
+			Assert.True (top.Subviews [0].MouseEvent (new MouseEvent {
+				X = 30,
+				Y = 4,
+				Flags = MouseFlags.ReportMousePosition,
+				View = top.Subviews [0]
+			}));
+			Application.Refresh ();
+			Assert.Equal (new Point (41, 22), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+                 ┌───────────┐          
+                 │ SubMenu1  │┌────────┐
+                 │ SubMenu2  ││ One    │
+                 │ SubMenu3  ││ Two    │
+                 │ SubMenu4  ││ Three  │
+                 │ SubMenu5  ││ Four  ►│
+                 │ SubMenu6  ││ Five   │
+                 │ SubMenu7  ││ Six    │
+                 └───────────┘└────────┘
+", output);
+
+			((FakeDriver)Application.Driver).SetBufferSize (18, 8);
+			cm.Position = new Point (19, 10);
+			cm.Show ();
+			Application.Refresh ();
+			Assert.Equal (new Point (19, 10), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+        ┌────────┐
+        │ One    │
+        │ Two    │
+        │ Three  │
+        │ Four  ►│
+        │ Five   │
+        │ Six    │
+        └────────┘
+", output);
+
+			Assert.True (top.Subviews [0].MouseEvent (new MouseEvent {
+				X = 30,
+				Y = 4,
+				Flags = MouseFlags.ReportMousePosition,
+				View = top.Subviews [0]
+			}));
+			Application.Refresh ();
+			Assert.Equal (new Point (19, 10), cm.Position);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+┌───────────┐────┐
+│ SubMenu1  │    │
+│ SubMenu2  │    │
+│ SubMenu3  │ee  │
+│ SubMenu4  │r  ►│
+│ SubMenu5  │e   │
+│ SubMenu6  │    │
+│ SubMenu7  │────┘
+", output);
+		}
 	}
 }

+ 91 - 0
UnitTests/ListViewTests.cs

@@ -30,6 +30,97 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame);
 		}
 
+		[Fact]
+		public void ListViewSelectThenDown ()
+		{
+			var lv = new ListView (new List<string> () { "One", "Two", "Three" });
+			lv.AllowsMarking = true;
+
+			Assert.NotNull (lv.Source);
+
+			// first item should be selected by default
+			Assert.Equal (0, lv.SelectedItem);
+
+			// nothing is ticked
+			Assert.False (lv.Source.IsMarked (0));
+			Assert.False (lv.Source.IsMarked (1));
+			Assert.False (lv.Source.IsMarked (2));
+
+			lv.AddKeyBinding (Key.Space | Key.ShiftMask, Command.ToggleChecked, Command.LineDown);
+
+			var ev = new KeyEvent (Key.Space | Key.ShiftMask, new KeyModifiers () { Shift = true });
+
+			// view should indicate that it has accepted and consumed the event
+			Assert.True (lv.ProcessKey (ev));
+
+			// second item should now be selected
+			Assert.Equal (1, lv.SelectedItem);
+
+			// first item only should be ticked
+			Assert.True (lv.Source.IsMarked (0));
+			Assert.False (lv.Source.IsMarked (1));
+			Assert.False (lv.Source.IsMarked (2));
+
+			// Press key combo again
+			Assert.True (lv.ProcessKey (ev));
+			Assert.Equal (2, lv.SelectedItem);
+			Assert.True (lv.Source.IsMarked (0));
+			Assert.True (lv.Source.IsMarked (1));
+			Assert.False (lv.Source.IsMarked (2));
+
+			// Press key combo again
+			Assert.True (lv.ProcessKey (ev));
+			Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+			Assert.True (lv.Source.IsMarked (0));
+			Assert.True (lv.Source.IsMarked (1));
+			Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
+
+			// Press key combo again 
+			Assert.True (lv.ProcessKey (ev));
+			Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+			Assert.True (lv.Source.IsMarked (0));
+			Assert.True (lv.Source.IsMarked (1));
+			Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
+		}
+		[Fact]
+		public void SettingEmptyKeybindingThrows ()
+		{
+			var lv = new ListView (new List<string> () { "One", "Two", "Three" });
+			Assert.Throws<ArgumentException> (() => lv.AddKeyBinding (Key.Space));
+		}
+
+
+		/// <summary>
+		/// Tests that when none of the Commands in a chained keybinding are possible
+		/// the <see cref="View.ProcessKey(KeyEvent)"/> returns the appropriate result
+		/// </summary>
+		[Fact]
+		public void ListViewProcessKeyReturnValue_WithMultipleCommands ()
+		{
+			var lv = new ListView (new List<string> () { "One", "Two", "Three", "Four" });
+
+			Assert.NotNull (lv.Source);
+
+			// first item should be selected by default
+			Assert.Equal (0, lv.SelectedItem);
+
+			// bind shift down to move down twice in control
+			lv.AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDown, Command.LineDown);
+
+			var ev = new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers () { Shift = true });
+
+			Assert.True (lv.ProcessKey (ev), "The first time we move down 2 it should be possible");
+
+			// After moving down twice from One we should be at 'Three'
+			Assert.Equal (2, lv.SelectedItem);
+
+			// clear the items
+			lv.SetSource (null);
+
+			// Press key combo again - return should be false this time as none of the Commands are allowable
+			Assert.False (lv.ProcessKey (ev), "We cannot move down so will not respond to this");
+		}
+
 		private class NewListDataSource : IListDataSource {
 			public int Count => throw new NotImplementedException ();
 

+ 2 - 2
UnitTests/MenuTests.cs

@@ -670,7 +670,7 @@ Edit
 
 			menu.CloseAllMenus ();
 			menu.Frame = new Rect (0, 0, menu.Frame.Width, menu.Frame.Height);
-			((FakeDriver)Application.Driver).SetBufferSize (7, 4);
+			((FakeDriver)Application.Driver).SetBufferSize (7, 3);
 			menu.OpenMenu ();
 			Application.Refresh ();
 
@@ -681,7 +681,7 @@ Edit
 ";
 
 			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
-			Assert.Equal (new Rect (0, 1, 7, 3), pos);
+			Assert.Equal (new Rect (0, 0, 7, 3), pos);
 		}
 
 		[Fact, AutoInitShutdown]

+ 245 - 0
UnitTests/TextViewTests.cs

@@ -6024,6 +6024,251 @@ secon
 d    
 line.
 ", output);
+
+			Assert.True (tv.MouseEvent (new MouseEvent () { X = 0, Y = 3, Flags = MouseFlags.Button1Pressed }));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (0, 3), tv.CursorPosition);
+			Assert.Equal (new Point (12, 0), cp);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+This 
+is   
+the  
+first
+     
+line.
+This 
+is   
+the  
+secon
+d    
+line.
+", output);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void DeleteTextBackwards_WordWrap_False_Return_Undo ()
+		{
+			const string text = "This is the first line.\nThis is the second line.\n";
+			var tv = new TextView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Text = text
+			};
+			var envText = tv.Text;
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+
+			Assert.False (tv.WordWrap);
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+This is the first line. 
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (3, 0);
+			Assert.Equal (new Point (3, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (0, 1);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.This is the second line.
+", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			while (tv.Text != envText) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			}
+			Assert.Equal (envText, tv.Text);
+			Assert.Equal (new Point (3, 0), tv.CursorPosition);
+			Assert.False (tv.IsDirty);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void DeleteTextBackwards_WordWrap_True_Return_Undo ()
+		{
+			const string text = "This is the first line.\nThis is the second line.\n";
+			var tv = new TextView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Text = text,
+				WordWrap = true
+			};
+			var envText = tv.Text;
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+
+			Assert.True (tv.WordWrap);
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+This is the first line. 
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (3, 0);
+			Assert.Equal (new Point (3, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (0, 1);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.This is the second line.
+", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			while (tv.Text != envText) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			}
+			Assert.Equal (envText, tv.Text);
+			Assert.Equal (new Point (3, 0), tv.CursorPosition);
+			Assert.False (tv.IsDirty);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void DeleteTextForwards_WordWrap_False_Return_Undo ()
+		{
+			const string text = "This is the first line.\nThis is the second line.\n";
+			var tv = new TextView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Text = text
+			};
+			var envText = tv.Text;
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+
+			Assert.False (tv.WordWrap);
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+This is the first line. 
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (2, 0);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (22, 0);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.This is the second line.
+", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			while (tv.Text != envText) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			}
+			Assert.Equal (envText, tv.Text);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			Assert.False (tv.IsDirty);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void DeleteTextForwards_WordWrap_True_Return_Undo ()
+		{
+			const string text = "This is the first line.\nThis is the second line.\n";
+			var tv = new TextView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Text = text,
+				WordWrap = true
+			};
+			var envText = tv.Text;
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+
+			Assert.True (tv.WordWrap);
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+This is the first line. 
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (2, 0);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			tv.CursorPosition = new Point (22, 0);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (22, 0), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.This is the second line.
+", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			GraphViewTests.AssertDriverContentsWithFrameAre (@"
+Ths is the first line.  
+This is the second line.
+", output);
+
+			while (tv.Text != envText) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			}
+			Assert.Equal (envText, tv.Text);
+			Assert.Equal (new Point (2, 0), tv.CursorPosition);
+			Assert.False (tv.IsDirty);
 		}
 	}
 }

+ 9 - 0
UnitTests/TreeViewTests.cs

@@ -543,6 +543,15 @@ namespace Terminal.Gui.Views {
 			Assert.Equal (1, tree.ScrollOffsetVertical);
 		}
 
+		[Fact]
+		public void GoToEnd_ShouldNotFailOnEmptyTreeView ()
+		{
+			var tree = new TreeView ();
+
+			var exception = Record.Exception (() => tree.GoToEnd ());
+
+			Assert.Null (exception);
+		}
 
 		[Fact]
 		public void ObjectActivated_CustomKey ()

+ 1 - 1
UnitTests/UnitTests.csproj

@@ -18,7 +18,7 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
     <PackageReference Include="ReportGenerator" Version="5.1.9" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="xunit" Version="2.4.2" />