Browse Source

Adds Key Binding support. Also refactors Autocomplete and Undo/Redo. (#1556)

* Refactored ProcessKey to use public methods for case logic

* Added KeyBinding class

* Refactored key binding to split key->command from command->implementation

This reduces duplication and simplifies the API

* Finishing key bindings implementation in ListView.

* Adding more unit tests to the ListView.

* Added key bindings to the Button and more features.

* Replaces Action for Func<KeyEvent, bool> on CommandImplementations.

* Allowing commands to have any number of arguments.

* Implementing key bindings on Checkbox view.

* Added test for changing HotKey in Button and made ReplaceKeyBinding protected

* Changed `CommandImplementations` to `Func<KeyEvent, bool>` to better understand current command implementations

* Implementing key bindings in ComboBox.

* Renamed Command keys and fixed ComboBox issues:

- Fixed pressing Esc in ListAndCombos scenario without selecting cause an array out of bounds error
- Changed the Esc key in ComboBox to also collapse the list selection
- Added bool return to public virtual method Expand and Collapse (this is a breaking change)

* Implementing key bindings in DateField.

* Organizing some things.

* Implementing key bindings on TimeField.

* No key bindings on FrameView.

* Added keybinding support to TreeView

* Added mouse support and more features.

* Updating NuGet packages.

* Putting text on the same line.

* Changing function command to Func<bool>.

* Added a read only Position, CursorPosition properties and events.

* Keybindings for GraphView

* Added a stream argument to ApplyEdits to only save the edits.

* Implementing key bindings on the HexView.

* Added MenuOpened event and others bug fixes.

* Fixing typo.

* Unifying constructors initializations.

* Implementing keybindings in the Menu.

* Removing unnecessary variable.

* Implementing keybindings in RadioGroup view.

* Changing Home to TopHome and End to BottomEnd.

* Implementing keybindings in the ScrollView.

* Changing the PageLeft and PageRight keybindings.

* Fixing PageLeft and RightPage.

* Removing CleanUp command.

* Key bindings for TabView

* Keybindings for TableView

* Fixed unit tests for PageDown to correctly assign input focus to the TableView

* Fixes the CalculateLeftColumn method avoiding jump two columns on forward moving.

* Fixes #1525. Gives the same backspace behavior as TextView.

* Changes kill-to-start key to work on Linux too.

* Fixes SelectedStart, SelectedText and some cleaning.

* Implementing keybindings in TextField.

* Updated command names and merged as discussed with @BDisp

- Merged LeftItem and LeftChar to Left (same for Right).
- Also renamed Kill to Cut
- Added ScrollLeft / ScrollRight (and renamed ScrollLineUp to just ScrollUp

* Renamed Command.InsertChar to ToggleOverwriteMode and added Enable/Disable

* Removed 'Mode' suffix from toggle overwrite

* Allows navigation to outside a TextView if IsMdiContainer is true.

* Implementing keybindings in Toplevel.

* Fixing null reference exception.

* Changing to keys instances events instead static.

* Transferring the events to the Toplevel.

* Implementing keybindings in TextView.

* Removing static from the QuitKeyChanged and adding unit test.

* Replacing Added with the Initialized event.

* Ignore control characters and other special keys.

* Changing InvokeKeybindings to return Nullable bool and added two more keys to the Toplevel.

* Implementing keybindings in Autocomplete. I had to derive from View.

* Added keybindings menu item to UICatalog

* Added ClearBinding

* Implementing IAutocomplete, abstract Autocomplete and derived TextViewAutocomplete.

* Implementing keybindings in the TextValidateProvider

* Add keybinding to CellActivationKey.

* Fixing some formats.

* Add ObjectActivationKey to the keybindings.

* Made it much easier to implement abstract base `Autocomplete` in other views by moving methods up out of `TextViewAutocomplete` implementation

* Allowing Autocomplete to popup inside or outside the container.

* Fixes the cursor not being showing if the text length is equal to the view width.

* A unit test to prove the 4df5897.

* Removed unused method `GetCursorPosition` from Autocomplete

* Trimmed down implementation specific methods from IAutocomplete

* Fixed xmldoc comment tag

* Format Autocomplete on multiline and fixes wrap settings.

* Adding keys from a to z to avoid the Key.Space on ToString.

* Fixes the vertical position outside the container.

* Adding more key unit tests.

* Changing comment to upper case and proving that doesn't will breaking nothing.

* Replaces Pos.Bottom to Pos.AnchorEnd.

* Fixes popup on resizing.

* Should only using the Pos.Bottom to position outside the view.

* Fixes #1584

* Fixes https://github.com/migueldeicaza/gui.cs/issues/1584#issuecomment-1027987475

* Fixes some bugs with SelectedItem.

* Command must also return a nullable bool.

* Ensures updating the ComboBox text on leaving the control.

* Only with the nullable bool was possible to make the MoveUp and the MoveDown working.

* Added logging of which scenario failed in test

Co-authored-by: BDisp <[email protected]>
Thomas Nind 3 years ago
parent
commit
ea7981dc59
53 changed files with 6495 additions and 1837 deletions
  1. 59 3
      Terminal.Gui/Core/Application.cs
  2. 0 305
      Terminal.Gui/Core/Autocomplete.cs
  3. 642 0
      Terminal.Gui/Core/Autocomplete/Autocomplete.cs
  4. 114 0
      Terminal.Gui/Core/Autocomplete/IAutocomplete.cs
  5. 388 0
      Terminal.Gui/Core/Command.cs
  6. 104 1
      Terminal.Gui/Core/Event.cs
  7. 17 2
      Terminal.Gui/Core/TextFormatter.cs
  8. 147 69
      Terminal.Gui/Core/Toplevel.cs
  9. 188 1
      Terminal.Gui/Core/View.cs
  10. 65 28
      Terminal.Gui/Views/Button.cs
  11. 34 22
      Terminal.Gui/Views/Checkbox.cs
  12. 149 53
      Terminal.Gui/Views/ComboBox.cs
  13. 105 62
      Terminal.Gui/Views/DateField.cs
  14. 36 30
      Terminal.Gui/Views/GraphView.cs
  15. 74 66
      Terminal.Gui/Views/HexView.cs
  16. 44 39
      Terminal.Gui/Views/ListView.cs
  17. 62 66
      Terminal.Gui/Views/Menu.cs
  18. 97 63
      Terminal.Gui/Views/RadioGroup.cs
  19. 41 29
      Terminal.Gui/Views/ScrollView.cs
  20. 18 17
      Terminal.Gui/Views/TabView.cs
  21. 145 85
      Terminal.Gui/Views/TableView.cs
  22. 249 163
      Terminal.Gui/Views/TextField.cs
  23. 35 19
      Terminal.Gui/Views/TextValidateField.cs
  24. 827 492
      Terminal.Gui/Views/TextView.cs
  25. 103 60
      Terminal.Gui/Views/TimeField.cs
  26. 172 90
      Terminal.Gui/Views/TreeView.cs
  27. 199 0
      UICatalog/KeyBindingsDialog.cs
  28. 1 1
      UICatalog/Scenarios/BackgroundWorkerCollection.cs
  29. 18 18
      UICatalog/Scenarios/BordersComparisons.cs
  30. 20 1
      UICatalog/Scenarios/Text.cs
  31. 186 0
      UICatalog/Scenarios/TextViewAutocompletePopup.cs
  32. 17 0
      UICatalog/UICatalog.cs
  33. 9 0
      UnitTests/ApplicationTests.cs
  34. 108 10
      UnitTests/AutocompleteTests.cs
  35. 103 0
      UnitTests/ButtonTests.cs
  36. 63 0
      UnitTests/CheckboxTests.cs
  37. 183 17
      UnitTests/ComboBoxTests.cs
  38. 98 0
      UnitTests/DateFieldTests.cs
  39. 36 0
      UnitTests/FrameViewTests.cs
  40. 58 0
      UnitTests/HexViewTests.cs
  41. 34 1
      UnitTests/KeyTests.cs
  42. 88 0
      UnitTests/ListViewTests.cs
  43. 3 2
      UnitTests/MenuTests.cs
  44. 128 0
      UnitTests/PosTests.cs
  45. 116 0
      UnitTests/RadioGroupTests.cs
  46. 9 1
      UnitTests/ScenarioTests.cs
  47. 175 0
      UnitTests/ScrollViewTests.cs
  48. 6 9
      UnitTests/TableViewTests.cs
  49. 4 0
      UnitTests/TextFieldTests.cs
  50. 463 11
      UnitTests/TextViewTests.cs
  51. 98 0
      UnitTests/TimeFieldTests.cs
  52. 332 1
      UnitTests/ToplevelTests.cs
  53. 25 0
      UnitTests/ViewTests.cs

+ 59 - 3
Terminal.Gui/Core/Application.cs

@@ -140,18 +140,74 @@ namespace Terminal.Gui {
 			}
 		}
 
+		static Key alternateForwardKey = Key.PageDown | Key.CtrlMask;
+
 		/// <summary>
 		/// Alternative key to navigate forwards through all views. Ctrl+Tab is always used.
 		/// </summary>
-		public static Key AlternateForwardKey { get; set; } = Key.PageDown | Key.CtrlMask;
+		public static Key AlternateForwardKey {
+			get => alternateForwardKey;
+			set {
+				if (alternateForwardKey != value) {
+					var oldKey = alternateForwardKey;
+					alternateForwardKey = value;
+					OnAlternateForwardKeyChanged (oldKey);
+				}
+			}
+		}
+
+		static void OnAlternateForwardKeyChanged (Key oldKey)
+		{
+			foreach (var top in toplevels) {
+				top.OnAlternateForwardKeyChanged (oldKey);
+			}
+		}
+
+		static Key alternateBackwardKey = Key.PageUp | Key.CtrlMask;
+
 		/// <summary>
 		/// Alternative key to navigate backwards through all views. Shift+Ctrl+Tab is always used.
 		/// </summary>
-		public static Key AlternateBackwardKey { get; set; } = Key.PageUp | Key.CtrlMask;
+		public static Key AlternateBackwardKey {
+			get => alternateBackwardKey;
+			set {
+				if (alternateBackwardKey != value) {
+					var oldKey = alternateBackwardKey;
+					alternateBackwardKey = value;
+					OnAlternateBackwardKeyChanged (oldKey);
+				}
+			}
+		}
+
+		static void OnAlternateBackwardKeyChanged (Key oldKey)
+		{
+			foreach (var top in toplevels) {
+				top.OnAlternateBackwardKeyChanged (oldKey);
+			}
+		}
+
+		static Key quitKey = Key.Q | Key.CtrlMask;
+
 		/// <summary>
 		/// Gets or sets the key to quit the application.
 		/// </summary>
-		public static Key QuitKey { get; set; } = Key.Q | Key.CtrlMask;
+		public static Key QuitKey {
+			get => quitKey;
+			set {
+				if (quitKey != value) {
+					var oldKey = quitKey;
+					quitKey = value;
+					OnQuitKeyChanged (oldKey);
+				}
+			}
+		}
+
+		static void OnQuitKeyChanged (Key oldKey)
+		{
+			foreach (var top in toplevels) {
+				top.OnQuitKeyChanged (oldKey);
+			}
+		}
 
 		/// <summary>
 		/// The <see cref="MainLoop"/>  driver for the application

+ 0 - 305
Terminal.Gui/Core/Autocomplete.cs

@@ -1,305 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Text;
-using Rune = System.Rune;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Renders an overlay on another view at a given point that allows selecting
-	/// from a range of 'autocomplete' options.
-	/// </summary>
-	public class Autocomplete {
-
-		/// <summary>
-		/// The maximum width of the autocomplete dropdown
-		/// </summary>
-		public int MaxWidth { get; set; } = 10;
-
-		/// <summary>
-		/// The maximum number of visible rows in the autocomplete dropdown to render
-		/// </summary>
-		public int MaxHeight { get; set; } = 6;
-
-		/// <summary>
-		/// True if the autocomplete should be considered open and visible
-		/// </summary>
-		protected bool Visible { get; set; } = true;
-
-		/// <summary>
-		/// The strings that form the current list of suggestions to render
-		/// based on what the user has typed so far.
-		/// </summary>
-		public ReadOnlyCollection<string> Suggestions { get; protected set; } = new ReadOnlyCollection<string>(new string[0]);
-
-		/// <summary>
-		/// The full set of all strings that can be suggested.
-		/// </summary>
-		/// <returns></returns>
-		public List<string> AllSuggestions { get; set; } = new List<string>();
-
-		/// <summary>
-		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
-		/// </summary>
-		public int SelectedIdx { get; set; }
-
-		/// <summary>
-		/// When more suggestions are available than can be rendered the user
-		/// can scroll down the dropdown list.  This indicates how far down they
-		/// have gone
-		/// </summary>
-		public int ScrollOffset {get;set;}
-
-		/// <summary>
-		/// The colors to use to render the overlay.  Accessing this property before
-		/// the Application has been initialised will cause an error
-		/// </summary>
-		public ColorScheme ColorScheme { 
-			get
-			{
-				if(colorScheme == null)
-				{
-					colorScheme = Colors.Menu;
-				}
-				return colorScheme;
-			}
-		 	set
-			{
-				colorScheme = value;
-			}
-		}
-
-		private ColorScheme colorScheme;
-
-		/// <summary>
-		/// The key that the user must press to accept the currently selected autocomplete suggestion
-		/// </summary>
-		public Key SelectionKey { get; set; } = Key.Enter;
-
-		/// <summary>
-		/// The key that the user can press to close the currently popped autocomplete menu
-		/// </summary>
-		public Key CloseKey {get;set;} = Key.Esc;
-
-		/// <summary>
-		/// Renders the autocomplete dialog inside the given <paramref name="view"/> at the
-		/// given point.
-		/// </summary>
-		/// <param name="view">The view the overlay should be rendered into</param>
-		/// <param name="renderAt"></param>
-		public void RenderOverlay (View view, Point renderAt)
-		{
-			if (!Visible || !view.HasFocus || Suggestions.Count == 0) {
-				return;
-			}
-
-			view.Move (renderAt.X, renderAt.Y);
-
-			// don't overspill vertically
-			var height = Math.Min(view.Bounds.Height - renderAt.Y,MaxHeight);
-
-			var toRender = Suggestions.Skip(ScrollOffset).Take(height).ToArray();
-
-			if(toRender.Length == 0)
-			{
-				return;
-			}
-
-			var width = Math.Min(MaxWidth,toRender.Max(s=>s.Length));
-
-			// don't overspill horizontally
-			width = Math.Min(view.Bounds.Width - renderAt.X ,width);
-
-			for(int i=0;i<toRender.Length; i++) {
-
-				if(i ==  SelectedIdx - ScrollOffset) {
-					Application.Driver.SetAttribute (ColorScheme.Focus);
-				}
-				else {
-					Application.Driver.SetAttribute (ColorScheme.Normal);
-				}
-
-				view.Move (renderAt.X, renderAt.Y+i);
-
-				var text = TextFormatter.ClipOrPad(toRender[i],width);
-
-				Application.Driver.AddStr (text );
-			}
-		}
-
-		/// <summary>
-		/// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
-		/// </summary>
-		public void EnsureSelectedIdxIsValid()
-		{				
-			SelectedIdx = Math.Max (0,Math.Min (Suggestions.Count - 1, SelectedIdx));
-			
-			// if user moved selection up off top of current scroll window
-			if(SelectedIdx < ScrollOffset)
-			{
-				ScrollOffset = SelectedIdx;
-			}
-
-			// if user moved selection down past bottom of current scroll window
-			while(SelectedIdx >= ScrollOffset + MaxHeight ){
-				ScrollOffset++;
-			}
-		}
-
-		/// <summary>
-		/// Handle key events before <paramref name="hostControl"/> e.g. to make key events like
-		/// up/down apply to the autocomplete control instead of changing the cursor position in 
-		/// the underlying text view.
-		/// </summary>
-		/// <param name="hostControl"></param>
-		/// <param name="kb"></param>
-		/// <returns></returns>
-		public bool ProcessKey (TextView hostControl, KeyEvent kb)
-		{
-			if(IsWordChar((char)kb.Key))
-			{
-				Visible = true;
-			}
-
-			if(!Visible || Suggestions.Count == 0) {
-				return false;
-			}
-
-			if (kb.Key == Key.CursorDown) {
-				SelectedIdx++;
-				EnsureSelectedIdxIsValid();
-				hostControl.SetNeedsDisplay ();
-				return true;
-			}
-
-			if (kb.Key == Key.CursorUp) {
-				SelectedIdx--;
-				EnsureSelectedIdxIsValid();
-				hostControl.SetNeedsDisplay ();
-				return true;
-			}
-
-			if(kb.Key == SelectionKey && SelectedIdx >=0 && SelectedIdx < Suggestions.Count) {
-
-				var accepted = Suggestions [SelectedIdx];
-								
-				var typedSoFar = GetCurrentWord (hostControl) ?? "";
-				
-				if(typedSoFar.Length < accepted.Length) {
-
-					// delete the text
-					for(int i=0;i<typedSoFar.Length;i++)
-					{
-						hostControl.DeleteTextBackwards();
-					}
-
-					hostControl.InsertText (accepted);
-					return true;
-				}
-
-				return false;
-			}
-
-			if(kb.Key == CloseKey)
-			{
-				ClearSuggestions ();
-				Visible = false;
-				hostControl.SetNeedsDisplay();
-				return true;
-			}
-
-			return false;
-		}
-
-		/// <summary>
-		/// Clears <see cref="Suggestions"/>
-		/// </summary>
-		public void ClearSuggestions ()
-		{
-			Suggestions = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
-		}
-
-
-		/// <summary>
-		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
-		/// match with the current cursor position/text in the <paramref name="hostControl"/>
-		/// </summary>
-		/// <param name="hostControl">The text view that you want suggestions for</param>
-		public void GenerateSuggestions (TextView hostControl)
-		{
-			// if there is nothing to pick from
-			if(AllSuggestions.Count == 0) {
-				ClearSuggestions ();
-				return;
-			}
-
-			var currentWord = GetCurrentWord (hostControl); 
-
-			if(string.IsNullOrWhiteSpace(currentWord)) {
-				ClearSuggestions ();
-			}
-			else {
-				Suggestions = AllSuggestions.Where (o => 
-				o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
-				!o.Equals(currentWord,StringComparison.CurrentCultureIgnoreCase)
-				).ToList ().AsReadOnly();
-
-				EnsureSelectedIdxIsValid();
-			}
-		}
-
-		private string GetCurrentWord (TextView hostControl)
-		{
-			var currentLine = hostControl.GetCurrentLine ();
-			var cursorPosition = Math.Min (hostControl.CurrentColumn, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition);
-		}
-
-		private string IdxToWord (List<Rune> line, int idx)
-		{
-			StringBuilder sb = new StringBuilder ();
-
-			// do not generate suggestions if the cursor is positioned in the middle of a word
-			bool areMidWord;
-
-			if(idx == line.Count) {
-				// the cursor positioned at the very end of the line
-				areMidWord = false;
-			}
-			else {
-				// we are in the middle of a word if the cursor is over a letter/number
-				areMidWord = IsWordChar (line [idx]);
-			}
-
-			// if we are in the middle of a word then there is no way to autocomplete that word
-			if(areMidWord) {
-				return null;
-			}
-
-			// we are at the end of a word.  Work out what has been typed so far
-			while(idx-- > 0) {
-
-				if(IsWordChar(line [idx])) {
-					sb.Insert(0,(char)line [idx]);
-				}
-				else {
-					break;
-				}
-			}
-			return sb.ToString ();
-		}
-
-		/// <summary>
-		/// Return true if the given symbol should be considered part of a word
-		/// and can be contained in matches.  Base behaviour is to use <see cref="char.IsLetterOrDigit(char)"/>
-		/// </summary>
-		/// <param name="rune"></param>
-		/// <returns></returns>
-		public virtual bool IsWordChar (Rune rune)
-		{
-			return Char.IsLetterOrDigit ((char)rune);
-		}
-	}
-}

+ 642 - 0
Terminal.Gui/Core/Autocomplete/Autocomplete.cs

@@ -0,0 +1,642 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Renders an overlay on another view at a given point that allows selecting
+	/// from a range of 'autocomplete' options.
+	/// </summary>
+	public abstract class Autocomplete : IAutocomplete {
+
+		private class Popup : View {
+			Autocomplete autocomplete;
+
+			public Popup (Autocomplete autocomplete)
+			{
+				this.autocomplete = autocomplete;
+				CanFocus = true;
+				WantMousePositionReports = true;
+			}
+
+			public override Rect Frame {
+				get => base.Frame;
+				set {
+					base.Frame = value;
+					X = value.X;
+					Y = value.Y;
+					Width = value.Width;
+					Height = value.Height;
+				}
+			}
+
+			public override void Redraw (Rect bounds)
+			{
+				if (autocomplete.LastPopupPos == null) {
+					return;
+				}
+
+				autocomplete.RenderOverlay ((Point)autocomplete.LastPopupPos);
+			}
+
+			public override bool MouseEvent (MouseEvent mouseEvent)
+			{
+				return autocomplete.MouseEvent (mouseEvent);
+			}
+		}
+
+		private View top, popup;
+		private bool closed;
+		int toRenderLength;
+
+		private Point? LastPopupPos { get; set; }
+
+		private ColorScheme colorScheme;
+		private View hostControl;
+
+		/// <summary>
+		/// The host control to handle.
+		/// </summary>
+		public virtual View HostControl {
+			get => hostControl;
+			set {
+				hostControl = value;
+				top = hostControl.SuperView;
+				if (top != null) {
+					top.DrawContent += Top_DrawContent;
+					top.DrawContentComplete += Top_DrawContentComplete;
+					top.Removed += Top_Removed;
+				}
+			}
+		}
+
+		private void Top_Removed (View obj)
+		{
+			Visible = false;
+			ManipulatePopup ();
+		}
+
+		private void Top_DrawContentComplete (Rect obj)
+		{
+			ManipulatePopup ();
+		}
+
+		private void Top_DrawContent (Rect obj)
+		{
+			if (!closed) {
+				ReopenSuggestions ();
+			}
+			ManipulatePopup ();
+			if (Visible) {
+				top.BringSubviewToFront (popup);
+			}
+		}
+
+		private void ManipulatePopup ()
+		{
+			if (Visible && popup == null) {
+				popup = new Popup (this) {
+					Frame = Rect.Empty
+				};
+				top?.Add (popup);
+			}
+
+			if (!Visible && popup != null) {
+				top.Remove (popup);
+				popup.Dispose ();
+				popup = null;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets If the popup is displayed inside or outside the host limits.
+		/// </summary>
+		public bool PopupInsideContainer { get; set; } = true;
+
+		/// <summary>
+		/// The maximum width of the autocomplete dropdown
+		/// </summary>
+		public virtual int MaxWidth { get; set; } = 10;
+
+		/// <summary>
+		/// The maximum number of visible rows in the autocomplete dropdown to render
+		/// </summary>
+		public virtual int MaxHeight { get; set; } = 6;
+
+		/// <summary>
+		/// True if the autocomplete should be considered open and visible
+		/// </summary>
+		public virtual bool Visible { get; set; }
+
+		/// <summary>
+		/// The strings that form the current list of suggestions to render
+		/// based on what the user has typed so far.
+		/// </summary>
+		public virtual ReadOnlyCollection<string> Suggestions { get; set; } = new ReadOnlyCollection<string> (new string [0]);
+
+		/// <summary>
+		/// The full set of all strings that can be suggested.
+		/// </summary>
+		/// <returns></returns>
+		public virtual List<string> AllSuggestions { get; set; } = new List<string> ();
+
+		/// <summary>
+		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
+		/// </summary>
+		public virtual int SelectedIdx { get; set; }
+
+		/// <summary>
+		/// When more suggestions are available than can be rendered the user
+		/// can scroll down the dropdown list.  This indicates how far down they
+		/// have gone
+		/// </summary>
+		public virtual int ScrollOffset { get; set; }
+
+		/// <summary>
+		/// The colors to use to render the overlay.  Accessing this property before
+		/// the Application has been initialized will cause an error
+		/// </summary>
+		public virtual ColorScheme ColorScheme {
+			get {
+				if (colorScheme == null) {
+					colorScheme = Colors.Menu;
+				}
+				return colorScheme;
+			}
+			set {
+				colorScheme = value;
+			}
+		}
+
+		/// <summary>
+		/// The key that the user must press to accept the currently selected autocomplete suggestion
+		/// </summary>
+		public virtual Key SelectionKey { get; set; } = Key.Enter;
+
+		/// <summary>
+		/// The key that the user can press to close the currently popped autocomplete menu
+		/// </summary>
+		public virtual Key CloseKey { get; set; } = Key.Esc;
+
+		/// <summary>
+		/// The key that the user can press to reopen the currently popped autocomplete menu
+		/// </summary>
+		public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask;
+
+		/// <summary>
+		/// Renders the autocomplete dialog inside or outside the given <see cref="HostControl"/> at the
+		/// given point.
+		/// </summary>
+		/// <param name="renderAt"></param>
+		public virtual void RenderOverlay (Point renderAt)
+		{
+			if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) {
+				LastPopupPos = null;
+				Visible = false;
+				return;
+			}
+
+			LastPopupPos = renderAt;
+
+			int height, width;
+
+			if (PopupInsideContainer) {
+				// don't overspill vertically
+				height = Math.Min (HostControl.Bounds.Height - renderAt.Y, MaxHeight);
+				// There is no space below, lets see if can popup on top
+				if (height < Suggestions.Count && HostControl.Bounds.Height - renderAt.Y >= height) {
+					// Verifies that the upper limit available is greater than the lower limit
+					if (renderAt.Y > HostControl.Bounds.Height - renderAt.Y) {
+						renderAt.Y = Math.Max (renderAt.Y - Math.Min (Suggestions.Count + 1, MaxHeight + 1), 0);
+						height = Math.Min (Math.Min (Suggestions.Count, MaxHeight), LastPopupPos.Value.Y - 1);
+					}
+				}
+			} else {
+				// don't overspill vertically
+				height = Math.Min (Math.Min (top.Bounds.Height - HostControl.Frame.Bottom, MaxHeight), Suggestions.Count);
+				// There is no space below, lets see if can popup on top
+				if (height < Suggestions.Count && HostControl.Frame.Y - top.Frame.Y >= height) {
+					// Verifies that the upper limit available is greater than the lower limit
+					if (HostControl.Frame.Y > top.Bounds.Height - HostControl.Frame.Y) {
+						renderAt.Y = Math.Max (HostControl.Frame.Y - Math.Min (Suggestions.Count, MaxHeight), 0);
+						height = Math.Min (Math.Min (Suggestions.Count, MaxHeight), HostControl.Frame.Y);
+					}
+				} else {
+					renderAt.Y = HostControl.Frame.Bottom;
+				}
+			}
+
+			if (ScrollOffset > Suggestions.Count - height) {
+				ScrollOffset = 0;
+			}
+			var toRender = Suggestions.Skip (ScrollOffset).Take (height).ToArray ();
+			toRenderLength = toRender.Length;
+
+			if (toRender.Length == 0) {
+				return;
+			}
+
+			width = Math.Min (MaxWidth, toRender.Max (s => s.Length));
+
+			if (PopupInsideContainer) {
+				// don't overspill horizontally, let's see if can be displayed on the left
+				if (width > HostControl.Bounds.Width - renderAt.X) {
+					// Verifies that the left limit available is greater than the right limit
+					if (renderAt.X > HostControl.Bounds.Width - renderAt.X) {
+						renderAt.X -= Math.Min (width, LastPopupPos.Value.X);
+						width = Math.Min (width, LastPopupPos.Value.X);
+					} else {
+						width = Math.Min (width, HostControl.Bounds.Width - renderAt.X);
+					}
+				}
+			} else {
+				// don't overspill horizontally, let's see if can be displayed on the left
+				if (width > top.Bounds.Width - (renderAt.X + HostControl.Frame.X)) {
+					// Verifies that the left limit available is greater than the right limit
+					if (renderAt.X + HostControl.Frame.X > top.Bounds.Width - (renderAt.X + HostControl.Frame.X)) {
+						renderAt.X -= Math.Min (width, LastPopupPos.Value.X);
+						width = Math.Min (width, LastPopupPos.Value.X);
+					} else {
+						width = Math.Min (width, top.Bounds.Width - renderAt.X);
+					}
+				}
+			}
+
+			if (PopupInsideContainer) {
+				popup.Frame = new Rect (
+					new Point (HostControl.Frame.X + renderAt.X, HostControl.Frame.Y + renderAt.Y),
+					new Size (width, height));
+			} else {
+				popup.Frame = new Rect (
+					new Point (HostControl.Frame.X + renderAt.X, renderAt.Y),
+					new Size (width, height));
+			}
+
+			popup.Move (0, 0);
+
+			for (int i = 0; i < toRender.Length; i++) {
+
+				if (i == SelectedIdx - ScrollOffset) {
+					Application.Driver.SetAttribute (ColorScheme.Focus);
+				} else {
+					Application.Driver.SetAttribute (ColorScheme.Normal);
+				}
+
+				popup.Move (0, i);
+
+				var text = TextFormatter.ClipOrPad (toRender [i], width);
+
+				Application.Driver.AddStr (text);
+			}
+		}
+
+		/// <summary>
+		/// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
+		/// </summary>
+		public virtual void EnsureSelectedIdxIsValid ()
+		{
+			SelectedIdx = Math.Max (0, Math.Min (Suggestions.Count - 1, SelectedIdx));
+
+			// if user moved selection up off top of current scroll window
+			if (SelectedIdx < ScrollOffset) {
+				ScrollOffset = SelectedIdx;
+			}
+
+			// if user moved selection down past bottom of current scroll window
+			while (toRenderLength > 0 && SelectedIdx >= ScrollOffset + toRenderLength) {
+				ScrollOffset++;
+			}
+		}
+
+		/// <summary>
+		/// Handle key events before <see cref="HostControl"/> e.g. to make key events like
+		/// up/down apply to the autocomplete control instead of changing the cursor position in
+		/// the underlying text view.
+		/// </summary>
+		/// <param name="kb">The key event.</param>
+		/// <returns><c>true</c>if the key can be handled <c>false</c>otherwise.</returns>
+		public virtual bool ProcessKey (KeyEvent kb)
+		{
+			if (IsWordChar ((char)kb.Key)) {
+				Visible = true;
+				closed = false;
+			}
+
+			if (kb.Key == Reopen) {
+				return ReopenSuggestions ();
+			}
+
+			if (closed || Suggestions.Count == 0) {
+				Visible = false;
+				return false;
+			}
+
+			if (kb.Key == Key.CursorDown) {
+				MoveDown ();
+				return true;
+			}
+
+			if (kb.Key == Key.CursorUp) {
+				MoveUp ();
+				return true;
+			}
+
+			if (kb.Key == SelectionKey) {
+				return Select ();
+			}
+
+			if (kb.Key == CloseKey) {
+				Close ();
+				return true;
+			}
+
+			return false;
+		}
+
+		/// <summary>
+		/// Handle mouse events before <see cref="HostControl"/> e.g. to make mouse events like
+		/// report/click apply to the autocomplete control instead of changing the cursor position in
+		/// the underlying text view.
+		/// </summary>
+		/// <param name="me">The mouse event.</param>
+		/// <param name="fromHost">If was called from the popup or from the host.</param>
+		/// <returns><c>true</c>if the mouse can be handled <c>false</c>otherwise.</returns>
+		public virtual bool MouseEvent (MouseEvent me, bool fromHost = false)
+		{
+			if (fromHost) {
+				GenerateSuggestions ();
+				if (Visible && Suggestions.Count == 0) {
+					Visible = false;
+					HostControl?.SetNeedsDisplay ();
+					return true;
+				} else if (!Visible && Suggestions.Count > 0) {
+					Visible = true;
+					HostControl?.SetNeedsDisplay ();
+					Application.UngrabMouse ();
+					return false;
+				} else {
+					// not in the popup
+					if (Visible && HostControl != null) {
+						Visible = false;
+						closed = false;
+					}
+					HostControl?.SetNeedsDisplay ();
+				}
+				return false;
+			}
+
+			if (popup == null || Suggestions.Count == 0) {
+				ManipulatePopup ();
+				return false;
+			}
+
+			if (me.Flags == MouseFlags.ReportMousePosition) {
+				RenderSelectedIdxByMouse (me);
+				return true;
+			}
+
+			if (me.Flags == MouseFlags.Button1Clicked) {
+				SelectedIdx = me.Y - ScrollOffset;
+				return Select ();
+			}
+
+			if (me.Flags == MouseFlags.WheeledDown) {
+				MoveDown ();
+				return true;
+			}
+
+			if (me.Flags == MouseFlags.WheeledUp) {
+				MoveUp ();
+				return true;
+			}
+
+			return false;
+		}
+
+		/// <summary>
+		/// Render the current selection in the Autocomplete context menu by the mouse reporting.
+		/// </summary>
+		/// <param name="me"></param>
+		protected void RenderSelectedIdxByMouse (MouseEvent me)
+		{
+			if (SelectedIdx != me.Y - ScrollOffset) {
+				SelectedIdx = me.Y - ScrollOffset;
+				if (LastPopupPos != null) {
+					RenderOverlay ((Point)LastPopupPos);
+				}
+			}
+		}
+
+		/// <summary>
+		/// Clears <see cref="Suggestions"/>
+		/// </summary>
+		public virtual void ClearSuggestions ()
+		{
+			Suggestions = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
+		}
+
+
+		/// <summary>
+		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
+		/// match with the current cursor position/text in the <see cref="HostControl"/>
+		/// </summary>
+		public virtual void GenerateSuggestions ()
+		{
+			// if there is nothing to pick from
+			if (AllSuggestions.Count == 0) {
+				ClearSuggestions ();
+				return;
+			}
+
+			var currentWord = GetCurrentWord ();
+
+			if (string.IsNullOrWhiteSpace (currentWord)) {
+				ClearSuggestions ();
+			} else {
+				Suggestions = AllSuggestions.Where (o =>
+				o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
+				!o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase)
+				).ToList ().AsReadOnly ();
+
+				EnsureSelectedIdxIsValid ();
+			}
+		}
+
+
+		/// <summary>
+		/// Return true if the given symbol should be considered part of a word
+		/// and can be contained in matches.  Base behavior is to use <see cref="char.IsLetterOrDigit(char)"/>
+		/// </summary>
+		/// <param name="rune"></param>
+		/// <returns></returns>
+		public virtual bool IsWordChar (Rune rune)
+		{
+			return Char.IsLetterOrDigit ((char)rune);
+		}
+
+		/// <summary>
+		/// Completes the autocomplete selection process.  Called when user hits the <see cref="SelectionKey"/>.
+		/// </summary>
+		/// <returns></returns>
+		protected bool Select ()
+		{
+			if (SelectedIdx >= 0 && SelectedIdx < Suggestions.Count) {
+				var accepted = Suggestions [SelectedIdx];
+
+				return InsertSelection (accepted);
+
+			}
+
+			return false;
+		}
+
+		/// <summary>
+		/// Called when the user confirms a selection at the current cursor location in
+		/// the <see cref="HostControl"/>.  The <paramref name="accepted"/> string
+		/// is the full autocomplete word to be inserted.  Typically a host will have to
+		/// remove some characters such that the <paramref name="accepted"/> string 
+		/// completes the word instead of simply being appended.
+		/// </summary>
+		/// <param name="accepted"></param>
+		/// <returns>True if the insertion was possible otherwise false</returns>
+		protected virtual bool InsertSelection (string accepted)
+		{
+			var typedSoFar = GetCurrentWord () ?? "";
+
+			if (typedSoFar.Length < accepted.Length) {
+
+				// delete the text
+				for (int i = 0; i < typedSoFar.Length; i++) {
+					DeleteTextBackwards ();
+				}
+
+				InsertText (accepted);
+				return true;
+			}
+
+			return false;
+		}
+
+		/// <summary>
+		/// Returns the currently selected word from the <see cref="HostControl"/>.
+		/// <para>
+		/// When overriding this method views can make use of <see cref="IdxToWord(List{Rune}, int)"/>
+		/// </para>
+		/// </summary>
+		/// <returns></returns>
+		protected abstract string GetCurrentWord ();
+
+		/// <summary>
+		/// <para>
+		/// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/> 
+		/// or null.  Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
+		/// </para>
+		/// 
+		/// <para>Use this method to determine whether autocomplete should be shown when the cursor is at
+		/// a given point in a line and to get the word from which suggestions should be generated.</para>
+		/// </summary>
+		/// <param name="line"></param>
+		/// <param name="idx"></param>
+		/// <returns></returns>
+		protected virtual string IdxToWord (List<Rune> line, int idx)
+		{
+			StringBuilder sb = new StringBuilder ();
+
+			// do not generate suggestions if the cursor is positioned in the middle of a word
+			bool areMidWord;
+
+			if (idx == line.Count) {
+				// the cursor positioned at the very end of the line
+				areMidWord = false;
+			} else {
+				// we are in the middle of a word if the cursor is over a letter/number
+				areMidWord = IsWordChar (line [idx]);
+			}
+
+			// if we are in the middle of a word then there is no way to autocomplete that word
+			if (areMidWord) {
+				return null;
+			}
+
+			// we are at the end of a word.  Work out what has been typed so far
+			while (idx-- > 0) {
+
+				if (IsWordChar (line [idx])) {
+					sb.Insert (0, (char)line [idx]);
+				} else {
+					break;
+				}
+			}
+			return sb.ToString ();
+		}
+
+		/// <summary>
+		/// Deletes the text backwards before insert the selected text in the <see cref="HostControl"/>.
+		/// </summary>
+		protected abstract void DeleteTextBackwards ();
+
+		/// <summary>
+		/// Inser the selected text in the <see cref="HostControl"/>.
+		/// </summary>
+		/// <param name="accepted"></param>
+		protected abstract void InsertText (string accepted);
+
+		/// <summary>
+		/// Closes the Autocomplete context menu if it is showing and <see cref="ClearSuggestions"/>
+		/// </summary>
+		protected void Close ()
+		{
+			ClearSuggestions ();
+			Visible = false;
+			closed = true;
+			HostControl?.SetNeedsDisplay ();
+			ManipulatePopup ();
+		}
+
+		/// <summary>
+		/// Moves the selection in the Autocomplete context menu up one
+		/// </summary>
+		protected void MoveUp ()
+		{
+			SelectedIdx--;
+			if (SelectedIdx < 0) {
+				SelectedIdx = Suggestions.Count - 1;
+			}
+			EnsureSelectedIdxIsValid ();
+			HostControl?.SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Moves the selection in the Autocomplete context menu down one
+		/// </summary>
+		protected void MoveDown ()
+		{
+			SelectedIdx++;
+			if (SelectedIdx > Suggestions.Count - 1) {
+				SelectedIdx = 0;
+			}
+			EnsureSelectedIdxIsValid ();
+			HostControl?.SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Reopen the popup after it has been closed.
+		/// </summary>
+		/// <returns></returns>
+		protected bool ReopenSuggestions ()
+		{
+			GenerateSuggestions ();
+			if (Suggestions.Count > 0) {
+				Visible = true;
+				closed = false;
+				HostControl?.SetNeedsDisplay ();
+				return true;
+			}
+			return false;
+		}
+	}
+}

+ 114 - 0
Terminal.Gui/Core/Autocomplete/IAutocomplete.cs

@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Renders an overlay on another view at a given point that allows selecting
+	/// from a range of 'autocomplete' options.
+	/// </summary>
+	public interface IAutocomplete {
+
+		/// <summary>
+		/// The host control that will use autocomplete.
+		/// </summary>
+		View HostControl { get; set; }
+
+		/// <summary>
+		/// Gets or sets where the popup will be displayed.
+		/// </summary>
+		bool PopupInsideContainer { get; set; }
+
+		/// <summary>
+		/// The maximum width of the autocomplete dropdown
+		/// </summary>
+		int MaxWidth { get; set; }
+
+		/// <summary>
+		/// The maximum number of visible rows in the autocomplete dropdown to render
+		/// </summary>
+		int MaxHeight { get; set; }
+
+		/// <summary>
+		/// True if the autocomplete should be considered open and visible
+		/// </summary>
+		bool Visible { get; set; }
+
+		/// <summary>
+		/// The strings that form the current list of suggestions to render
+		/// based on what the user has typed so far.
+		/// </summary>
+		ReadOnlyCollection<string> Suggestions { get; set; }
+
+		/// <summary>
+		/// The full set of all strings that can be suggested.
+		/// </summary>
+		List<string> AllSuggestions { get; set; }
+
+		/// <summary>
+		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
+		/// </summary>
+		int SelectedIdx { get; set; }
+
+		/// <summary>
+		/// The colors to use to render the overlay.  Accessing this property before
+		/// the Application has been initialized will cause an error
+		/// </summary>
+		ColorScheme ColorScheme { get; set; }
+
+		/// <summary>
+		/// The key that the user must press to accept the currently selected autocomplete suggestion
+		/// </summary>
+		Key SelectionKey { get; set; }
+
+		/// <summary>
+		/// The key that the user can press to close the currently popped autocomplete menu
+		/// </summary>
+		Key CloseKey { get; set; }
+
+		/// <summary>
+		/// The key that the user can press to reopen the currently popped autocomplete menu
+		/// </summary>
+		Key Reopen { get; set; }
+
+		/// <summary>
+		/// Renders the autocomplete dialog inside the given <see cref="HostControl"/> at the
+		/// given point.
+		/// </summary>
+		/// <param name="renderAt"></param>
+		void RenderOverlay (Point renderAt);
+
+
+		/// <summary>
+		/// Handle key events before <see cref="HostControl"/> e.g. to make key events like
+		/// up/down apply to the autocomplete control instead of changing the cursor position in
+		/// the underlying text view.
+		/// </summary>
+		/// <param name="kb">The key event.</param>
+		/// <returns><c>true</c>if the key can be handled <c>false</c>otherwise.</returns>
+		bool ProcessKey (KeyEvent kb);
+
+		/// <summary>
+		/// Handle mouse events before <see cref="HostControl"/> e.g. to make mouse events like
+		/// report/click apply to the autocomplete control instead of changing the cursor position in
+		/// the underlying text view.
+		/// </summary>
+		/// <param name="me">The mouse event.</param>
+		/// <param name="fromHost">If was called from the popup or from the host.</param>
+		/// <returns><c>true</c>if the mouse can be handled <c>false</c>otherwise.</returns>
+		bool MouseEvent (MouseEvent me, bool fromHost = false);
+
+		/// <summary>
+		/// Clears <see cref="Suggestions"/>
+		/// </summary>
+		void ClearSuggestions ();
+
+		/// <summary>
+		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
+		/// match with the current cursor position/text in the <see cref="HostControl"/>.
+		/// </summary>
+		void GenerateSuggestions ();
+	}
+}

+ 388 - 0
Terminal.Gui/Core/Command.cs

@@ -0,0 +1,388 @@
+// These classes use a keybinding system based on the design implemented in Scintilla.Net which is an MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs
+
+using System;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Actions which can be performed by the application or bound to keys in a <see cref="View"/> control.
+	/// </summary>
+	public enum Command {
+
+		/// <summary>
+		/// Moves the caret down one line.
+		/// </summary>
+		LineDown,
+
+		/// <summary>
+		/// Extends the selection down one line.
+		/// </summary>
+		LineDownExtend,
+
+		/// <summary>
+		/// Moves the caret down to the last child node of the branch that holds the current selection
+		/// </summary>
+		LineDownToLastBranch,
+
+		/// <summary>
+		/// Scrolls down one line (without changing the selection).
+		/// </summary>
+		ScrollDown,
+
+		// --------------------------------------------------------------------
+
+		/// <summary>
+		/// Moves the caret up one line.
+		/// </summary>
+		LineUp,
+
+		/// <summary>
+		/// Extends the selection up one line.
+		/// </summary>
+		LineUpExtend,
+
+		/// <summary>
+		/// Moves the caret up to the first child node of the branch that holds the current selection
+		/// </summary>
+		LineUpToFirstBranch,
+
+		/// <summary>
+		/// Scrolls up one line (without changing the selection).
+		/// </summary>
+		ScrollUp,
+
+		/// <summary>
+		/// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// </summary>
+		Left,
+
+		/// <summary>
+		/// Scrolls one character to the left
+		/// </summary>
+		ScrollLeft,
+
+		/// <summary>
+		/// Extends the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// </summary>
+		LeftExtend,
+
+		/// <summary>
+		/// Moves the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// </summary>
+		Right,
+
+		/// <summary>
+		/// Scrolls one character to the right.
+		/// </summary>
+		ScrollRight,
+
+		/// <summary>
+		/// Extends the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc.
+		/// </summary>
+		RightExtend,
+
+		/// <summary>
+		/// Moves the caret to the start of the previous word.
+		/// </summary>
+		WordLeft,
+
+		/// <summary>
+		/// Extends the selection to the start of the previous word.
+		/// </summary>
+		WordLeftExtend,
+
+		/// <summary>
+		/// Moves the caret to the start of the next word.
+		/// </summary>
+		WordRight,
+
+		/// <summary>
+		/// Extends the selection to the start of the next word.
+		/// </summary>
+		WordRightExtend,
+
+		/// <summary>
+		/// Deletes and copies to the clipboard the characters from the current position to the end of the line.
+		/// </summary>
+		CutToEndLine,
+
+		/// <summary>
+		/// Deletes and copies to the clipboard the characters from the current position to the start of the line.
+		/// </summary>
+		CutToStartLine,
+
+		/// <summary>
+		/// Deletes the characters forwards.
+		/// </summary>
+		KillWordForwards,
+
+		/// <summary>
+		/// Deletes the characters backwards.
+		/// </summary>
+		KillWordBackwards,
+
+		/// <summary>
+		/// Toggles overwrite mode such that newly typed text overwrites the text that is
+		/// already there (typically associated with the Insert key).
+		/// </summary>
+		ToggleOverwrite,
+
+
+		/// <summary>
+		/// Enables overwrite mode such that newly typed text overwrites the text that is
+		/// already there (typically associated with the Insert key).
+		/// </summary>
+		EnableOverwrite,
+
+		/// <summary>
+		/// Disables overwrite mode (<see cref="EnableOverwrite"/>)
+		/// </summary>
+		DisableOverwrite,
+
+		/// <summary>
+		/// Move the page down.
+		/// </summary>
+		PageDown,
+
+		/// <summary>
+		/// Move the page down increase selection area to cover revealed objects/characters.
+		/// </summary>
+		PageDownExtend,
+
+		/// <summary>
+		/// Move the page up.
+		/// </summary>
+		PageUp,
+
+		/// <summary>
+		/// Move the page up increase selection area to cover revealed objects/characters.
+		/// </summary>
+		PageUpExtend,
+
+		/// <summary>
+		/// Moves to top begin.
+		/// </summary>
+		TopHome,
+
+		/// <summary>
+		/// Extends the selection to the top begin.
+		/// </summary>
+		TopHomeExtend,
+
+		/// <summary>
+		/// Moves to bottom end.
+		/// </summary>
+		BottomEnd,
+
+		/// <summary>
+		/// Extends the selection to the bottom end.
+		/// </summary>
+		BottomEndExtend,
+
+		/// <summary>
+		/// Open selected item.
+		/// </summary>
+		OpenSelectedItem,
+
+		/// <summary>
+		/// Toggle the checked state.
+		/// </summary>
+		ToggleChecked,
+
+		/// <summary>
+		/// Accepts the current state (e.g. selection, button press etc)
+		/// </summary>
+		Accept,
+
+		/// <summary>
+		/// Toggles the Expanded or collapsed state of a a list or item (with subitems)
+		/// </summary>
+		ToggleExpandCollapse,
+
+		/// <summary>
+		/// Expands a list or item (with subitems)
+		/// </summary>
+		Expand,
+
+		/// <summary>
+		/// Recursively Expands all child items and their child items (if any)
+		/// </summary>
+		ExpandAll,
+
+		/// <summary>
+		/// Collapses a list or item (with subitems)
+		/// </summary>
+		Collapse,
+
+		/// <summary>
+		/// Recursively collapses a list items of their children (if any)
+		/// </summary>
+		CollapseAll,
+
+		/// <summary>
+		/// Cancels any current temporary states on the control e.g. expanding
+		/// a combo list
+		/// </summary>
+		Cancel,
+
+		/// <summary>
+		/// Unix emulation
+		/// </summary>
+		UnixEmulation,
+
+		/// <summary>
+		/// Deletes the character on the right.
+		/// </summary>
+		DeleteCharRight,
+
+		/// <summary>
+		/// Deletes the character on the left.
+		/// </summary>
+		DeleteCharLeft,
+
+		/// <summary>
+		/// Selects all objects in the control
+		/// </summary>
+		SelectAll,
+
+		/// <summary>
+		/// Moves the cursor to the start of line.
+		/// </summary>
+		StartOfLine,
+
+		/// <summary>
+		/// Extends the selection to the start of line.
+		/// </summary>
+		StartOfLineExtend,
+
+		/// <summary>
+		/// Moves the cursor to the end of line.
+		/// </summary>
+		EndOfLine,
+
+		/// <summary>
+		/// Extends the selection to the end of line.
+		/// </summary>
+		EndOfLineExtend,
+
+		/// <summary>
+		/// Moves the cursor to the top of page.
+		/// </summary>
+		StartOfPage,
+
+		/// <summary>
+		/// Moves the cursor to the bottom of page.
+		/// </summary>
+		EndOfPage,
+
+		/// <summary>
+		/// Moves to the left page.
+		/// </summary>
+		PageLeft,
+
+		/// <summary>
+		/// Moves to the right page.
+		/// </summary>
+		PageRight,
+
+		/// <summary>
+		/// Moves to the left begin.
+		/// </summary>
+		LeftHome,
+
+		/// <summary>
+		/// Extends the selection to the left begin.
+		/// </summary>
+		LeftHomeExtend,
+
+		/// <summary>
+		/// Moves to the right end.
+		/// </summary>
+		RightEnd,
+
+		/// <summary>
+		/// Extends the selection to the right end.
+		/// </summary>
+		RightEndExtend,
+
+		/// <summary>
+		/// Undo changes.
+		/// </summary>
+		Undo,
+
+		/// <summary>
+		/// Redo changes.
+		/// </summary>
+		Redo,
+
+		/// <summary>
+		/// Copies the current selection.
+		/// </summary>
+		Copy,
+
+		/// <summary>
+		/// Cuts the current selection.
+		/// </summary>
+		Cut,
+
+		/// <summary>
+		/// Pastes the current selection.
+		/// </summary>
+		Paste,
+
+		/// <summary>
+		/// Quit a toplevel.
+		/// </summary>
+		QuitToplevel,
+
+		/// <summary>
+		/// Suspend a application (used on Linux).
+		/// </summary>
+		Suspend,
+
+		/// <summary>
+		/// Moves focus to the next view.
+		/// </summary>
+		NextView,
+
+		/// <summary>
+		/// Moves focuss to the previous view.
+		/// </summary>
+		PreviousView,
+
+		/// <summary>
+		/// Moves focus to the next view or toplevel (case of Mdi).
+		/// </summary>
+		NextViewOrTop,
+
+		/// <summary>
+		/// Moves focus to the next previous or toplevel (case of Mdi).
+		/// </summary>
+		PreviousViewOrTop,
+
+		/// <summary>
+		/// Refresh the application.
+		/// </summary>
+		Refresh,
+
+		/// <summary>
+		/// Toggles the extended selection.
+		/// </summary>
+		ToggleExtend,
+
+		/// <summary>
+		/// Inserts a new line.
+		/// </summary>
+		NewLine,
+
+		/// <summary>
+		/// Inserts a tab.
+		/// </summary>
+		Tab,
+
+		/// <summary>
+		/// Inserts a shift tab.
+		/// </summary>
+		BackTab
+	}
+}

+ 104 - 1
Terminal.Gui/Core/Event.cs

@@ -237,7 +237,110 @@ namespace Terminal.Gui {
 		/// The key code for the user pressing Shift-Z
 		/// </summary>
 		Z,
-
+		/// <summary>
+		/// The key code for the user pressing A
+		/// </summary>
+		a = 97,
+		/// <summary>
+		/// The key code for the user pressing B
+		/// </summary>
+		b,
+		/// <summary>
+		/// The key code for the user pressing C
+		/// </summary>
+		c,
+		/// <summary>
+		/// The key code for the user pressing D
+		/// </summary>
+		d,
+		/// <summary>
+		/// The key code for the user pressing E
+		/// </summary>
+		e,
+		/// <summary>
+		/// The key code for the user pressing F
+		/// </summary>
+		f,
+		/// <summary>
+		/// The key code for the user pressing G
+		/// </summary>
+		g,
+		/// <summary>
+		/// The key code for the user pressing H
+		/// </summary>
+		h,
+		/// <summary>
+		/// The key code for the user pressing I
+		/// </summary>
+		i,
+		/// <summary>
+		/// The key code for the user pressing J
+		/// </summary>
+		j,
+		/// <summary>
+		/// The key code for the user pressing K
+		/// </summary>
+		k,
+		/// <summary>
+		/// The key code for the user pressing L
+		/// </summary>
+		l,
+		/// <summary>
+		/// The key code for the user pressing M
+		/// </summary>
+		m,
+		/// <summary>
+		/// The key code for the user pressing N
+		/// </summary>
+		n,
+		/// <summary>
+		/// The key code for the user pressing O
+		/// </summary>
+		o,
+		/// <summary>
+		/// The key code for the user pressing P
+		/// </summary>
+		p,
+		/// <summary>
+		/// The key code for the user pressing Q
+		/// </summary>
+		q,
+		/// <summary>
+		/// The key code for the user pressing R
+		/// </summary>
+		r,
+		/// <summary>
+		/// The key code for the user pressing S
+		/// </summary>
+		s,
+		/// <summary>
+		/// The key code for the user pressing T
+		/// </summary>
+		t,
+		/// <summary>
+		/// The key code for the user pressing U
+		/// </summary>
+		u,
+		/// <summary>
+		/// The key code for the user pressing V
+		/// </summary>
+		v,
+		/// <summary>
+		/// The key code for the user pressing W
+		/// </summary>
+		w,
+		/// <summary>
+		/// The key code for the user pressing X
+		/// </summary>
+		x,
+		/// <summary>
+		/// The key code for the user pressing Y
+		/// </summary>
+		y,
+		/// <summary>
+		/// The key code for the user pressing Z
+		/// </summary>
+		z,
 		/// <summary>
 		/// The key code for the user pressing the delete key.
 		/// </summary>

+ 17 - 2
Terminal.Gui/Core/TextFormatter.cs

@@ -123,6 +123,11 @@ namespace Terminal.Gui {
 		Key hotKey;
 		Size size;
 
+		/// <summary>
+		/// Event invoked when the <see cref="HotKey"/> is changed.
+		/// </summary>
+		public event Action<Key> HotKeyChanged;
+
 		/// <summary>
 		///   The text to be displayed. This text is never modified.
 		/// </summary>
@@ -270,7 +275,16 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the hotkey. Will be an upper case letter or digit.
 		/// </summary>
-		public Key HotKey { get => hotKey; internal set => hotKey = value; }
+		public Key HotKey {
+			get => hotKey;
+			internal set {
+				if (hotKey != value) {
+					var oldKey = hotKey;
+					hotKey = value;
+					HotKeyChanged?.Invoke (oldKey);
+				}
+			}
+		}
 
 		/// <summary>
 		/// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of <c>0x100000</c> causes
@@ -304,7 +318,8 @@ namespace Terminal.Gui {
 
 				if (NeedsFormat) {
 					var shown_text = text;
-					if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) {
+					if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out Key newHotKey)) {
+						HotKey = newHotKey;
 						shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier);
 						shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos);
 					}

+ 147 - 69
Terminal.Gui/Core/Toplevel.cs

@@ -197,6 +197,85 @@ namespace Terminal.Gui {
 		void Initialize ()
 		{
 			ColorScheme = Colors.TopLevel;
+
+			// Things this view knows how to do
+			AddCommand (Command.QuitToplevel, () => { QuitToplevel (); return true; });
+			AddCommand (Command.Suspend, () => { Driver.Suspend (); ; return true; });
+			AddCommand (Command.NextView, () => { MoveNextView (); return true; });
+			AddCommand (Command.PreviousView, () => { MovePreviousView (); return true; });
+			AddCommand (Command.NextViewOrTop, () => { MoveNextViewOrTop (); return true; });
+			AddCommand (Command.PreviousViewOrTop, () => { MovePreviousViewOrTop (); return true; });
+			AddCommand (Command.Refresh, () => { Application.Refresh (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Application.QuitKey, Command.QuitToplevel);
+			AddKeyBinding (Key.Z | Key.CtrlMask, Command.Suspend);
+
+			AddKeyBinding (Key.Tab, Command.NextView);
+
+			AddKeyBinding (Key.CursorRight, Command.NextView);
+			AddKeyBinding (Key.F | Key.CtrlMask, Command.NextView);
+
+			AddKeyBinding (Key.CursorDown, Command.NextView);
+			AddKeyBinding (Key.I | Key.CtrlMask, Command.NextView); // Unix
+
+			AddKeyBinding (Key.BackTab | Key.ShiftMask, Command.PreviousView);
+			AddKeyBinding (Key.CursorLeft, Command.PreviousView);
+			AddKeyBinding (Key.CursorUp, Command.PreviousView);
+			AddKeyBinding (Key.B | Key.CtrlMask, Command.PreviousView);
+
+			AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextViewOrTop);
+			AddKeyBinding (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix
+
+			AddKeyBinding (Key.Tab | Key.ShiftMask | Key.CtrlMask, Command.PreviousViewOrTop);
+			AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix
+
+			AddKeyBinding (Key.L | Key.CtrlMask, Command.Refresh);
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="Application.AlternateForwardKey"/> is changed.
+		/// </summary>
+		public event Action<Key> AlternateForwardKeyChanged;
+
+		/// <summary>
+		/// Virtual method to invoke the <see cref="AlternateForwardKeyChanged"/> event.
+		/// </summary>
+		/// <param name="oldKey"></param>
+		public virtual void OnAlternateForwardKeyChanged (Key oldKey)
+		{
+			ReplaceKeyBinding (oldKey, Application.AlternateForwardKey);
+			AlternateForwardKeyChanged?.Invoke (oldKey);
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="Application.AlternateBackwardKey"/> is changed.
+		/// </summary>
+		public event Action<Key> AlternateBackwardKeyChanged;
+
+		/// <summary>
+		/// Virtual method to invoke the <see cref="AlternateBackwardKeyChanged"/> event.
+		/// </summary>
+		/// <param name="oldKey"></param>
+		public virtual void OnAlternateBackwardKeyChanged (Key oldKey)
+		{
+			ReplaceKeyBinding (oldKey, Application.AlternateBackwardKey);
+			AlternateBackwardKeyChanged?.Invoke (oldKey);
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="Application.QuitKey"/> is changed.
+		/// </summary>
+		public event Action<Key> QuitKeyChanged;
+
+		/// <summary>
+		/// Virtual method to invoke the <see cref="QuitKeyChanged"/> event.
+		/// </summary>
+		/// <param name="oldKey"></param>
+		public virtual void OnQuitKeyChanged (Key oldKey)
+		{
+			ReplaceKeyBinding (oldKey, Application.QuitKey);
+			QuitKeyChanged?.Invoke (oldKey);
 		}
 
 		/// <summary>
@@ -293,85 +372,84 @@ namespace Terminal.Gui {
 			if (base.ProcessKey (keyEvent))
 				return true;
 
-			switch (ShortcutHelper.GetModifiersKey (keyEvent)) {
-			case Key k when k == Application.QuitKey:
-				// FIXED: stop current execution of this container
-				if (Application.MdiTop != null) {
-					Application.MdiTop.RequestStop ();
-				} else {
-					Application.RequestStop ();
-				}
-				break;
-			case Key.Z | Key.CtrlMask:
-				Driver.Suspend ();
-				return true;
+			var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (keyEvent),
+				new KeyModifiers () { Alt = keyEvent.IsAlt, Ctrl = keyEvent.IsCtrl, Shift = keyEvent.IsShift }));
+			if (result != null)
+				return (bool)result;
 
 #if false
-			case Key.F5:
+			if (keyEvent.Key == Key.F5) {
 				Application.DebugDrawBounds = !Application.DebugDrawBounds;
 				SetNeedsDisplay ();
 				return true;
+			}
 #endif
-			case Key.Tab:
-			case Key.CursorRight:
-			case Key.CursorDown:
-			case Key.I | Key.CtrlMask: // Unix
-				var old = GetDeepestFocusedSubview (Focused);
-				if (!FocusNext ())
-					FocusNext ();
-				if (old != Focused && old != Focused?.Focused) {
-					old?.SetNeedsDisplay ();
-					Focused?.SetNeedsDisplay ();
-				} else {
-					FocusNearestView (SuperView?.TabIndexes, Direction.Forward);
-				}
-				return true;
-			case Key.BackTab | Key.ShiftMask:
-			case Key.CursorLeft:
-			case Key.CursorUp:
-				old = GetDeepestFocusedSubview (Focused);
-				if (!FocusPrev ())
-					FocusPrev ();
-				if (old != Focused && old != Focused?.Focused) {
-					old?.SetNeedsDisplay ();
-					Focused?.SetNeedsDisplay ();
-				} else {
-					FocusNearestView (SuperView?.TabIndexes?.Reverse (), Direction.Backward);
+			return false;
+		}
+
+		private void MovePreviousViewOrTop ()
+		{
+			if (Application.MdiTop == null) {
+				var top = Modal ? this : Application.Top;
+				top.FocusPrev ();
+				if (top.Focused == null) {
+					top.FocusPrev ();
 				}
-				return true;
-			case Key.Tab | Key.CtrlMask:
-			case Key key when key == Application.AlternateForwardKey: // Needed on Unix
-				if (Application.MdiTop == null) {
-					var top = Modal ? this : Application.Top;
+				top.SetNeedsDisplay ();
+				Application.EnsuresTopOnFront ();
+			} else {
+				MovePrevious ();
+			}
+		}
+
+		private void MoveNextViewOrTop ()
+		{
+			if (Application.MdiTop == null) {
+				var top = Modal ? this : Application.Top;
+				top.FocusNext ();
+				if (top.Focused == null) {
 					top.FocusNext ();
-					if (top.Focused == null) {
-						top.FocusNext ();
-					}
-					top.SetNeedsDisplay ();
-					Application.EnsuresTopOnFront ();
-				} else {
-					MoveNext ();
-				}
-				return true;
-			case Key.Tab | Key.ShiftMask | Key.CtrlMask:
-			case Key key when key == Application.AlternateBackwardKey: // Needed on Unix
-				if (Application.MdiTop == null) {
-					var top = Modal ? this : Application.Top;
-					top.FocusPrev ();
-					if (top.Focused == null) {
-						top.FocusPrev ();
-					}
-					top.SetNeedsDisplay ();
-					Application.EnsuresTopOnFront ();
-				} else {
-					MovePrevious ();
 				}
-				return true;
-			case Key.L | Key.CtrlMask:
-				Application.Refresh ();
-				return true;
+				top.SetNeedsDisplay ();
+				Application.EnsuresTopOnFront ();
+			} else {
+				MoveNext ();
+			}
+		}
+
+		private void MovePreviousView ()
+		{
+			var old = GetDeepestFocusedSubview (Focused);
+			if (!FocusPrev ())
+				FocusPrev ();
+			if (old != Focused && old != Focused?.Focused) {
+				old?.SetNeedsDisplay ();
+				Focused?.SetNeedsDisplay ();
+			} else {
+				FocusNearestView (SuperView?.TabIndexes?.Reverse (), Direction.Backward);
+			}
+		}
+
+		private void MoveNextView ()
+		{
+			var old = GetDeepestFocusedSubview (Focused);
+			if (!FocusNext ())
+				FocusNext ();
+			if (old != Focused && old != Focused?.Focused) {
+				old?.SetNeedsDisplay ();
+				Focused?.SetNeedsDisplay ();
+			} else {
+				FocusNearestView (SuperView?.TabIndexes, Direction.Forward);
+			}
+		}
+
+		private void QuitToplevel ()
+		{
+			if (Application.MdiTop != null) {
+				Application.MdiTop.RequestStop ();
+			} else {
+				Application.RequestStop ();
 			}
-			return false;
 		}
 
 		///<inheritdoc/>

+ 188 - 1
Terminal.Gui/Core/View.cs

@@ -178,6 +178,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action VisibleChanged;
 
+		/// <summary>
+		/// Event invoked when the <see cref="HotKey"/> is changed.
+		/// </summary>
+		public event Action<Key> HotKeyChanged;
+
 		/// <summary>
 		/// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
 		/// </summary>
@@ -250,6 +255,12 @@ namespace Terminal.Gui {
 		// This is null, and allocated on demand.
 		List<View> tabIndexes;
 
+		/// <summary>
+		/// Configurable keybindings supported by the control
+		/// </summary>
+		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>
 		/// This returns a tab index list of the subviews contained by this view.
 		/// </summary>
@@ -709,6 +720,7 @@ namespace Terminal.Gui {
 			TextDirection direction = TextDirection.LeftRight_TopBottom, Border border = null)
 		{
 			textFormatter = new TextFormatter ();
+			textFormatter.HotKeyChanged += TextFormatter_HotKeyChanged;
 			TextDirection = direction;
 			Border = border;
 			if (Border != null) {
@@ -736,6 +748,11 @@ namespace Terminal.Gui {
 			Text = text;
 		}
 
+		private void TextFormatter_HotKeyChanged (Key obj)
+		{
+			HotKeyChanged?.Invoke (obj);
+		}
+
 		/// <summary>
 		/// Sets a flag indicating this view needs to be redisplayed because its state has changed.
 		/// </summary>
@@ -1315,7 +1332,7 @@ namespace Terminal.Gui {
 		/// The color scheme for this view, if it is not defined, it returns the <see cref="SuperView"/>'s
 		/// color scheme.
 		/// </summary>
-		public ColorScheme ColorScheme {
+		public virtual ColorScheme ColorScheme {
 			get {
 				if (colorScheme == null)
 					return SuperView?.ColorScheme;
@@ -1426,6 +1443,10 @@ namespace Terminal.Gui {
 					}
 				}
 			}
+
+			// Invoke DrawContentCompleteEvent
+			OnDrawContentComplete (bounds);
+
 			ClearLayoutNeeded ();
 			ClearNeedsDisplay ();
 		}
@@ -1455,6 +1476,31 @@ namespace Terminal.Gui {
 			DrawContent?.Invoke (viewport);
 		}
 
+		/// <summary>
+		/// Event invoked when the content area of the View is completed drawing.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// Will be invoked after any subviews removed with <see cref="Remove(View)"/> have been completed drawing.
+		/// </para>
+		/// <para>
+		/// Rect provides the view-relative rectangle describing the currently visible viewport into the <see cref="View"/>.
+		/// </para>
+		/// </remarks>
+		public event Action<Rect> DrawContentComplete;
+
+		/// <summary>
+		/// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls.
+		/// </summary>
+		/// <param name="viewport">The view-relative rectangle describing the currently visible viewport into the <see cref="View"/></param>
+		/// <remarks>
+		/// This method will be called after any subviews removed with <see cref="Remove(View)"/> have been completed drawing.
+		/// </remarks>
+		public virtual void OnDrawContentComplete (Rect viewport)
+		{
+			DrawContentComplete?.Invoke (viewport);
+		}
+
 		/// <summary>
 		/// Causes the specified subview to have focus.
 		/// </summary>
@@ -1551,6 +1597,131 @@ namespace Terminal.Gui {
 			return false;
 		}
 
+		/// <summary>
+		/// Invokes any binding that is registered on this <see cref="View"/>
+		/// and matches the <paramref name="keyEvent"/>
+		/// </summary>
+		/// <param name="keyEvent">The key event passed.</param>
+		protected bool? InvokeKeybindings (KeyEvent keyEvent)
+		{
+			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})");
+				}
+
+				return CommandImplementations [command] ();
+			}
+
+			return null;
+		}
+
+
+		/// <summary>
+		/// <para>Adds a new key combination that will trigger the given <paramref name="command"/>
+		/// (if supported by the View - see <see cref="GetSupportedCommands"/>)
+		/// </para>
+		/// <para>If the key is already bound to a different <see cref="Command"/> it will be
+		/// rebound to this one</para>
+		/// </summary>
+		/// <param name="key"></param>
+		/// <param name="command"></param>
+		public void AddKeyBinding (Key key, Command command)
+		{
+			if (KeyBindings.ContainsKey (key)) {
+				KeyBindings [key] = command;
+			} else {
+				KeyBindings.Add (key, command);
+			}
+		}
+
+		/// <summary>
+		/// Replaces a key combination already bound to <see cref="Command"/>.
+		/// </summary>
+		/// <param name="fromKey">The key to be replaced.</param>
+		/// <param name="toKey">The new key to be used.</param>
+		protected void ReplaceKeyBinding (Key fromKey, Key toKey)
+		{
+			if (KeyBindings.ContainsKey (fromKey)) {
+				Command value = KeyBindings [fromKey];
+				KeyBindings.Remove (fromKey);
+				KeyBindings [toKey] = value;
+			}
+		}
+
+		/// <summary>
+		/// Checks if key combination already exist.
+		/// </summary>
+		/// <param name="key">The key to check.</param>
+		/// <returns><c>true</c> If the key already exist, <c>false</c>otherwise.</returns>
+		public bool ContainsKeyBinding (Key key)
+		{
+			return KeyBindings.ContainsKey (key);
+		}
+
+		/// <summary>
+		/// Removes all bound keys from the View making including the default
+		/// key combinations such as cursor navigation, scrolling etc
+		/// </summary>
+		public void ClearKeybindings ()
+		{
+			KeyBindings.Clear ();
+		}
+
+		/// <summary>
+		/// Clears the existing keybinding (if any) for the given <paramref name="key"/>
+		/// </summary>
+		/// <param name="key"></param>
+		public void ClearKeybinding (Key key)
+		{
+			KeyBindings.Remove (key);
+		}
+
+		/// <summary>
+		/// Removes all key bindings that trigger the given command.  Views can have multiple different
+		/// keys bound to the same command and this method will clear all of them.
+		/// </summary>
+		/// <param name="command"></param>
+		public void ClearKeybinding (Command command)
+		{
+			foreach(var kvp in KeyBindings.Where(kvp=>kvp.Value == command).ToArray())
+			{
+				KeyBindings.Remove (kvp.Key);
+			}
+			
+		}
+
+		/// <summary>
+		/// <para>States that the given <see cref="View"/> supports a given <paramref name="command"/>
+		/// and what <paramref name="f"/> to perform to make that command happen
+		/// </para>
+		/// <para>If the <paramref name="command"/> already has an implementation the <paramref name="f"/>
+		/// will replace the old one</para>
+		/// </summary>
+		/// <param name="command">The command.</param>
+		/// <param name="f">The function.</param>
+		protected void AddCommand (Command command, Func<bool?> f)
+		{
+			// if there is already an implementation of this command
+			if (CommandImplementations.ContainsKey (command)) {
+				// replace that implementation
+				CommandImplementations [command] = f;
+			} else {
+				// else record how to perform the action (this should be the normal case)
+				CommandImplementations.Add (command, f);
+			}
+		}
+
+		/// <summary>
+		/// Returns all commands that are supported by this <see cref="View"/>
+		/// </summary>
+		/// <returns></returns>
+		public IEnumerable<Command> GetSupportedCommands ()
+		{
+			return CommandImplementations.Keys;
+		}
+
 		/// <inheritdoc/>
 		public override bool ProcessHotKey (KeyEvent keyEvent)
 		{
@@ -2559,5 +2730,21 @@ namespace Terminal.Gui {
 		{
 			return Enabled ? ColorScheme.Normal : ColorScheme.Disabled;
 		}
+
+		/// <summary>
+		/// Get the top superview of a given <see cref="View"/>.
+		/// </summary>
+		/// <returns>The superview view.</returns>
+		public View GetTopSuperView ()
+		{
+			View top = Application.Top;
+			for (var v = this?.SuperView; v != null; v = v.SuperView) {
+				if (v != null) {
+					top = v;
+				}
+			}
+
+			return top;
+		}
 	}
 }

+ 65 - 28
Terminal.Gui/Views/Button.cs

@@ -57,7 +57,7 @@ namespace Terminal.Gui {
 		/// </param>
 		public Button (ustring text, bool is_default = false) : base (text)
 		{
-			Init (text, is_default);
+			Initialize (text, is_default);
 		}
 
 		/// <summary>
@@ -89,7 +89,7 @@ namespace Terminal.Gui {
 		public Button (int x, int y, ustring text, bool is_default)
 		    : base (new Rect (x, y, text.RuneCount + 4 + (is_default ? 2 : 0), 1), text)
 		{
-			Init (text, is_default);
+			Initialize (text, is_default);
 		}
 
 		Rune _leftBracket;
@@ -97,7 +97,7 @@ namespace Terminal.Gui {
 		Rune _leftDefault;
 		Rune _rightDefault;
 
-		void Init (ustring text, bool is_default)
+		void Initialize (ustring text, bool is_default)
 		{
 			TextAlignment = TextAlignment.Centered;
 
@@ -112,6 +112,29 @@ namespace Terminal.Gui {
 			this.is_default = is_default;
 			this.text = text ?? string.Empty;
 			Update ();
+
+			HotKeyChanged += Button_HotKeyChanged;
+
+			// Things this view knows how to do
+			AddCommand (Command.Accept, () => AcceptKey ());
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.Enter, Command.Accept);
+			AddKeyBinding (Key.Space, Command.Accept);
+			if (HotKey != Key.Null) {
+				AddKeyBinding (Key.Space | HotKey, Command.Accept);
+			}
+		}
+
+		private void Button_HotKeyChanged (Key obj)
+		{
+			if (HotKey != Key.Null) {
+				if (ContainsKeyBinding (obj)) {
+					ReplaceKeyBinding (Key.Space | obj, Key.Space | HotKey);
+				} else {
+					AddKeyBinding (Key.Space | HotKey, Command.Accept);
+				}
+			}
 		}
 
 		/// <summary>
@@ -171,16 +194,6 @@ namespace Terminal.Gui {
 			SetNeedsDisplay ();
 		}
 
-		bool CheckKey (KeyEvent key)
-		{
-			if (key.Key == (Key.AltMask | HotKey)) {
-				SetFocus ();
-				Clicked?.Invoke ();
-				return true;
-			}
-			return false;
-		}
-
 		///<inheritdoc/>
 		public override bool ProcessHotKey (KeyEvent kb)
 		{
@@ -188,10 +201,7 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			if (kb.IsAlt)
-				return CheckKey (kb);
-
-			return false;
+			return ExecuteHotKey (kb);
 		}
 
 		///<inheritdoc/>
@@ -201,11 +211,7 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			if (IsDefault && kb.KeyValue == '\n') {
-				Clicked?.Invoke ();
-				return true;
-			}
-			return CheckKey (kb);
+			return ExecuteColdKey (kb);
 		}
 
 		///<inheritdoc/>
@@ -215,14 +221,45 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			var c = kb.KeyValue;
-			if (c == '\n' || c == ' ' || kb.Key == HotKey) {
-				Clicked?.Invoke ();
-				return true;
-			}
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
+
 			return base.ProcessKey (kb);
 		}
 
+		bool ExecuteHotKey (KeyEvent ke)
+		{
+			if (ke.Key == (Key.AltMask | HotKey)) {
+				return AcceptKey ();
+			}
+			return false;
+		}
+
+		bool ExecuteColdKey (KeyEvent ke)
+		{
+			if (IsDefault && ke.KeyValue == '\n') {
+				return AcceptKey ();
+			}
+			return ExecuteHotKey (ke);
+		}
+
+		bool AcceptKey ()
+		{
+			if (!HasFocus) {
+				SetFocus ();
+			}
+			OnClicked ();
+			return true;
+		}
+
+		/// <summary>
+		/// Virtual method to invoke the <see cref="Clicked"/> event.
+		/// </summary>
+		public virtual void OnClicked ()
+		{
+			Clicked?.Invoke ();
+		}
 
 		/// <summary>
 		///   Clicked <see cref="Action"/>, raised when the user clicks the primary mouse button within the Bounds of this <see cref="View"/>
@@ -245,7 +282,7 @@ namespace Terminal.Gui {
 						SetFocus ();
 						SetNeedsDisplay ();
 					}
-					Clicked?.Invoke ();
+					OnClicked ();
 				}
 
 				return true;

+ 34 - 22
Terminal.Gui/Views/Checkbox.cs

@@ -47,11 +47,7 @@ namespace Terminal.Gui {
 		/// <param name="is_checked">If set to <c>true</c> is checked.</param>
 		public CheckBox (ustring s, bool is_checked = false) : base ()
 		{
-			Checked = is_checked;
-			Text = s;
-			CanFocus = true;
-			Height = 1;
-			Width = s.RuneCount + 4;
+			Initialize (s, is_checked);
 		}
 
 		/// <summary>
@@ -73,11 +69,24 @@ namespace Terminal.Gui {
 		///   text length. 
 		/// </remarks>
 		public CheckBox (int x, int y, ustring s, bool is_checked) : base (new Rect (x, y, s.Length + 4, 1))
+		{
+			Initialize (s, is_checked);
+		}
+
+		void Initialize (ustring s, bool is_checked)
 		{
 			Checked = is_checked;
 			Text = s;
-
 			CanFocus = true;
+			Height = 1;
+			Width = s.RuneCount + 4;
+
+			// Things this view knows how to do
+			AddCommand (Command.ToggleChecked, () => ToggleChecked ());
+
+			// Default keybindings for this view
+			AddKeyBinding ((Key)' ', Command.ToggleChecked);
+			AddKeyBinding (Key.Space, Command.ToggleChecked);
 		}
 
 		/// <summary>
@@ -138,31 +147,34 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			if (kb.KeyValue == ' ') {
-				var previousChecked = Checked;
-				Checked = !Checked;
-				OnToggled (previousChecked);
-				SetNeedsDisplay ();
-				return true;
-			}
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
+
 			return base.ProcessKey (kb);
 		}
 
 		///<inheritdoc/>
-		public override bool ProcessHotKey (KeyEvent ke)
+		public override bool ProcessHotKey (KeyEvent kb)
 		{
-			if (ke.Key == (Key.AltMask | HotKey)) {
-				SetFocus ();
-				var previousChecked = Checked;
-				Checked = !Checked;
-				OnToggled (previousChecked);
-				SetNeedsDisplay ();
-				return true;
-			}
+			if (kb.Key == (Key.AltMask | HotKey))
+				return ToggleChecked ();
 
 			return false;
 		}
 
+		bool ToggleChecked ()
+		{
+			if (!HasFocus) {
+				SetFocus ();
+			}
+			var previousChecked = Checked;
+			Checked = !Checked;
+			OnToggled (previousChecked);
+			SetNeedsDisplay ();
+			return true;
+		}
+
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent me)
 		{

+ 149 - 53
Terminal.Gui/Views/ComboBox.cs

@@ -156,6 +156,32 @@ namespace Terminal.Gui {
 				SetNeedsDisplay ();
 				Search_Changed (Text);
 			};
+
+			// Things this view knows how to do
+			AddCommand (Command.Accept, () => ActivateSelected ());
+			AddCommand (Command.ToggleExpandCollapse, () => ExpandCollapse ());
+			AddCommand (Command.Expand, () => Expand ());
+			AddCommand (Command.Collapse, () => Collapse ());
+			AddCommand (Command.LineDown, () => MoveDown ());
+			AddCommand (Command.LineUp, () => MoveUp ());
+			AddCommand (Command.PageDown, () => PageDown ());
+			AddCommand (Command.PageUp, () => PageUp ());
+			AddCommand (Command.TopHome, () => MoveHome ());
+			AddCommand (Command.BottomEnd, () => MoveEnd ());
+			AddCommand (Command.Cancel, () => CancelSelected ());
+			AddCommand (Command.UnixEmulation, () => UnixEmulation ());
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.Enter, Command.Accept);
+			AddKeyBinding (Key.F4, Command.ToggleExpandCollapse);
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+			AddKeyBinding (Key.PageUp, Command.PageUp);
+			AddKeyBinding (Key.Home, Command.TopHome);
+			AddKeyBinding (Key.End, Command.BottomEnd);
+			AddKeyBinding (Key.Esc, Command.Cancel);
+			AddKeyBinding (Key.U | Key.CtrlMask, Command.UnixEmulation);
 		}
 
 		private bool isShow = false;
@@ -182,6 +208,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets the drop down list state, expanded or collapsed.
+		/// </summary>
+		public bool IsShow => isShow;
+
 		///<inheritdoc/>
 		public new ColorScheme ColorScheme {
 			get {
@@ -318,89 +349,153 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent e)
 		{
-			if (e.Key == Key.Enter && listview.SelectedItem > -1) {
-				Selected ();
-				return true;
+			var result = InvokeKeybindings (e);
+			if (result != null)
+				return (bool)result;
+
+			return base.ProcessKey (e);
+		}
+
+		bool UnixEmulation ()
+		{
+			// Unix emulation
+			Reset ();
+			return true;
+		}
+
+		bool CancelSelected ()
+		{
+			search.SetFocus ();
+			search.Text = text = "";
+			OnSelectedChanged ();
+			Collapse ();
+			return true;
+		}
+
+		bool MoveEnd ()
+		{
+			if (HasItems ()) {
+				listview.MoveEnd ();
 			}
+			return true;
+		}
 
-			if (e.Key == Key.F4 && (search.HasFocus || listview.HasFocus)) {
-				if (!isShow) {
-					SetSearchSet ();
-					isShow = true;
-					ShowList ();
-					FocusSelectedItem ();
-				} else {
-					isShow = false;
-					HideList ();
-				}
-				return true;
+		bool MoveHome ()
+		{
+			if (HasItems ()) {
+				listview.MoveHome ();
 			}
+			return true;
+		}
 
-			if (e.Key == Key.CursorDown && search.HasFocus) { // jump to list
-				if (searchset?.Count > 0) {
-					listview.TabStop = true;
-					listview.SetFocus ();
-					SetValue (searchset [listview.SelectedItem]);
-					return true;
-				} else {
-					listview.TabStop = false;
-					SuperView.FocusNext ();
-				}
+		bool PageUp ()
+		{
+			if (HasItems ()) {
+				listview.MovePageUp ();
+			}
+			return true;
+		}
+
+		bool PageDown ()
+		{
+			if (HasItems ()) {
+				listview.MovePageDown ();
 			}
+			return true;
+		}
 
-			if (e.Key == Key.CursorUp && search.HasFocus) { // stop odd behavior on KeyUp when search has focus
+		bool? MoveUp ()
+		{
+			if (search.HasFocus) { // stop odd behavior on KeyUp when search has focus
 				return true;
 			}
 
-			if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search
+			if (listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search
 			{
 				search.CursorPosition = search.Text.RuneCount;
 				search.SetFocus ();
 				return true;
 			}
+			return null;
+		}
 
-			if (e.Key == Key.PageDown) {
-				if (listview.SelectedItem != -1) {
-					listview.MovePageDown ();
+		bool? MoveDown ()
+		{
+			if (search.HasFocus) { // jump to list
+				if (searchset?.Count > 0) {
+					listview.TabStop = true;
+					listview.SetFocus ();
+					SetValue (searchset [listview.SelectedItem]);
+				} else {
+					listview.TabStop = false;
+					SuperView?.FocusNext ();
 				}
 				return true;
 			}
+			return null;
+		}
 
-			if (e.Key == Key.PageUp) {
-				if (listview.SelectedItem != -1) {
-					listview.MovePageUp ();
+		/// <summary>
+		/// Toggles the expand/collapse state of the sublist in the combo box
+		/// </summary>
+		/// <returns></returns>
+		bool ExpandCollapse ()
+		{
+			if (search.HasFocus || listview.HasFocus) {
+				if (!isShow) {
+					return Expand ();
+				} else {
+					return Collapse ();
 				}
-				return true;
 			}
+			return false;
+		}
 
-			if (e.Key == Key.Home) {
-				if (listview.SelectedItem != -1) {
-					listview.MoveHome ();
-				}
+		bool ActivateSelected ()
+		{
+			if (HasItems ()) {
+				Selected ();
 				return true;
 			}
+			return false;
+		}
 
-			if (e.Key == Key.End) {
-				if (listview.SelectedItem != -1) {
-					listview.MoveEnd ();
-				}
-				return true;
-			}
+		bool HasItems ()
+		{
+			return Source?.Count > 0;
+		}
 
-			if (e.Key == Key.Esc) {
-				search.SetFocus ();
-				search.Text = text = "";
-				OnSelectedChanged ();
-				return true;
+		/// <summary>
+		/// Collapses the drop down list.  Returns true if the state chagned or false
+		/// if it was already collapsed and no action was taken
+		/// </summary>
+		public virtual bool Collapse ()
+		{
+			if (!isShow) {
+				return false;
 			}
 
-			// Unix emulation
-			if (e.Key == (Key.U | Key.CtrlMask)) {
-				Reset ();
-				return true;
+			isShow = false;
+			HideList ();
+			return true;
+		}
+
+		/// <summary>
+		/// Expands the drop down list.  Returns true if the state chagned or false
+		/// if it was already expanded and no action was taken
+		/// </summary>
+		public virtual bool Expand ()
+		{
+			if (isShow) {
+				return false;
 			}
 
-			return base.ProcessKey (e);
+			SetSearchSet ();
+			isShow = true;
+			ShowList ();
+			FocusSelectedItem ();
+
+			return true;
 		}
 
 		/// <summary>
@@ -489,6 +584,7 @@ namespace Terminal.Gui {
 
 		private void SetSearchSet ()
 		{
+			if (Source == null) { return; }
 			// force deep copy
 			foreach (var item in Source.ToList ()) {
 				searchset.Add (item);

+ 105 - 62
Terminal.Gui/Views/DateField.cs

@@ -26,8 +26,8 @@ namespace Terminal.Gui {
 		string longFormat;
 		string shortFormat;
 
-		int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } }
-		string Format { get { return isShort ? shortFormat : longFormat; } }
+		int fieldLen => isShort ? shortFieldLen : longFieldLen;
+		string format => isShort ? shortFormat : longFormat;
 
 		/// <summary>
 		///   DateChanged event, raised when the <see cref="Date"/> property has changed.
@@ -49,8 +49,7 @@ namespace Terminal.Gui {
 		/// <param name="isShort">If true, shows only two digits for the year.</param>
 		public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "")
 		{
-			this.isShort = isShort;
-			Initialize (date);
+			Initialize (date, isShort);
 		}
 
 		/// <summary>
@@ -64,20 +63,47 @@ namespace Terminal.Gui {
 		/// <param name="date"></param>
 		public DateField (DateTime date) : base ("")
 		{
-			this.isShort = true;
-			Width = FieldLen + 2;
+			Width = fieldLen + 2;
 			Initialize (date);
 		}
 
-		void Initialize (DateTime date)
+		void Initialize (DateTime date, bool isShort = false)
 		{
 			CultureInfo cultureInfo = CultureInfo.CurrentCulture;
 			sepChar = cultureInfo.DateTimeFormat.DateSeparator;
 			longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern);
 			shortFormat = GetShortFormat (longFormat);
-			CursorPosition = 1;
+			this.isShort = isShort;
 			Date = date;
+			CursorPosition = 1;
 			TextChanged += DateField_Changed;
+
+			// Things this view knows how to do
+			AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
+			AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; });
+			AddCommand (Command.LeftHome, () => MoveHome ());
+			AddCommand (Command.Left, () => MoveLeft ());
+			AddCommand (Command.RightEnd, () => MoveEnd ());
+			AddCommand (Command.Right, () => MoveRight ());
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
+			AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight);
+
+			AddKeyBinding (Key.Delete, Command.DeleteCharLeft);
+			AddKeyBinding (Key.Backspace, Command.DeleteCharLeft);
+
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome);
+
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.B | Key.CtrlMask, Command.Left);
+
+			AddKeyBinding (Key.End, Command.RightEnd);
+			AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd);
+
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.F | Key.CtrlMask, Command.Right);
 		}
 
 		void DateField_Changed (ustring e)
@@ -129,8 +155,8 @@ namespace Terminal.Gui {
 
 				var oldData = date;
 				date = value;
-				this.Text = value.ToString (Format);
-				var args = new DateTimeEventArgs<DateTime> (oldData, value, Format);
+				this.Text = value.ToString (format);
+				var args = new DateTimeEventArgs<DateTime> (oldData, value, format);
 				if (oldData != value) {
 					OnDateChanged (args);
 				}
@@ -157,12 +183,20 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <inheritdoc/>
+		public override int CursorPosition {
+			get => base.CursorPosition;
+			set {
+				base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1);
+			}
+		}
+
 		bool SetText (Rune key)
 		{
 			var text = TextModel.ToRunes (Text);
 			var newText = text.GetRange (0, CursorPosition);
 			newText.Add (key);
-			if (CursorPosition < FieldLen)
+			if (CursorPosition < fieldLen)
 				newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
 			return SetText (ustring.Make (newText));
 		}
@@ -174,7 +208,7 @@ namespace Terminal.Gui {
 			}
 
 			ustring [] vals = text.Split (ustring.Make (sepChar));
-			ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar));
+			ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar));
 			bool isValidDate = true;
 			int idx = GetFormatIndex (frm, "y");
 			int year = Int32.Parse (vals [idx].ToString ());
@@ -204,7 +238,7 @@ namespace Terminal.Gui {
 				day = Int32.Parse (vals [idx].ToString ());
 			string d = GetDate (month, day, year, frm);
 
-			if (!DateTime.TryParseExact (d, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
+			if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
 				!isValidDate)
 				return false;
 			Date = result;
@@ -238,7 +272,7 @@ namespace Terminal.Gui {
 		ustring GetDate (ustring text)
 		{
 			ustring [] vals = text.Split (ustring.Make (sepChar));
-			ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar));
+			ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar));
 			ustring [] date = { null, null, null };
 
 			for (int i = 0; i < frm.Length; i++) {
@@ -274,7 +308,7 @@ namespace Terminal.Gui {
 
 		void IncCursorPosition ()
 		{
-			if (CursorPosition == FieldLen)
+			if (CursorPosition == fieldLen)
 				return;
 			if (Text [++CursorPosition] == sepChar.ToCharArray () [0])
 				CursorPosition++;
@@ -297,60 +331,69 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			switch (kb.Key) {
-			case Key.DeleteChar:
-			case Key.D | Key.CtrlMask:
-				if (ReadOnly)
-					return true;
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
 
-				SetText ('0');
-				break;
-
-			case Key.Delete:
-			case Key.Backspace:
-				if (ReadOnly)
-					return true;
+			// Ignore non-numeric characters.
+			if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
+				return false;
 
-				SetText ('0');
-				DecCursorPosition ();
-				break;
+			if (ReadOnly)
+				return true;
 
-			// Home, C-A
-			case Key.Home:
-			case Key.A | Key.CtrlMask:
-				CursorPosition = 1;
-				break;
-
-			case Key.CursorLeft:
-			case Key.B | Key.CtrlMask:
-				DecCursorPosition ();
-				break;
-
-			case Key.End:
-			case Key.E | Key.CtrlMask: // End
-				CursorPosition = FieldLen;
-				break;
-
-			case Key.CursorRight:
-			case Key.F | Key.CtrlMask:
+			if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
 				IncCursorPosition ();
-				break;
 
-			default:
-				// Ignore non-numeric characters.
-				if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
-					return false;
+			return true;
+		}
 
-				if (ReadOnly)
-					return true;
+		bool MoveRight ()
+		{
+			IncCursorPosition ();
+			return true;
+		}
 
-				if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
-					IncCursorPosition ();
-				return true;
-			}
+		bool MoveEnd ()
+		{
+			CursorPosition = fieldLen;
+			return true;
+		}
+
+		bool MoveLeft ()
+		{
+			DecCursorPosition ();
 			return true;
 		}
 
+		bool MoveHome ()
+		{
+			// Home, C-A
+			CursorPosition = 1;
+			return true;
+		}
+
+		/// <inheritdoc/>
+		public override void DeleteCharLeft (bool useOldCursorPos = true)
+		{
+			if (ReadOnly)
+				return;
+
+			SetText ('0');
+			DecCursorPosition ();
+			return;
+		}
+
+		/// <inheritdoc/>
+		public override void DeleteCharRight ()
+		{
+			if (ReadOnly)
+				return;
+
+			SetText ('0');
+			return;
+		}
+
 		/// <inheritdoc/>
 		public override bool MouseEvent (MouseEvent ev)
 		{
@@ -360,8 +403,8 @@ namespace Terminal.Gui {
 				SetFocus ();
 
 			var point = ev.X;
-			if (point > FieldLen)
-				point = FieldLen;
+			if (point > fieldLen)
+				point = fieldLen;
 			if (point < 1)
 				point = 1;
 			CursorPosition = point;
@@ -386,7 +429,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The old <see cref="DateField"/> or <see cref="TimeField"/> value.
 		/// </summary>
-		public T OldValue {get;}
+		public T OldValue { get; }
 
 		/// <summary>
 		/// The new <see cref="DateField"/> or <see cref="TimeField"/> value.

+ 36 - 30
Terminal.Gui/Views/GraphView.cs

@@ -74,6 +74,23 @@ namespace Terminal.Gui {
 
 			AxisX = new HorizontalAxis ();
 			AxisY = new VerticalAxis ();
+
+			// Things this view knows how to do
+			AddCommand (Command.ScrollUp, () => { Scroll (0, CellSize.Y); return true; });
+			AddCommand (Command.ScrollDown, () => { Scroll (0, -CellSize.Y); return true; });
+			AddCommand (Command.ScrollRight, () => { Scroll (CellSize.X, 0); return true; });
+			AddCommand (Command.ScrollLeft, () => { Scroll (-CellSize.X, 0); return true; });
+			AddCommand (Command.PageUp, () => { PageUp (); return true; });
+			AddCommand (Command.PageDown, () => { PageDown(); return true; });
+
+			AddKeyBinding (Key.CursorRight, Command.ScrollRight);
+			AddKeyBinding (Key.CursorLeft, Command.ScrollLeft);
+			AddKeyBinding (Key.CursorUp, Command.ScrollUp);
+			AddKeyBinding (Key.CursorDown, Command.ScrollDown);
+			
+			// Not bound by default (preserves backwards compatibility)
+			//AddKeyBinding (Key.PageUp, Command.PageUp);
+			//AddKeyBinding (Key.PageDown, Command.PageDown);
 		}
 
 		/// <summary>
@@ -228,48 +245,37 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
-			//&& Focused == tabsBar
-
 			if (HasFocus && CanFocus) {
-				switch (keyEvent.Key) {
-
-				case Key.CursorLeft:
-					Scroll (-CellSize.X, 0);
-					return true;
-				case Key.CursorLeft | Key.CtrlMask:
-					Scroll (-CellSize.X * 5, 0);
-					return true;
-				case Key.CursorRight:
-					Scroll (CellSize.X, 0);
-					return true;
-				case Key.CursorRight | Key.CtrlMask:
-					Scroll (CellSize.X * 5, 0);
-					return true;
-				case Key.CursorDown:
-					Scroll (0, -CellSize.Y);
-					return true;
-				case Key.CursorDown | Key.CtrlMask:
-					Scroll (0, -CellSize.Y * 5);
-					return true;
-				case Key.CursorUp:
-					Scroll (0, CellSize.Y);
-					return true;
-				case Key.CursorUp | Key.CtrlMask:
-					Scroll (0, CellSize.Y * 5);
-					return true;
-				}
+				var result =  InvokeKeybindings (keyEvent);
+				if (result != null)
+					return (bool)result;
 			}
 
 			return base.ProcessKey (keyEvent);
 		}
 
+		/// <summary>
+		/// Scrolls the graph up 1 page
+		/// </summary>
+		public void PageUp()
+		{
+			Scroll (0, CellSize.Y * Bounds.Height);
+		}
+
+		/// <summary>
+		/// Scrolls the graph down 1 page
+		/// </summary>
+		public void PageDown()
+		{
+			Scroll(0, -1 * CellSize.Y * Bounds.Height);
+		}
 		/// <summary>
 		/// Scrolls the view by a given number of units in graph space.
 		/// See <see cref="CellSize"/> to translate this into rows/cols
 		/// </summary>
 		/// <param name="offsetX"></param>
 		/// <param name="offsetY"></param>
-		private void Scroll (float offsetX, float offsetY)
+		public void Scroll (float offsetX, float offsetY)
 		{
 			ScrollOffset = new PointF (
 				ScrollOffset.X + offsetX,

+ 74 - 66
Terminal.Gui/Views/HexView.cs

@@ -58,6 +58,41 @@ namespace Terminal.Gui {
 			CanFocus = true;
 			leftSide = true;
 			firstNibble = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.Left, () => MoveLeft ());
+			AddCommand (Command.Right, () => MoveRight ());
+			AddCommand (Command.LineDown, () => MoveDown (bytesPerLine));
+			AddCommand (Command.LineUp, () => MoveUp (bytesPerLine));
+			AddCommand (Command.ToggleChecked, () => ToggleSide ());
+			AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height));
+			AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height));
+			AddCommand (Command.TopHome, () => MoveHome ());
+			AddCommand (Command.BottomEnd, () => MoveEnd ());
+			AddCommand (Command.StartOfLine, () => MoveStartOfLine ());
+			AddCommand (Command.EndOfLine, () => MoveEndOfLine ());
+			AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)));
+			AddCommand (Command.EndOfPage, () => MoveDown (bytesPerLine * (Frame.Height - 1 - ((int)(position - displayStart) / bytesPerLine))));
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.Enter, Command.ToggleChecked);
+
+			AddKeyBinding ('v' + Key.AltMask, Command.PageUp);
+			AddKeyBinding (Key.PageUp, Command.PageUp);
+
+			AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+
+			AddKeyBinding (Key.Home, Command.TopHome);
+			AddKeyBinding (Key.End, Command.BottomEnd);
+			AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.StartOfLine);
+			AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.EndOfLine);
+			AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.StartOfPage);
+			AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.EndOfPage);
 		}
 
 		/// <summary>
@@ -390,76 +425,49 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
-			switch (keyEvent.Key) {
-			case Key.CursorLeft:
-				return MoveLeft ();
-			case Key.CursorRight:
-				return MoveRight ();
-			case Key.CursorDown:
-				return MoveDown (bytesPerLine);
-			case Key.CursorUp:
-				return MoveUp (bytesPerLine);
-			case Key.Enter:
-				return ToggleSide ();
-			case ((int)'v' + Key.AltMask):
-			case Key.PageUp:
-				return MoveUp (bytesPerLine * Frame.Height);
-			case Key.V | Key.CtrlMask:
-			case Key.PageDown:
-				return MoveDown (bytesPerLine * Frame.Height);
-			case Key.Home:
-				return MoveHome ();
-			case Key.End:
-				return MoveEnd ();
-			case Key.CursorLeft | Key.CtrlMask:
-				return MoveStartOfLine ();
-			case Key.CursorRight | Key.CtrlMask:
-				return MoveEndOfLine ();
-			case Key.CursorUp | Key.CtrlMask:
-				return MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine));
-			case Key.CursorDown | Key.CtrlMask:
-				return MoveDown (bytesPerLine * (Frame.Height - 1 - ((int)(position - displayStart) / bytesPerLine)));
-			default:
-				if (!AllowEdits)
-					return false;
+			var result = InvokeKeybindings (keyEvent);
+			if (result != null)
+				return (bool)result;
 
-				// Ignore control characters and other special keys
-				if (keyEvent.Key < Key.Space || keyEvent.Key > Key.CharMask)
-					return false;
+			if (!AllowEdits)
+				return false;
 
-				if (leftSide) {
-					int value;
-					var k = (char)keyEvent.Key;
-					if (k >= 'A' && k <= 'F')
-						value = k - 'A' + 10;
-					else if (k >= 'a' && k <= 'f')
-						value = k - 'a' + 10;
-					else if (k >= '0' && k <= '9')
-						value = k - '0';
-					else
-						return false;
+			// Ignore control characters and other special keys
+			if (keyEvent.Key < Key.Space || keyEvent.Key > Key.CharMask)
+				return false;
 
-					byte b;
-					if (!edits.TryGetValue (position, out b)) {
-						source.Position = position;
-						b = (byte)source.ReadByte ();
-					}
-					RedisplayLine (position);
-					if (firstNibble) {
-						firstNibble = false;
-						b = (byte)(b & 0xf | (value << bsize));
-						edits [position] = b;
-						OnEdited (new KeyValuePair<long, byte> (position, edits [position]));
-					} else {
-						b = (byte)(b & 0xf0 | value);
-						edits [position] = b;
-						OnEdited (new KeyValuePair<long, byte> (position, edits [position]));
-						MoveRight ();
-					}
-					return true;
-				} else
+			if (leftSide) {
+				int value;
+				var k = (char)keyEvent.Key;
+				if (k >= 'A' && k <= 'F')
+					value = k - 'A' + 10;
+				else if (k >= 'a' && k <= 'f')
+					value = k - 'a' + 10;
+				else if (k >= '0' && k <= '9')
+					value = k - '0';
+				else
 					return false;
-			}
+
+				byte b;
+				if (!edits.TryGetValue (position, out b)) {
+					source.Position = position;
+					b = (byte)source.ReadByte ();
+				}
+				RedisplayLine (position);
+				if (firstNibble) {
+					firstNibble = false;
+					b = (byte)(b & 0xf | (value << bsize));
+					edits [position] = b;
+					OnEdited (new KeyValuePair<long, byte> (position, edits [position]));
+				} else {
+					b = (byte)(b & 0xf0 | value);
+					edits [position] = b;
+					OnEdited (new KeyValuePair<long, byte> (position, edits [position]));
+					MoveRight ();
+				}
+				return true;
+			} else
+				return false;
 		}
 
 		/// <summary>

+ 44 - 39
Terminal.Gui/Views/ListView.cs

@@ -317,6 +317,38 @@ namespace Terminal.Gui {
 		{
 			Source = source;
 			CanFocus = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.LineUp, () => MoveUp ());
+			AddCommand (Command.LineDown, () => MoveDown ());
+			AddCommand (Command.ScrollUp, () => ScrollUp (1));
+			AddCommand (Command.ScrollDown, () => ScrollDown (1));
+			AddCommand (Command.PageUp, () => MovePageUp ());
+			AddCommand (Command.PageDown, () => MovePageDown ());
+			AddCommand (Command.TopHome, () => MoveHome ());
+			AddCommand (Command.BottomEnd, () => MoveEnd ());
+			AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ());
+			AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
+
+			// Default keybindings for all ListViews
+			AddKeyBinding (Key.CursorUp,Command.LineUp);
+			AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
+
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
+
+			AddKeyBinding(Key.PageUp,Command.PageUp);
+
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+			AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
+
+			AddKeyBinding (Key.Home, Command.TopHome);
+
+			AddKeyBinding (Key.End, Command.BottomEnd);
+
+			AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
+
+			AddKeyBinding (Key.Space, Command.ToggleChecked);
 		}
 
 		///<inheritdoc/>
@@ -383,42 +415,11 @@ namespace Terminal.Gui {
 			if (source == null)
 				return base.ProcessKey (kb);
 
-			switch (kb.Key) {
-			case Key.CursorUp:
-			case Key.P | Key.CtrlMask:
-				return MoveUp ();
-
-			case Key.CursorDown:
-			case Key.N | Key.CtrlMask:
-				return MoveDown ();
-
-			case Key.V | Key.CtrlMask:
-			case Key.PageDown:
-				return MovePageDown ();
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
 
-			case Key.PageUp:
-				return MovePageUp ();
-
-			case Key.Space:
-				if (MarkUnmarkRow ())
-					return true;
-				else
-					break;
-
-			case Key.Enter:
-				return OnOpenSelectedItem ();
-
-			case Key.End:
-				return MoveEnd ();
-
-			case Key.Home:
-				return MoveHome ();
-
-			default:
-				return false;
-			}
-
-			return true;
+			return false;
 		}
 
 		/// <summary>
@@ -602,40 +603,44 @@ namespace Terminal.Gui {
 		/// Scrolls the view down.
 		/// </summary>
 		/// <param name="lines">Number of lines to scroll down.</param>
-		public virtual void ScrollDown (int lines)
+		public virtual bool ScrollDown (int lines)
 		{
 			top = Math.Max (Math.Min (top + lines, source.Count - 1), 0);
 			SetNeedsDisplay ();
+			return true;
 		}
 
 		/// <summary>
 		/// Scrolls the view up.
 		/// </summary>
 		/// <param name="lines">Number of lines to scroll up.</param>
-		public virtual void ScrollUp (int lines)
+		public virtual bool ScrollUp (int lines)
 		{
 			top = Math.Max (top - lines, 0);
 			SetNeedsDisplay ();
+			return true;
 		}
 
 		/// <summary>
 		/// Scrolls the view right.
 		/// </summary>
 		/// <param name="cols">Number of columns to scroll right.</param>
-		public virtual void ScrollRight (int cols)
+		public virtual bool ScrollRight (int cols)
 		{
 			left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
 			SetNeedsDisplay ();
+			return true;
 		}
 
 		/// <summary>
 		/// Scrolls the view left.
 		/// </summary>
 		/// <param name="cols">Number of columns to scroll left.</param>
-		public virtual void ScrollLeft (int cols)
+		public virtual bool ScrollLeft (int cols)
 		{
 			left = Math.Max (left - cols, 0);
 			SetNeedsDisplay ();
+			return true;
 		}
 
 		int lastSelectedItem = -1;

+ 62 - 66
Terminal.Gui/Views/Menu.cs

@@ -413,6 +413,25 @@ namespace Terminal.Gui {
 				WantMousePositionReports = host.WantMousePositionReports;
 			}
 
+			// Things this view knows how to do
+			AddCommand (Command.LineUp, () => MoveUp ());
+			AddCommand (Command.LineDown, () => MoveDown ());
+			AddCommand (Command.Left, () => { this.host.PreviousMenu (true); return true; });
+			AddCommand (Command.Right, () => {
+				this.host.NextMenu (this.barItems.IsTopLevel || (this.barItems.Children != null
+					&& current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu)
+					? true : false); return true;
+			});
+			AddCommand (Command.Cancel, () => { CloseAllMenus (); return true; });
+			AddCommand (Command.Accept, () => { RunSelected (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.Esc, Command.Cancel);
+			AddKeyBinding (Key.Enter, Command.Accept);
 		}
 
 		internal Attribute DetermineColorSchemeFor (MenuItem item, int index)
@@ -550,40 +569,21 @@ namespace Terminal.Gui {
 
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			switch (kb.Key) {
-			case Key.Tab:
-				host.CleanUp ();
-				return true;
-			case Key.CursorUp:
-				return MoveUp ();
-			case Key.CursorDown:
-				return MoveDown ();
-			case Key.CursorLeft:
-				host.PreviousMenu (true);
-				return true;
-			case Key.CursorRight:
-				host.NextMenu (barItems.IsTopLevel || (barItems.Children != null && current > -1 && current < barItems.Children.Length && barItems.Children [current].IsFromSubMenu) ? true : false);
-				return true;
-			case Key.Esc:
-				CloseAllMenus ();
-				return true;
-			case Key.Enter:
-				RunSelected ();
-				return true;
-			default:
-				// TODO: rune-ify
-				if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) {
-					var x = Char.ToUpper ((char)kb.KeyValue);
-					foreach (var item in barItems.Children) {
-						if (item == null) continue;
-						if (item.IsEnabled () && item.HotKey == x) {
-							host.CloseMenu ();
-							Run (item.Action);
-							return true;
-						}
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
+
+			// TODO: rune-ify
+			if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) {
+				var x = Char.ToUpper ((char)kb.KeyValue);
+				foreach (var item in barItems.Children) {
+					if (item == null) continue;
+					if (item.IsEnabled () && item.HotKey == x) {
+						host.CloseMenu ();
+						Run (item.Action);
+						return true;
 					}
 				}
-				break;
 			}
 			return false;
 		}
@@ -832,6 +832,20 @@ namespace Terminal.Gui {
 			ColorScheme = Colors.Menu;
 			WantMousePositionReports = true;
 			IsMenuOpen = false;
+
+			// Things this view knows how to do
+			AddCommand (Command.Left, () => { MoveLeft (); return true; });
+			AddCommand (Command.Right, () => { MoveRight (); return true; });
+			AddCommand (Command.Cancel, () => { CloseMenuBar (); return true; });
+			AddCommand (Command.Accept, () => { ProcessMenu (selected, Menus [selected]); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.Esc, Command.Cancel);
+			AddKeyBinding (Key.C | Key.CtrlMask, Command.Cancel);
+			AddKeyBinding (Key.CursorDown, Command.Accept);
+			AddKeyBinding (Key.Enter, Command.Accept);
 		}
 
 		bool openedByAltKey;
@@ -1491,48 +1505,30 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			switch (kb.Key) {
-			case Key.CursorLeft:
-				MoveLeft ();
+			if (InvokeKeybindings (kb) == true)
 				return true;
 
-			case Key.CursorRight:
-				MoveRight ();
-				return true;
-
-			case Key.Esc:
-			case Key.C | Key.CtrlMask:
-				CloseMenuBar ();
-				return true;
-
-			case Key.CursorDown:
-			case Key.Enter:
-				ProcessMenu (selected, Menus [selected]);
-				return true;
-
-			default:
-				var key = kb.KeyValue;
-				if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
-					char c = Char.ToUpper ((char)key);
+			var key = kb.KeyValue;
+			if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
+				char c = Char.ToUpper ((char)key);
 
-					if (selected == -1 || Menus [selected].IsTopLevel)
-						return false;
+				if (selected == -1 || Menus [selected].IsTopLevel)
+					return false;
 
-					foreach (var mi in Menus [selected].Children) {
-						if (mi == null)
-							continue;
-						int p = mi.Title.IndexOf ('_');
-						if (p != -1 && p + 1 < mi.Title.RuneCount) {
-							if (mi.Title [p + 1] == c) {
-								Selected (mi);
-								return true;
-							}
+				foreach (var mi in Menus [selected].Children) {
+					if (mi == null)
+						continue;
+					int p = mi.Title.IndexOf ('_');
+					if (p != -1 && p + 1 < mi.Title.RuneCount) {
+						if (mi.Title [p + 1] == c) {
+							Selected (mi);
+							return true;
 						}
 					}
 				}
-
-				return false;
 			}
+
+			return false;
 		}
 
 		void CloseMenuBar ()

+ 97 - 63
Terminal.Gui/Views/RadioGroup.cs

@@ -14,20 +14,6 @@ namespace Terminal.Gui {
 		int horizontalSpace = 2;
 		List<(int pos, int length)> horizontal;
 
-		void Init (Rect rect, ustring [] radioLabels, int selected)
-		{
-			if (radioLabels == null) {
-				this.radioLabels = new List<ustring> ();
-			} else {
-				this.radioLabels = radioLabels.ToList ();
-			}
-
-			this.selected = selected;
-			SetWidthHeight (this.radioLabels);
-			CanFocus = true;
-		}
-
-
 		/// <summary>
 		/// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
@@ -40,7 +26,7 @@ namespace Terminal.Gui {
 		/// <param name="selected">The index of the item to be selected, the value is clamped to the number of items.</param>
 		public RadioGroup (ustring [] radioLabels, int selected = 0) : base ()
 		{
-			Init (Rect.Empty, radioLabels, selected);
+			Initialize (radioLabels, selected);
 		}
 
 		/// <summary>
@@ -51,7 +37,7 @@ namespace Terminal.Gui {
 		/// <param name="selected">The index of item to be selected, the value is clamped to the number of items.</param>
 		public RadioGroup (Rect rect, ustring [] radioLabels, int selected = 0) : base (rect)
 		{
-			Init (rect, radioLabels, selected);
+			Initialize (radioLabels, selected);
 		}
 
 		/// <summary>
@@ -61,11 +47,38 @@ namespace Terminal.Gui {
 		/// <param name="x">The x coordinate.</param>
 		/// <param name="y">The y coordinate.</param>
 		/// <param name="radioLabels">The radio labels; an array of strings that can contain hotkeys using an underscore before the letter.</param>
-		/// <param name="selected">The item to be selected, the value is clamped to the number of items.</param>		
+		/// <param name="selected">The item to be selected, the value is clamped to the number of items.</param>
 		public RadioGroup (int x, int y, ustring [] radioLabels, int selected = 0) :
 			this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected)
 		{ }
 
+		void Initialize (ustring [] radioLabels, int selected)
+		{
+			if (radioLabels == null) {
+				this.radioLabels = new List<ustring> ();
+			} else {
+				this.radioLabels = radioLabels.ToList ();
+			}
+
+			this.selected = selected;
+			SetWidthHeight (this.radioLabels);
+			CanFocus = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.LineUp, () => { MoveUp (); return true; });
+			AddCommand (Command.LineDown, () => { MoveDown (); return true; });
+			AddCommand (Command.TopHome, () => { MoveHome (); return true; });
+			AddCommand (Command.BottomEnd, () => { MoveEnd (); return true; });
+			AddCommand (Command.Accept, () => { SelectItem (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.Home, Command.TopHome);
+			AddKeyBinding (Key.End, Command.BottomEnd);
+			AddKeyBinding (Key.Space, Command.Accept);
+		}
+
 		/// <summary>
 		/// Gets or sets the <see cref="DisplayModeLayout"/> for this <see cref="RadioGroup"/>.
 		/// </summary>
@@ -215,33 +228,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		// TODO: Make this a global class
-		/// <summary>
-		/// Event arguments for the SelectedItemChagned event.
-		/// </summary>
-		public class SelectedItemChangedArgs : EventArgs {
-			/// <summary>
-			/// Gets the index of the item that was previously selected. -1 if there was no previous selection.
-			/// </summary>
-			public int PreviousSelectedItem { get; }
-
-			/// <summary>
-			/// Gets the index of the item that is now selected. -1 if there is no selection.
-			/// </summary>
-			public int SelectedItem { get; }
-
-			/// <summary>
-			/// Initializes a new <see cref="SelectedItemChangedArgs"/> class.
-			/// </summary>
-			/// <param name="selectedItem"></param>
-			/// <param name="previousSelectedItem"></param>
-			public SelectedItemChangedArgs (int selectedItem, int previousSelectedItem)
-			{
-				PreviousSelectedItem = previousSelectedItem;
-				SelectedItem = selectedItem;
-			}
-		}
-
 		/// <summary>
 		/// Invoked when the selected radio label has changed.
 		/// </summary>
@@ -311,28 +297,50 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			switch (kb.Key) {
-			case Key.CursorUp:
-				if (cursor > 0) {
-					cursor--;
-					SetNeedsDisplay ();
-					return true;
-				}
-				break;
-			case Key.CursorDown:
-				if (cursor + 1 < radioLabels.Count) {
-					cursor++;
-					SetNeedsDisplay ();
-					return true;
-				}
-				break;
-			case Key.Space:
-				SelectedItem = cursor;
-				return true;
-			}
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
+
 			return base.ProcessKey (kb);
 		}
 
+		void SelectItem ()
+		{
+			SelectedItem = cursor;
+		}
+
+		void MoveEnd ()
+		{
+			cursor = Math.Max (radioLabels.Count - 1, 0);
+		}
+
+		void MoveHome ()
+		{
+			cursor = 0;
+		}
+
+		void MoveDown ()
+		{
+			if (cursor + 1 < radioLabels.Count) {
+				cursor++;
+				SetNeedsDisplay ();
+			} else if (cursor > 0) {
+				cursor = 0;
+				SetNeedsDisplay ();
+			}
+		}
+
+		void MoveUp ()
+		{
+			if (cursor > 0) {
+				cursor--;
+				SetNeedsDisplay ();
+			} else if (radioLabels.Count - 1 > 0) {
+				cursor = radioLabels.Count - 1;
+				SetNeedsDisplay ();
+			}
+		}
+
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent me)
 		{
@@ -379,4 +387,30 @@ namespace Terminal.Gui {
 		/// </summary>
 		Horizontal
 	}
+
+	/// <summary>
+	/// Event arguments for the SelectedItemChagned event.
+	/// </summary>
+	public class SelectedItemChangedArgs : EventArgs {
+		/// <summary>
+		/// Gets the index of the item that was previously selected. -1 if there was no previous selection.
+		/// </summary>
+		public int PreviousSelectedItem { get; }
+
+		/// <summary>
+		/// Gets the index of the item that is now selected. -1 if there is no selection.
+		/// </summary>
+		public int SelectedItem { get; }
+
+		/// <summary>
+		/// Initializes a new <see cref="SelectedItemChangedArgs"/> class.
+		/// </summary>
+		/// <param name="selectedItem"></param>
+		/// <param name="previousSelectedItem"></param>
+		public SelectedItemChangedArgs (int selectedItem, int previousSelectedItem)
+		{
+			PreviousSelectedItem = previousSelectedItem;
+			SelectedItem = selectedItem;
+		}
+	}
 }

+ 41 - 29
Terminal.Gui/Views/ScrollView.cs

@@ -39,7 +39,7 @@ namespace Terminal.Gui {
 		/// <param name="frame"></param>
 		public ScrollView (Rect frame) : base (frame)
 		{
-			Init (frame);
+			Initialize (frame);
 		}
 
 
@@ -48,10 +48,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		public ScrollView () : base ()
 		{
-			Init (new Rect (0, 0, 0, 0));
+			Initialize (Rect.Empty);
 		}
 
-		void Init (Rect frame)
+		void Initialize (Rect frame)
 		{
 			contentView = new View (frame);
 			vertical = new ScrollBarView (1, 0, isVertical: true) {
@@ -74,6 +74,8 @@ namespace Terminal.Gui {
 				ContentOffset = new Point (horizontal.Position, ContentOffset.Y);
 			};
 			horizontal.Host = this;
+			vertical.OtherScrollBarView = horizontal;
+			horizontal.OtherScrollBarView = vertical;
 			base.Add (contentView);
 			CanFocus = true;
 
@@ -81,6 +83,39 @@ namespace Terminal.Gui {
 			MouseLeave += View_MouseLeave;
 			contentView.MouseEnter += View_MouseEnter;
 			contentView.MouseLeave += View_MouseLeave;
+
+			// Things this view knows how to do
+			AddCommand (Command.ScrollUp, () => ScrollUp (1));
+			AddCommand (Command.ScrollDown, () => ScrollDown (1));
+			AddCommand (Command.ScrollLeft, () => ScrollLeft (1));
+			AddCommand (Command.ScrollRight, () => ScrollRight (1));
+			AddCommand (Command.PageUp, () => ScrollUp (Bounds.Height));
+			AddCommand (Command.PageDown, () => ScrollDown (Bounds.Height));
+			AddCommand (Command.PageLeft, () => ScrollLeft (Bounds.Width));
+			AddCommand (Command.PageRight, () => ScrollRight (Bounds.Width));
+			AddCommand (Command.TopHome, () => ScrollUp (contentSize.Height));
+			AddCommand (Command.BottomEnd, () => ScrollDown (contentSize.Height));
+			AddCommand (Command.LeftHome, () => ScrollLeft (contentSize.Width));
+			AddCommand (Command.RightEnd, () => ScrollRight (contentSize.Width));
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorUp, Command.ScrollUp);
+			AddKeyBinding (Key.CursorDown, Command.ScrollDown);
+			AddKeyBinding (Key.CursorLeft, Command.ScrollLeft);
+			AddKeyBinding (Key.CursorRight, Command.ScrollRight);
+
+			AddKeyBinding (Key.PageUp, Command.PageUp);
+			AddKeyBinding ((Key)'v' | Key.AltMask, Command.PageUp);
+
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+			AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
+
+			AddKeyBinding (Key.PageUp | Key.CtrlMask, Command.PageLeft);
+			AddKeyBinding (Key.PageDown | Key.CtrlMask, Command.PageRight);
+			AddKeyBinding (Key.Home, Command.TopHome);
+			AddKeyBinding (Key.End, Command.BottomEnd);
+			AddKeyBinding (Key.Home | Key.CtrlMask, Command.LeftHome);
+			AddKeyBinding (Key.End | Key.CtrlMask, Command.RightEnd);
 		}
 
 		Size contentSize;
@@ -451,33 +486,10 @@ namespace Terminal.Gui {
 			if (base.ProcessKey (kb))
 				return true;
 
-			switch (kb.Key) {
-			case Key.CursorUp:
-				return ScrollUp (1);
-			case (Key)'v' | Key.AltMask:
-			case Key.PageUp:
-				return ScrollUp (Bounds.Height);
-
-			case Key.V | Key.CtrlMask:
-			case Key.PageDown:
-				return ScrollDown (Bounds.Height);
-
-			case Key.CursorDown:
-				return ScrollDown (1);
-
-			case Key.CursorLeft:
-				return ScrollLeft (1);
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
 
-			case Key.CursorRight:
-				return ScrollRight (1);
-
-			case Key.Home:
-				return ScrollUp (contentSize.Height);
-
-			case Key.End:
-				return ScrollDown (contentSize.Height);
-
-			}
 			return false;
 		}
 

+ 18 - 17
Terminal.Gui/Views/TabView.cs

@@ -110,6 +110,19 @@ namespace Terminal.Gui {
 
 			base.Add (tabsBar);
 			base.Add (contentView);
+
+			// Things this view knows how to do
+			AddCommand (Command.Left, () => { SwitchTabBy (-1); return true; });
+			AddCommand (Command.Right, () => { SwitchTabBy (1); return true; });
+			AddCommand (Command.LeftHome, () => { SelectedTab = Tabs.FirstOrDefault (); return true; });
+			AddCommand (Command.RightEnd, () => { SelectedTab = Tabs.LastOrDefault (); return true; });
+
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.End, Command.RightEnd);
 		}
 
 		/// <summary>
@@ -179,7 +192,7 @@ namespace Terminal.Gui {
 
 			if (Tabs.Any ()) {
 				tabsBar.Redraw (tabsBar.Bounds);
-				contentView.SetNeedsDisplay();
+				contentView.SetNeedsDisplay ();
 				contentView.Redraw (contentView.Bounds);
 			}
 		}
@@ -216,21 +229,9 @@ namespace Terminal.Gui {
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
 			if (HasFocus && CanFocus && Focused == tabsBar) {
-				switch (keyEvent.Key) {
-
-				case Key.CursorLeft:
-					SwitchTabBy (-1);
-					return true;
-				case Key.CursorRight:
-					SwitchTabBy (1);
-					return true;
-				case Key.Home:
-					SelectedTab = Tabs.FirstOrDefault ();
-					return true;
-				case Key.End:
-					SelectedTab = Tabs.LastOrDefault ();
-					return true;
-				}
+				var result = InvokeKeybindings (keyEvent);
+				if (result != null)
+					return (bool)result;
 			}
 
 			return base.ProcessKey (keyEvent);
@@ -673,7 +674,7 @@ namespace Terminal.Gui {
 
 			public override bool MouseEvent (MouseEvent me)
 			{
-				if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && 
+				if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
 				!me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
 					return false;

+ 145 - 85
Terminal.Gui/Views/TableView.cs

@@ -58,6 +58,7 @@ namespace Terminal.Gui {
 		private int selectedColumn;
 		private DataTable table;
 		private TableStyle style = new TableStyle ();
+		private Key cellActivationKey = Key.Enter;
 
 		/// <summary>
 		/// The default maximum cell width for <see cref="TableView.MaxCellWidth"/> and <see cref="ColumnStyle.MaxWidth"/>
@@ -171,7 +172,15 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The key which when pressed should trigger <see cref="CellActivated"/> event.  Defaults to Enter.
 		/// </summary>
-		public Key CellActivationKey { get; set; } = Key.Enter;
+		public Key CellActivationKey {
+			get => cellActivationKey;
+			set {
+				if (cellActivationKey != value) {
+					ReplaceKeyBinding (cellActivationKey, value);
+					cellActivationKey = value;
+				}
+			}
+		}
 
 		/// <summary>
 		/// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. 
@@ -188,32 +197,84 @@ namespace Terminal.Gui {
 		public TableView () : base ()
 		{
 			CanFocus = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; });
+			AddCommand (Command.Left, () => { ChangeSelectionByOffset (-1, 0, false); return true; });
+			AddCommand (Command.LineUp, () => { ChangeSelectionByOffset (0, -1, false); return true; });
+			AddCommand (Command.LineDown, () => { ChangeSelectionByOffset (0, 1, false); return true; });
+			AddCommand (Command.PageUp, () => { PageUp (false); return true; });
+			AddCommand (Command.PageDown, () => { PageDown (false); return true; });
+			AddCommand (Command.LeftHome, () => { ChangeSelectionToStartOfRow (false);  return true; });
+			AddCommand (Command.RightEnd, () => { ChangeSelectionToEndOfRow (false); return true; });
+			AddCommand (Command.TopHome, () => { ChangeSelectionToStartOfTable(false); return true; });
+			AddCommand (Command.BottomEnd, () => { ChangeSelectionToEndOfTable (false); return true; });
+
+			AddCommand (Command.RightExtend, () => { ChangeSelectionByOffset (1, 0, true); return true; });
+			AddCommand (Command.LeftExtend, () => { ChangeSelectionByOffset (-1, 0, true); return true; });
+			AddCommand (Command.LineUpExtend, () => { ChangeSelectionByOffset (0, -1, true); return true; });
+			AddCommand (Command.LineDownExtend, () => { ChangeSelectionByOffset (0, 1, true); return true; });
+			AddCommand (Command.PageUpExtend, () => { PageUp (true); return true; });
+			AddCommand (Command.PageDownExtend, () => { PageDown (true); return true; });
+			AddCommand (Command.LeftHomeExtend, () => { ChangeSelectionToStartOfRow (true); return true; });
+			AddCommand (Command.RightEndExtend, () => { ChangeSelectionToEndOfRow (true); return true; });
+			AddCommand (Command.TopHomeExtend, () => { ChangeSelectionToStartOfTable (true); return true; });
+			AddCommand (Command.BottomEndExtend, () => { ChangeSelectionToEndOfTable (true); return true; });
+
+			AddCommand (Command.SelectAll, () => { SelectAll(); return true; });
+			AddCommand (Command.Accept, () => { new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.PageUp, Command.PageUp);
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.End, Command.RightEnd);
+			AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome);
+			AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd);
+
+			AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend);
+			AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend);
+			AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend);
+			AddKeyBinding (Key.CursorDown| Key.ShiftMask, Command.LineDownExtend);
+			AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend);
+			AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend);
+			AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend);
+			AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend);
+			AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend);
+			AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend);
+
+			AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll);
+			AddKeyBinding (CellActivationKey, Command.Accept);
 		}
 
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
-		{
-			Move (0, 0);
-			var frame = Frame;
+			{
+				Move (0, 0);
+				var frame = Frame;
 
-			// What columns to render at what X offset in viewport
-			var columnsToRender = CalculateViewport (bounds).ToArray ();
+				// What columns to render at what X offset in viewport
+				var columnsToRender = CalculateViewport (bounds).ToArray ();
 
-			Driver.SetAttribute (GetNormalColor ());
+				Driver.SetAttribute (GetNormalColor ());
 
-			//invalidate current row (prevents scrolling around leaving old characters in the frame
-			Driver.AddStr (new string (' ', bounds.Width));
+				//invalidate current row (prevents scrolling around leaving old characters in the frame
+				Driver.AddStr (new string (' ', bounds.Width));
 
-			int line = 0;
+				int line = 0;
 
-			if (ShouldRenderHeaders ()) {
-				// Render something like:
-				/*
-					┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
-					│ArithmeticComparator│chi       │Healthboard│Interpretation│Labnumber│
-					└────────────────────┴──────────┴───────────┴──────────────┴─────────┘
-				*/
-				if (Style.ShowHorizontalHeaderOverline) {
+				if (ShouldRenderHeaders ()) {
+					// Render something like:
+					/*
+						┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
+						│ArithmeticComparator│chi       │Healthboard│Interpretation│Labnumber│
+						└────────────────────┴──────────┴───────────┴──────────────┴─────────┘
+					*/
+			if (Style.ShowHorizontalHeaderOverline) {
 					RenderHeaderOverline (line, bounds.Width, columnsToRender);
 					line++;
 				}
@@ -561,76 +622,13 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			if (keyEvent.Key == CellActivationKey && Table != null) {
-				OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow));
+			var result = InvokeKeybindings (keyEvent);
+			if (result != null) {
+				PositionCursor ();
 				return true;
 			}
 
-			switch (keyEvent.Key) {
-			case Key.CursorLeft:
-			case Key.CursorLeft | Key.ShiftMask:
-				ChangeSelectionByOffset (-1, 0, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.CursorRight:
-			case Key.CursorRight | Key.ShiftMask:
-				ChangeSelectionByOffset (1, 0, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.CursorDown:
-			case Key.CursorDown | Key.ShiftMask:
-				ChangeSelectionByOffset (0, 1, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.CursorUp:
-			case Key.CursorUp | Key.ShiftMask:
-				ChangeSelectionByOffset (0, -1, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.PageUp:
-			case Key.PageUp | Key.ShiftMask:
-				ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.PageDown:
-			case Key.PageDown | Key.ShiftMask:
-				ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.Home | Key.CtrlMask:
-			case Key.Home | Key.CtrlMask | Key.ShiftMask:
-				// jump to table origin
-				SetSelection (0, 0, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.Home:
-			case Key.Home | Key.ShiftMask:
-				// jump to start of line
-				SetSelection (0, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.End | Key.CtrlMask:
-			case Key.End | Key.CtrlMask | Key.ShiftMask:
-				// jump to end of table
-				SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			case Key.A | Key.CtrlMask:
-				SelectAll ();
-				Update ();
-				break;
-			case Key.End:
-			case Key.End | Key.ShiftMask:
-				//jump to end of row
-				SetSelection (Table.Columns.Count - 1, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask));
-				Update ();
-				break;
-			default:
-				// Not a keystroke we care about
-				return false;
-			}
-			PositionCursor ();
-			return true;
+			return false;
 		}
 
 		/// <summary>
@@ -671,6 +669,68 @@ namespace Terminal.Gui {
 		public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection)
 		{
 			SetSelection (SelectedColumn + offsetX, SelectedRow + offsetY, extendExistingSelection);
+			Update ();
+		}
+
+		/// <summary>
+		/// Moves the selection up by one page
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void PageUp(bool extend)
+		{
+			ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), extend);
+			Update ();
+		}
+
+		/// <summary>
+		/// Moves the selection down by one page
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void PageDown(bool extend)
+		{
+			ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), extend);
+			Update ();
+		}
+
+		/// <summary>
+		/// Moves or extends the selection to the first cell in the table (0,0)
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void ChangeSelectionToStartOfTable (bool extend)
+		{
+			SetSelection (0, 0, extend);
+			Update ();
+		}
+
+		/// <summary>
+		/// Moves or extends the selection to the final cell in the table
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void ChangeSelectionToEndOfTable(bool extend)
+		{
+			SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, extend);
+			Update ();
+		}
+
+
+		/// <summary>
+		/// Moves or extends the selection to the last cell in the current row
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void ChangeSelectionToEndOfRow (bool extend)
+		{
+			SetSelection (Table.Columns.Count - 1, SelectedRow, extend);
+			Update ();
+		}
+
+		/// <summary>
+		/// Moves or extends the selection to the first cell in the current row
+		/// </summary>
+		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
+		public void ChangeSelectionToStartOfRow (bool extend)
+		{
+			SetSelection (0, SelectedRow, extend);
+			Update ();
 		}
 
 		/// <summary>

+ 249 - 163
Terminal.Gui/Views/TextField.cs

@@ -9,6 +9,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using NStack;
+using Rune = System.Rune;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -94,6 +95,109 @@ namespace Terminal.Gui {
 			CanFocus = true;
 			Used = true;
 			WantMousePositionReports = true;
+
+			Initialized += TextField_Initialized;
+
+			// Things this view knows how to do
+			AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
+			AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; });
+			AddCommand (Command.LeftHomeExtend, () => { MoveHomeExtend (); return true; });
+			AddCommand (Command.RightEndExtend, () => { MoveEndExtend (); return true; });
+			AddCommand (Command.LeftHome, () => { MoveHome (); return true; });
+			AddCommand (Command.LeftExtend, () => { MoveLeftExtend (); return true; });
+			AddCommand (Command.RightExtend, () => { MoveRightExtend (); return true; });
+			AddCommand (Command.WordLeftExtend, () => { MoveWordLeftExtend (); return true; });
+			AddCommand (Command.WordRightExtend, () => { MoveWordRightExtend (); return true; });
+			AddCommand (Command.Left, () => { MoveLeft (); return true; });
+			AddCommand (Command.RightEnd, () => { MoveEnd (); return true; });
+			AddCommand (Command.Right, () => { MoveRight (); return true; });
+			AddCommand (Command.CutToEndLine, () => { KillToEnd (); return true; });
+			AddCommand (Command.CutToStartLine, () => { KillToStart (); return true; });
+			AddCommand (Command.Undo, () => { UndoChanges (); return true; });
+			AddCommand (Command.Redo, () => { RedoChanges (); return true; });
+			AddCommand (Command.WordLeft, () => { MoveWordLeft (); return true; });
+			AddCommand (Command.WordRight, () => { MoveWordRight (); return true; });
+			AddCommand (Command.KillWordForwards, () => { KillWordForwards (); return true; });
+			AddCommand (Command.KillWordBackwards, () => { KillWordBackwards (); return true; });
+			AddCommand (Command.ToggleOverwrite, () => { SetOverwrite (!Used); return true; });
+			AddCommand (Command.EnableOverwrite, () => { SetOverwrite (true); return true; });
+			AddCommand (Command.DisableOverwrite, () => { SetOverwrite (false); return true; });
+			AddCommand (Command.Copy, () => { Copy (); return true; });
+			AddCommand (Command.Cut, () => { Cut (); return true; });
+			AddCommand (Command.Paste, () => { Paste (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
+			AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight);
+
+			AddKeyBinding (Key.Delete, Command.DeleteCharLeft);
+			AddKeyBinding (Key.Backspace, Command.DeleteCharLeft);
+
+			AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend);
+			AddKeyBinding (Key.Home | Key.ShiftMask | Key.CtrlMask, Command.LeftHomeExtend);
+			AddKeyBinding (Key.A | Key.ShiftMask | Key.CtrlMask, Command.LeftHomeExtend);
+
+			AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend);
+			AddKeyBinding (Key.End | Key.ShiftMask | Key.CtrlMask, Command.RightEndExtend);
+			AddKeyBinding (Key.E | Key.ShiftMask | Key.CtrlMask, Command.RightEndExtend);
+
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.Home | Key.CtrlMask, Command.LeftHome);
+			AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome);
+
+			AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend);
+			AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LeftExtend);
+
+			AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend);
+			AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.RightExtend);
+
+			AddKeyBinding (Key.CursorLeft | Key.ShiftMask | Key.CtrlMask, Command.WordLeftExtend);
+			AddKeyBinding (Key.CursorUp | Key.ShiftMask | Key.CtrlMask, Command.WordLeftExtend);
+			AddKeyBinding ((Key)((int)'B' + Key.ShiftMask | Key.AltMask), Command.WordLeftExtend);
+
+			AddKeyBinding (Key.CursorRight | Key.ShiftMask | Key.CtrlMask, Command.WordRightExtend);
+			AddKeyBinding (Key.CursorDown | Key.ShiftMask | Key.CtrlMask, Command.WordRightExtend);
+			AddKeyBinding ((Key)((int)'F' + Key.ShiftMask | Key.AltMask), Command.WordRightExtend);
+
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.B | Key.CtrlMask, Command.Left);
+
+			AddKeyBinding (Key.End, Command.RightEnd);
+			AddKeyBinding (Key.End | Key.CtrlMask, Command.RightEnd);
+			AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd);
+
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.F | Key.CtrlMask, Command.Right);
+
+			AddKeyBinding (Key.K | Key.CtrlMask, Command.CutToEndLine);
+			AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine);
+
+			AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo);
+			AddKeyBinding (Key.Backspace | Key.AltMask, Command.Undo);
+
+			AddKeyBinding (Key.Y | Key.CtrlMask, Command.Redo);
+
+			AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.WordLeft);
+			AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.WordLeft);
+			AddKeyBinding ((Key)((int)'B' + Key.AltMask), Command.WordLeft);
+
+			AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight);
+			AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.WordRight);
+			AddKeyBinding ((Key)((int)'F' + Key.AltMask), Command.WordRight);
+
+			AddKeyBinding (Key.DeleteChar | Key.CtrlMask, Command.KillWordForwards);
+			AddKeyBinding (Key.Backspace | Key.CtrlMask, Command.KillWordBackwards);
+			AddKeyBinding (Key.InsertChar, Command.ToggleOverwrite);
+			AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy);
+			AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut);
+			AddKeyBinding (Key.V | Key.CtrlMask, Command.Paste);
+		}
+
+
+		void TextField_Initialized (object sender, EventArgs e)
+		{
+			Autocomplete.HostControl = this;
+			Autocomplete.PopupInsideContainer = false;
 		}
 
 		///<inheritdoc/>
@@ -107,6 +211,12 @@ namespace Terminal.Gui {
 			return base.OnLeave (view);
 		}
 
+		/// <summary>
+		/// Provides autocomplete context menu based on suggestions at the current cursor
+		/// position. Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature.
+		/// </summary>
+		public IAutocomplete Autocomplete { get; protected set; } = new TextFieldAutocomplete ();
+
 		///<inheritdoc/>
 		public override Rect Frame {
 			get => base.Frame;
@@ -174,7 +284,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		///    Sets or gets the current cursor position.
 		/// </summary>
-		public int CursorPosition {
+		public virtual int CursorPosition {
 			get { return point; }
 			set {
 				if (value < 0) {
@@ -188,6 +298,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets the left offset position.
+		/// </summary>
+		public int ScrollOffset => first;
+
 		/// <summary>
 		///   Sets the cursor position.
 		/// </summary>
@@ -248,6 +363,17 @@ namespace Terminal.Gui {
 			}
 
 			PositionCursor ();
+
+			if (SelectedLength > 0)
+				return;
+
+			// draw autocomplete
+			Autocomplete.GenerateSuggestions ();
+
+			var renderAt = new Point (
+				CursorPosition - ScrollOffset, 0);
+
+			Autocomplete.RenderOverlay (renderAt);
 		}
 
 		Attribute GetReadOnlyColor ()
@@ -330,174 +456,65 @@ namespace Terminal.Gui {
 			// Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2
 			oldCursorPos = point;
 
-			switch (ShortcutHelper.GetModifiersKey (kb)) {
-			case Key.DeleteChar:
-			case Key.D | Key.CtrlMask: // Delete
-				DeleteCharRight ();
-				break;
-
-			case Key.Delete:
-			case Key.Backspace:
-				DeleteCharLeft ();
-				break;
-
-			case Key.Home | Key.ShiftMask:
-			case Key.Home | Key.ShiftMask | Key.CtrlMask:
-			case Key.A | Key.ShiftMask | Key.CtrlMask:
-				MoveHomeExtend ();
-				break;
-
-			case Key.End | Key.ShiftMask:
-			case Key.End | Key.ShiftMask | Key.CtrlMask:
-			case Key.E | Key.ShiftMask | Key.CtrlMask:
-				MoveEndExtend ();
-				break;
-
-			// Home, C-A
-			case Key.Home:
-			case Key.Home | Key.CtrlMask:
-			case Key.A | Key.CtrlMask:
-				MoveHome ();
-				break;
-
-			case Key.CursorLeft | Key.ShiftMask:
-			case Key.CursorUp | Key.ShiftMask:
-				MoveLeftExtend ();
-				break;
-
-			case Key.CursorRight | Key.ShiftMask:
-			case Key.CursorDown | Key.ShiftMask:
-				MoveRightExtend ();
-				break;
-
-			case Key.CursorLeft | Key.ShiftMask | Key.CtrlMask:
-			case Key.CursorUp | Key.ShiftMask | Key.CtrlMask:
-			case (Key)((int)'B' + Key.ShiftMask | Key.AltMask):
-				MoveWordLeftExtend ();
-				break;
-
-			case Key.CursorRight | Key.ShiftMask | Key.CtrlMask:
-			case Key.CursorDown | Key.ShiftMask | Key.CtrlMask:
-			case (Key)((int)'F' + Key.ShiftMask | Key.AltMask):
-				MoveWordRightExtend ();
-				break;
-
-			case Key.CursorLeft:
-			case Key.B | Key.CtrlMask:
-				MoveLeft ();
-				break;
-
-			case Key.End:
-			case Key.End | Key.CtrlMask:
-			case Key.E | Key.CtrlMask: // End
-				MoveEnd ();
-				break;
-
-			case Key.CursorRight:
-			case Key.F | Key.CtrlMask:
-				MoveRight ();
-				break;
-
-			case Key.K | Key.CtrlMask: // kill-to-end
-				KillToEnd ();
-				break;
-
-			case Key.K | Key.AltMask: // kill-to-start
-				KillToStart ();
-				break;
-
-			// Undo
-			case Key.Z | Key.CtrlMask:
-			case Key.Backspace | Key.AltMask:
-				UndoChanges ();
-				break;
-
-			//Redo
-			case Key.Y | Key.CtrlMask: // Control-y, yank
-				RedoChanges ();
-				break;
-
-			case Key.CursorLeft | Key.CtrlMask:
-			case Key.CursorUp | Key.CtrlMask:
-			case (Key)((int)'B' + Key.AltMask):
-				MoveWordLeft ();
-				break;
-
-			case Key.CursorRight | Key.CtrlMask:
-			case Key.CursorDown | Key.CtrlMask:
-			case (Key)((int)'F' + Key.AltMask):
-				MoveWordRight ();
-				break;
-
-			case Key.DeleteChar | Key.CtrlMask: // kill-word-forwards
-				KillWordForwards ();
-				break;
-
-			case Key.Backspace | Key.CtrlMask: // kill-word-backwards
-				KillWordBackwards ();
-				break;
-
-			case Key.InsertChar:
-				InsertChar ();
-				break;
-
-			case Key.C | Key.CtrlMask:
-				Copy ();
-				break;
-
-			case Key.X | Key.CtrlMask:
-				Cut ();
-				break;
-
-			case Key.V | Key.CtrlMask:
-				Paste ();
-				break;
-
-			// MISSING:
-			// Alt-D, Alt-backspace
-			// Alt-Y
-			// Delete adding to kill buffer
-
-			default:
-				// Ignore other control characters.
-				if (kb.Key < Key.Space || kb.Key > Key.CharMask)
-					return false;
-
-				if (ReadOnly)
-					return true;
-
-				if (length > 0) {
-					DeleteSelectedText ();
-					oldCursorPos = point;
-				}
-				var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key));
-				if (Used) {
-					point++;
-					if (point == text.Count + 1) {
-						SetText (text.Concat (kbstr).ToList ());
-					} else {
-						if (oldCursorPos > text.Count) {
-							oldCursorPos = text.Count;
-						}
-						SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.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))));
-					point++;
-				}
-				Adjust ();
+			// Give autocomplete first opportunity to respond to key presses
+			if (SelectedLength == 0 && Autocomplete.ProcessKey (kb)) {
 				return true;
 			}
+
+			var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (kb),
+				new KeyModifiers () { Alt = kb.IsAlt, Ctrl = kb.IsCtrl, Shift = kb.IsShift }));
+			if (result != null)
+				return (bool)result;
+
+			// Ignore other control characters.
+			if (kb.Key < Key.Space || kb.Key > Key.CharMask)
+				return false;
+
+			if (ReadOnly)
+				return true;
+
+			InsertText (kb);
+
 			return true;
 		}
 
-		void InsertChar ()
+		void InsertText (KeyEvent kb, bool useOldCursorPos = true)
 		{
-			Used = !Used;
+			if (length > 0) {
+				DeleteSelectedText ();
+				oldCursorPos = point;
+			}
+			if (!useOldCursorPos) {
+				oldCursorPos = point;
+			}
+			var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key));
+			if (Used) {
+				point++;
+				if (point == text.Count + 1) {
+					SetText (text.Concat (kbstr).ToList ());
+				} else {
+					if (oldCursorPos > text.Count) {
+						oldCursorPos = text.Count;
+					}
+					SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.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))));
+				point++;
+			}
+			Adjust ();
+		}
+
+		void SetOverwrite (bool overwrite)
+		{
+			Used = overwrite;
 			SetNeedsDisplay ();
 		}
 
-		void KillWordBackwards ()
+		/// <summary>
+		/// Deletes word backwards.
+		/// </summary>
+		public virtual void KillWordBackwards ()
 		{
 			ClearAllSelection ();
 			int bw = WordBackward (point);
@@ -508,7 +525,10 @@ namespace Terminal.Gui {
 			Adjust ();
 		}
 
-		void KillWordForwards ()
+		/// <summary>
+		/// Deletes word forwards.
+		/// </summary>
+		public virtual void KillWordForwards ()
 		{
 			ClearAllSelection ();
 			int fw = WordForward (point);
@@ -702,7 +722,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		void DeleteCharLeft ()
+		/// <summary>
+		/// Deletes the left character.
+		/// </summary>
+		public virtual void DeleteCharLeft (bool useOldCursorPos = true)
 		{
 			if (ReadOnly)
 				return;
@@ -711,6 +734,9 @@ namespace Terminal.Gui {
 				if (point == 0)
 					return;
 
+				if (!useOldCursorPos) {
+					oldCursorPos = point;
+				}
 				point--;
 				if (oldCursorPos < text.Count) {
 					SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - oldCursorPos)));
@@ -723,7 +749,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		void DeleteCharRight ()
+		/// <summary>
+		/// Deletes the right character.
+		/// </summary>
+		public virtual void DeleteCharRight ()
 		{
 			if (ReadOnly)
 				return;
@@ -868,6 +897,11 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			// Give autocomplete first opportunity to respond to mouse clicks
+			if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) {
+				return true;
+			}
+
 			if (ev.Flags == MouseFlags.Button1Pressed) {
 				EnsureHasFocus ();
 				PositionCursor (ev);
@@ -1099,6 +1133,29 @@ namespace Terminal.Gui {
 
 			return base.OnEnter (view);
 		}
+
+		/// <summary>
+		/// Inserts the given <paramref name="toAdd"/> text at the current cursor position
+		/// exactly as if the user had just typed it
+		/// </summary>
+		/// <param name="toAdd">Text to add</param>
+		/// <param name="useOldCursorPos">If uses the <see cref="oldCursorPos"/>.</param>
+		public void InsertText (string toAdd, bool useOldCursorPos = true)
+		{
+			foreach (var ch in toAdd) {
+
+				Key key;
+
+				try {
+					key = (Key)ch;
+				} catch (Exception) {
+
+					throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key");
+				}
+
+				InsertText (new KeyEvent () { Key = key }, useOldCursorPos);
+			}
+		}
 	}
 
 	/// <summary>
@@ -1123,4 +1180,33 @@ namespace Terminal.Gui {
 			NewText = newText;
 		}
 	}
+
+	/// <summary>
+	/// Renders an overlay on another view at a given point that allows selecting
+	/// from a range of 'autocomplete' options.
+	/// An implementation on a TextField.
+	/// </summary>
+	public class TextFieldAutocomplete : Autocomplete {
+
+		/// <inheritdoc/>
+		protected override void DeleteTextBackwards ()
+		{
+			((TextField)HostControl).DeleteCharLeft (false);
+		}
+
+		/// <inheritdoc/>
+		protected override string GetCurrentWord ()
+		{
+			var host = (TextField)HostControl;
+			var currentLine = host.Text.ToRuneList ();
+			var cursorPosition = Math.Min (host.CursorPosition, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition);
+		}
+
+		/// <inheritdoc/>
+		protected override void InsertText (string accepted)
+		{
+			((TextField)HostControl).InsertText (accepted, false);
+		}
+	}
 }

+ 35 - 19
Terminal.Gui/Views/TextValidateField.cs

@@ -391,6 +391,25 @@ namespace Terminal.Gui {
 		{
 			Height = 1;
 			CanFocus = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.LeftHome, () => { HomeKeyHandler (); return true; });
+			AddCommand (Command.RightEnd, () => { EndKeyHandler (); return true; });
+			AddCommand (Command.DeleteCharRight, () => { DeleteKeyHandler (); return true; });
+			AddCommand (Command.DeleteCharLeft, () => { BackspaceKeyHandler (); return true; });
+			AddCommand (Command.Left, () => { CursorLeft (); return true; });
+			AddCommand (Command.Right, () => { CursorRight (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.End, Command.RightEnd);
+
+			AddKeyBinding (Key.Delete, Command.DeleteCharRight);
+			AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
+
+			AddKeyBinding (Key.Backspace, Command.DeleteCharLeft);
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.CursorRight, Command.Right);
 		}
 
 		/// <summary>
@@ -446,7 +465,7 @@ namespace Terminal.Gui {
 			}
 		}
 
-		///inheritdoc/>
+		///<inheritdoc/>
 		public override void PositionCursor ()
 		{
 			var (left, _) = GetMargins (Frame.Width);
@@ -526,6 +545,7 @@ namespace Terminal.Gui {
 		{
 			var current = cursorPosition;
 			cursorPosition = provider.CursorLeft (cursorPosition);
+			SetNeedsDisplay ();
 			return current != cursorPosition;
 		}
 
@@ -537,6 +557,7 @@ namespace Terminal.Gui {
 		{
 			var current = cursorPosition;
 			cursorPosition = provider.CursorRight (cursorPosition);
+			SetNeedsDisplay ();
 			return current != cursorPosition;
 		}
 
@@ -551,6 +572,7 @@ namespace Terminal.Gui {
 			}
 			cursorPosition = provider.CursorLeft (cursorPosition);
 			provider.Delete (cursorPosition);
+			SetNeedsDisplay ();
 			return true;
 		}
 
@@ -564,6 +586,7 @@ namespace Terminal.Gui {
 				cursorPosition = provider.CursorLeft (cursorPosition);
 			}
 			provider.Delete (cursorPosition);
+			SetNeedsDisplay ();
 			return true;
 		}
 
@@ -574,6 +597,7 @@ namespace Terminal.Gui {
 		bool HomeKeyHandler ()
 		{
 			cursorPosition = provider.CursorStart ();
+			SetNeedsDisplay ();
 			return true;
 		}
 
@@ -584,6 +608,7 @@ namespace Terminal.Gui {
 		bool EndKeyHandler ()
 		{
 			cursorPosition = provider.CursorEnd ();
+			SetNeedsDisplay ();
 			return true;
 		}
 
@@ -594,30 +619,21 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			switch (kb.Key) {
-			case Key.Home: HomeKeyHandler (); break;
-			case Key.End: EndKeyHandler (); break;
-			case Key.Delete:
-			case Key.DeleteChar: DeleteKeyHandler (); break;
-			case Key.Backspace: BackspaceKeyHandler (); break;
-			case Key.CursorLeft: CursorLeft (); break;
-			case Key.CursorRight: CursorRight (); break;
-			default:
-				if (kb.Key < Key.Space || kb.Key > Key.CharMask)
-					return false;
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
 
-				var key = new Rune ((uint)kb.KeyValue);
+			if (kb.Key < Key.Space || kb.Key > Key.CharMask)
+				return false;
 
-				var inserted = provider.InsertAt ((char)key, cursorPosition);
+			var key = new Rune ((uint)kb.KeyValue);
 
-				if (inserted) {
-					CursorRight ();
-				}
+			var inserted = provider.InsertAt ((char)key, cursorPosition);
 
-				break;
+			if (inserted) {
+				CursorRight ();
 			}
 
-			SetNeedsDisplay ();
 			return true;
 		}
 

File diff suppressed because it is too large
+ 827 - 492
Terminal.Gui/Views/TextView.cs


+ 103 - 60
Terminal.Gui/Views/TimeField.cs

@@ -26,8 +26,8 @@ namespace Terminal.Gui {
 		string longFormat;
 		string shortFormat;
 
-		int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } }
-		string Format { get { return isShort ? shortFormat : longFormat; } }
+		int fieldLen => isShort ? shortFieldLen : longFieldLen;
+		string format => isShort ? shortFormat : longFormat;
 
 		/// <summary>
 		///   TimeChanged event, raised when the Date has changed.
@@ -49,8 +49,7 @@ namespace Terminal.Gui {
 		/// <param name="isShort">If true, the seconds are hidden. Sets the <see cref="IsShortFormat"/> property.</param>
 		public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "")
 		{
-			this.isShort = isShort;
-			Initialize (time);
+			Initialize (time, isShort);
 		}
 
 		/// <summary>
@@ -59,8 +58,7 @@ namespace Terminal.Gui {
 		/// <param name="time">Initial time</param>
 		public TimeField (TimeSpan time) : base (string.Empty)
 		{
-			this.isShort = true;
-			Width = FieldLen + 2;
+			Width = fieldLen + 2;
 			Initialize (time);
 		}
 
@@ -69,21 +67,49 @@ namespace Terminal.Gui {
 		/// </summary>
 		public TimeField () : this (time: TimeSpan.MinValue) { }
 
-		void Initialize (TimeSpan time)
+		void Initialize (TimeSpan time, bool isShort = false)
 		{
 			CultureInfo cultureInfo = CultureInfo.CurrentCulture;
 			sepChar = cultureInfo.DateTimeFormat.TimeSeparator;
 			longFormat = $" hh\\{sepChar}mm\\{sepChar}ss";
 			shortFormat = $" hh\\{sepChar}mm";
-			CursorPosition = 1;
+			this.isShort = isShort;
 			Time = time;
+			CursorPosition = 1;
 			TextChanged += TextField_TextChanged;
+
+			// Things this view knows how to do
+			AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
+			AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; });
+			AddCommand (Command.LeftHome, () => MoveHome ());
+			AddCommand (Command.Left, () => MoveLeft ());
+			AddCommand (Command.RightEnd, () => MoveEnd ());
+			AddCommand (Command.Right, () => MoveRight ());
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight);
+			AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight);
+
+			AddKeyBinding (Key.Delete, Command.DeleteCharLeft);
+			AddKeyBinding (Key.Backspace, Command.DeleteCharLeft);
+
+			AddKeyBinding (Key.Home, Command.LeftHome);
+			AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome);
+
+			AddKeyBinding (Key.CursorLeft, Command.Left);
+			AddKeyBinding (Key.B | Key.CtrlMask, Command.Left);
+
+			AddKeyBinding (Key.End, Command.RightEnd);
+			AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd);
+
+			AddKeyBinding (Key.CursorRight, Command.Right);
+			AddKeyBinding (Key.F | Key.CtrlMask, Command.Right);
 		}
 
 		void TextField_TextChanged (ustring e)
 		{
 			try {
-				if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))
+				if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))
 					Text = e;
 			} catch (Exception) {
 				Text = e;
@@ -105,8 +131,8 @@ namespace Terminal.Gui {
 
 				var oldTime = time;
 				time = value;
-				this.Text = " " + value.ToString (Format.Trim ());
-				var args = new DateTimeEventArgs<TimeSpan> (oldTime, value, Format);
+				this.Text = " " + value.ToString (format.Trim ());
+				var args = new DateTimeEventArgs<TimeSpan> (oldTime, value, format);
 				if (oldTime != value) {
 					OnTimeChanged (args);
 				}
@@ -133,12 +159,20 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <inheritdoc/>
+		public override int CursorPosition {
+			get => base.CursorPosition;
+			set {
+				base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1);
+			}
+		}
+
 		bool SetText (Rune key)
 		{
 			var text = TextModel.ToRunes (Text);
 			var newText = text.GetRange (0, CursorPosition);
 			newText.Add (key);
-			if (CursorPosition < FieldLen)
+			if (CursorPosition < fieldLen)
 				newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
 			return SetText (ustring.Make (newText));
 		}
@@ -183,7 +217,7 @@ namespace Terminal.Gui {
 			}
 			string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}";
 
-			if (!TimeSpan.TryParseExact (t.Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
+			if (!TimeSpan.TryParseExact (t.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
 				!isValidTime)
 				return false;
 			Time = result;
@@ -192,7 +226,7 @@ namespace Terminal.Gui {
 
 		void IncCursorPosition ()
 		{
-			if (CursorPosition == FieldLen)
+			if (CursorPosition == fieldLen)
 				return;
 			if (Text [++CursorPosition] == sepChar.ToCharArray () [0])
 				CursorPosition++;
@@ -215,60 +249,69 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			switch (kb.Key) {
-			case Key.DeleteChar:
-			case Key.D | Key.CtrlMask:
-				if (ReadOnly)
-					return true;
+			var result = InvokeKeybindings (kb);
+			if (result != null)
+				return (bool)result;
 
-				SetText ('0');
-				break;
-
-			case Key.Delete:
-			case Key.Backspace:
-				if (ReadOnly)
-					return true;
+			// Ignore non-numeric characters.
+			if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9))
+				return false;
 
-				SetText ('0');
-				DecCursorPosition ();
-				break;
+			if (ReadOnly)
+				return true;
 
-			// Home, C-A
-			case Key.Home:
-			case Key.A | Key.CtrlMask:
-				CursorPosition = 1;
-				break;
-
-			case Key.CursorLeft:
-			case Key.B | Key.CtrlMask:
-				DecCursorPosition ();
-				break;
-
-			case Key.End:
-			case Key.E | Key.CtrlMask: // End
-				CursorPosition = FieldLen;
-				break;
-
-			case Key.CursorRight:
-			case Key.F | Key.CtrlMask:
+			if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
 				IncCursorPosition ();
-				break;
 
-			default:
-				// Ignore non-numeric characters.
-				if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9))
-					return false;
+			return true;
+		}
 
-				if (ReadOnly)
-					return true;
+		bool MoveRight ()
+		{
+			IncCursorPosition ();
+			return true;
+		}
 
-				if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
-					IncCursorPosition ();
-				return true;
-			}
+		bool MoveEnd ()
+		{
+			CursorPosition = fieldLen;
 			return true;
 		}
 
+		bool MoveLeft ()
+		{
+			DecCursorPosition ();
+			return true;
+		}
+
+		bool MoveHome ()
+		{
+			// Home, C-A
+			CursorPosition = 1;
+			return true;
+		}
+
+		/// <inheritdoc/>
+		public override void DeleteCharLeft (bool useOldCursorPos = true)
+		{
+			if (ReadOnly)
+				return;
+
+			SetText ('0');
+			DecCursorPosition ();
+			return;
+		}
+
+		/// <inheritdoc/>
+		public override void DeleteCharRight ()
+		{
+			if (ReadOnly)
+				return;
+
+			SetText ('0');
+			return;
+		}
+
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent ev)
 		{
@@ -278,8 +321,8 @@ namespace Terminal.Gui {
 				SetFocus ();
 
 			var point = ev.X;
-			if (point > FieldLen)
-				point = FieldLen;
+			if (point > fieldLen)
+				point = fieldLen;
 			if (point < 1)
 				point = 1;
 			CursorPosition = point;

+ 172 - 90
Terminal.Gui/Views/TreeView.cs

@@ -2,11 +2,11 @@
 // by [email protected]).  Phillip has explicitly granted permission for his design
 // and code to be used in this library under the MIT license.
 
+using NStack;
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
-using NStack;
 using Terminal.Gui.Trees;
 
 namespace Terminal.Gui {
@@ -122,7 +122,15 @@ namespace Terminal.Gui {
 		/// Key which when pressed triggers <see cref="TreeView{T}.ObjectActivated"/>.
 		/// Defaults to Enter
 		/// </summary>
-		public Key ObjectActivationKey { get; set; } = Key.Enter;
+		public Key ObjectActivationKey {
+			get => objectActivationKey;
+			set {
+				if (objectActivationKey != value) {
+					ReplaceKeyBinding (ObjectActivationKey, value);
+					objectActivationKey = value;
+				}
+			}
+		}
 
 		/// <summary>
 		/// Mouse event to trigger <see cref="TreeView{T}.ObjectActivated"/>.
@@ -148,6 +156,7 @@ namespace Terminal.Gui {
 		/// (nodes added but no tree builder set)
 		/// </summary>
 		public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set";
+		private Key objectActivationKey = Key.Enter;
 
 		/// <summary>
 		/// Called when the <see cref="SelectedObject"/> changes
@@ -227,6 +236,54 @@ namespace Terminal.Gui {
 		public TreeView () : base ()
 		{
 			CanFocus = true;
+
+			// Things this view knows how to do
+			AddCommand (Command.PageUp, () => { MovePageUp (false); return true; });
+			AddCommand (Command.PageDown, () => { MovePageDown (false); return true; });
+			AddCommand (Command.PageUpExtend, () => { MovePageUp (true); return true; });
+			AddCommand (Command.PageDownExtend, () => { MovePageDown (true); return true; });
+			AddCommand (Command.Expand, () => { Expand (); return true; });
+			AddCommand (Command.ExpandAll, () => { ExpandAll (SelectedObject); return true; });
+			AddCommand (Command.Collapse, () => { CursorLeft (false); return true; });
+			AddCommand (Command.CollapseAll, () => { CursorLeft (true); return true; });
+			AddCommand (Command.LineUp, () => { AdjustSelection (-1, false); return true; });
+			AddCommand (Command.LineUpExtend, () => { AdjustSelection (-1, true); return true; });
+			AddCommand (Command.LineUpToFirstBranch, () => { AdjustSelectionToBranchStart (); return true; });
+
+			AddCommand (Command.LineDown, () => { AdjustSelection (1, false); return true; });
+			AddCommand (Command.LineDownExtend, () => { AdjustSelection (1, true); return true; });
+			AddCommand (Command.LineDownToLastBranch, () => { AdjustSelectionToBranchEnd (); return true; });
+
+			AddCommand (Command.TopHome, () => { GoToFirst (); return true; });
+			AddCommand (Command.BottomEnd, () => { GoToEnd (); return true; });
+			AddCommand (Command.SelectAll, () => { SelectAll (); return true; });
+
+			AddCommand (Command.ScrollUp, () => { ScrollUp (); return true; });
+			AddCommand (Command.ScrollDown, () => { ScrollDown (); return true; });
+			AddCommand (Command.Accept, () => { ActivateSelectedObjectIfAny (); return true; });
+
+			// Default keybindings for this view
+			AddKeyBinding (Key.PageUp, Command.PageUp);
+			AddKeyBinding (Key.PageDown, Command.PageDown);
+			AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend);
+			AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend);
+			AddKeyBinding (Key.CursorRight, Command.Expand);
+			AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.ExpandAll);
+			AddKeyBinding (Key.CursorLeft, Command.Collapse);
+			AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.CollapseAll);
+
+			AddKeyBinding (Key.CursorUp, Command.LineUp);
+			AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend);
+			AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.LineUpToFirstBranch);
+
+			AddKeyBinding (Key.CursorDown, Command.LineDown);
+			AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend);
+			AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.LineDownToLastBranch);
+
+			AddKeyBinding (Key.Home, Command.TopHome);
+			AddKeyBinding (Key.End, Command.BottomEnd);
+			AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll);
+			AddKeyBinding (ObjectActivationKey, Command.Accept);
 		}
 
 		/// <summary>
@@ -504,87 +561,98 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
-			if (keyEvent.Key == ObjectActivationKey) {
-				var o = SelectedObject;
+			if (!Enabled) {
+				return false;
+			}
 
-				if (o != null) {
-					OnObjectActivated (new ObjectActivatedEventArgs<T> (this, o));
+			// if it is a single character pressed without any control keys
+			if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) {
 
-					PositionCursor ();
+				if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) {
+					AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue);
 					return true;
 				}
 			}
 
-			if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) {
+			try {
+				var result = InvokeKeybindings (keyEvent);
+				if (result != null)
+					return (bool)result;
+			} finally {
 
-				var character = (char)keyEvent.KeyValue;
+				PositionCursor ();
+			}
 
-				// if it is a single character pressed without any control keys
-				if (char.IsLetterOrDigit (character) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) {
-					// search for next branch that begins with that letter
-					var characterAsStr = character.ToString ();
-					AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, StringComparison.CurrentCultureIgnoreCase));
+			return base.ProcessKey (keyEvent);
+		}
 
-					PositionCursor ();
-					return true;
-				}
-
-			}
 
+		/// <summary>
+		/// <para>Triggers the <see cref="ObjectActivated"/> event with the <see cref="SelectedObject"/>.</para>
+		/// 
+		/// <para>This method also ensures that the selected object is visible</para>
+		/// </summary>
+		public void ActivateSelectedObjectIfAny ()
+		{
+			var o = SelectedObject;
 
-			switch (keyEvent.Key) {
-
-			case Key.CursorRight:
-				Expand (SelectedObject);
-				break;
-			case Key.CursorRight | Key.CtrlMask:
-				ExpandAll (SelectedObject);
-				break;
-			case Key.CursorLeft:
-			case Key.CursorLeft | Key.CtrlMask:
-				CursorLeft (keyEvent.Key.HasFlag (Key.CtrlMask));
-				break;
-
-			case Key.CursorUp:
-			case Key.CursorUp | Key.ShiftMask:
-				AdjustSelection (-1, keyEvent.Key.HasFlag (Key.ShiftMask));
-				break;
-			case Key.CursorDown:
-			case Key.CursorDown | Key.ShiftMask:
-				AdjustSelection (1, keyEvent.Key.HasFlag (Key.ShiftMask));
-				break;
-			case Key.CursorUp | Key.CtrlMask:
-				AdjustSelectionToBranchStart ();
-				break;
-			case Key.CursorDown | Key.CtrlMask:
-				AdjustSelectionToBranchEnd ();
-				break;
-			case Key.PageUp:
-			case Key.PageUp | Key.ShiftMask:
-				AdjustSelection (-Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask));
-				break;
-
-			case Key.PageDown:
-			case Key.PageDown | Key.ShiftMask:
-				AdjustSelection (Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask));
-				break;
-			case Key.A | Key.CtrlMask:
-				SelectAll ();
-				break;
-			case Key.Home:
-				GoToFirst ();
-				break;
-			case Key.End:
-				GoToEnd ();
-				break;
-
-			default:
-				// we don't care about this keystroke
-				return false;
+			if (o != null) {
+				OnObjectActivated (new ObjectActivatedEventArgs<T> (this, o));
+				PositionCursor ();
 			}
+		}
+
+		/// <summary>
+		/// <para>Moves the <see cref="SelectedObject"/> to the next item that begins with <paramref name="character"/></para>
+		/// <para>This method will loop back to the start of the tree if reaching the end without finding a match</para>
+		/// </summary>
+		/// <param name="character">The first character of the next item you want selected</param>
+		/// <param name="caseSensitivity">Case sensitivity of the search</param>
+		public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase)
+		{
+			// search for next branch that begins with that letter
+			var characterAsStr = character.ToString ();
+			AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity));
 
 			PositionCursor ();
-			return true;
+		}
+
+		/// <summary>
+		/// Moves the selection up by the height of the control (1 page).
+		/// </summary>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <exception cref="NotImplementedException"></exception>
+		public void MovePageUp (bool expandSelection = false)
+		{
+			AdjustSelection (-Bounds.Height, expandSelection);
+		}
+
+		/// <summary>
+		/// Moves the selection down by the height of the control (1 page).
+		/// </summary>
+		/// <param name="expandSelection">True if the navigation should add the covered nodes to the selected current selection</param>
+		/// <exception cref="NotImplementedException"></exception>
+		public void MovePageDown (bool expandSelection = false)
+		{
+			AdjustSelection (Bounds.Height, expandSelection);
+		}
+
+		/// <summary>
+		/// Scrolls the view area down a single line without changing the current selection
+		/// </summary>
+		public void ScrollDown ()
+		{
+			ScrollOffsetVertical++;
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Scrolls the view area up a single line without changing the current selection
+		/// </summary>
+		public void ScrollUp ()
+		{
+			ScrollOffsetVertical--;
+			SetNeedsDisplay ();
 		}
 
 		/// <summary>
@@ -618,13 +686,11 @@ namespace Terminal.Gui {
 
 			if (me.Flags == MouseFlags.WheeledDown) {
 
-				ScrollOffsetVertical++;
-				SetNeedsDisplay ();
+				ScrollDown ();
 
 				return true;
 			} else if (me.Flags == MouseFlags.WheeledUp) {
-				ScrollOffsetVertical--;
-				SetNeedsDisplay ();
+				ScrollUp ();
 
 				return true;
 			}
@@ -736,7 +802,7 @@ namespace Terminal.Gui {
 			if (CanFocus && HasFocus && Visible && SelectedObject != null) {
 
 				var map = BuildLineMap ();
-				var idx = map.IndexOf(b => b.Model.Equals (SelectedObject));
+				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
 
 				// if currently selected line is visible
 				if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) {
@@ -839,7 +905,7 @@ namespace Terminal.Gui {
 			} else {
 				var map = BuildLineMap ();
 
-				var idx = map.IndexOf(b => b.Model.Equals (SelectedObject));
+				var idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
 
 				if (idx == -1) {
 
@@ -848,7 +914,7 @@ namespace Terminal.Gui {
 				} else {
 					var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
 
-					var newBranch = map.ElementAt(newIdx);
+					var newBranch = map.ElementAt (newIdx);
 
 					// If it is a multi selection
 					if (expandSelection && MultiSelect) {
@@ -858,7 +924,7 @@ namespace Terminal.Gui {
 							multiSelectedRegions.Push (new TreeSelection<T> (head.Origin, newIdx, map));
 						} else {
 							// or start a new multi selection region
-							multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt(idx), newIdx, map));
+							multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (idx), newIdx, map));
 						}
 					}
 
@@ -884,13 +950,13 @@ namespace Terminal.Gui {
 
 			var map = BuildLineMap ();
 
-			int currentIdx = map.IndexOf(b => Equals (b.Model, o));
+			int currentIdx = map.IndexOf (b => Equals (b.Model, o));
 
 			if (currentIdx == -1) {
 				return;
 			}
 
-			var currentBranch = map.ElementAt(currentIdx);
+			var currentBranch = map.ElementAt (currentIdx);
 			var next = currentBranch;
 
 			for (; currentIdx >= 0; currentIdx--) {
@@ -905,7 +971,7 @@ namespace Terminal.Gui {
 
 				// look at next branch up for consideration
 				currentBranch = next;
-				next = map.ElementAt(currentIdx);
+				next = map.ElementAt (currentIdx);
 			}
 
 			// We ran all the way to top of tree
@@ -924,13 +990,13 @@ namespace Terminal.Gui {
 
 			var map = BuildLineMap ();
 
-			int currentIdx = map.IndexOf(b => Equals (b.Model, o));
+			int currentIdx = map.IndexOf (b => Equals (b.Model, o));
 
 			if (currentIdx == -1) {
 				return;
 			}
 
-			var currentBranch = map.ElementAt(currentIdx);
+			var currentBranch = map.ElementAt (currentIdx);
 			var next = currentBranch;
 
 			for (; currentIdx < map.Count; currentIdx++) {
@@ -945,7 +1011,7 @@ namespace Terminal.Gui {
 
 				// look at next branch for consideration
 				currentBranch = next;
-				next = map.ElementAt(currentIdx);
+				next = map.ElementAt (currentIdx);
 			}
 
 			GoToEnd ();
@@ -970,7 +1036,7 @@ namespace Terminal.Gui {
 
 			// or the current selected branch
 			if (SelectedObject != null) {
-				idxStart = map.IndexOf(b => Equals (b.Model, SelectedObject));
+				idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject));
 			}
 
 			// if currently selected object mysteriously vanished, search from beginning
@@ -980,9 +1046,9 @@ namespace Terminal.Gui {
 
 			// loop around all indexes and back to first index
 			for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) {
-				if (predicate (map.ElementAt(idxCur))) {
-					SelectedObject = map.ElementAt(idxCur).Model;
-					EnsureVisible (map.ElementAt(idxCur).Model);
+				if (predicate (map.ElementAt (idxCur))) {
+					SelectedObject = map.ElementAt (idxCur).Model;
+					EnsureVisible (map.ElementAt (idxCur).Model);
 					SetNeedsDisplay ();
 					return;
 				}
@@ -997,7 +1063,7 @@ namespace Terminal.Gui {
 		{
 			var map = BuildLineMap ();
 
-			var idx = map.IndexOf(b => Equals (b.Model, model));
+			var idx = map.IndexOf (b => Equals (b.Model, model));
 
 			if (idx == -1) {
 				return;
@@ -1017,6 +1083,14 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Expands the currently <see cref="SelectedObject"/>
+		/// </summary>
+		public void Expand ()
+		{
+			Expand (SelectedObject);
+		}
+
 		/// <summary>
 		/// Expands the supplied object if it is contained in the tree (either as a root object or 
 		/// as an exposed branch object)
@@ -1082,6 +1156,14 @@ namespace Terminal.Gui {
 			return ObjectToBranch (o)?.IsExpanded ?? false;
 		}
 
+		/// <summary>
+		/// Collapses the <see cref="SelectedObject"/>
+		/// </summary>
+		public void Collapse ()
+		{
+			Collapse (selectedObject);
+		}
+
 		/// <summary>
 		/// Collapses the supplied object if it is currently expanded 
 		/// </summary>
@@ -1224,7 +1306,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt(0), map.Count, map));
+			multiSelectedRegions.Push (new TreeSelection<T> (map.ElementAt (0), map.Count, map));
 			SetNeedsDisplay ();
 
 			OnSelectionChanged (new SelectionChangedEventArgs<T> (this, SelectedObject, SelectedObject));
@@ -1258,7 +1340,7 @@ namespace Terminal.Gui {
 			Origin = from;
 			included.Add (Origin.Model);
 
-			var oldIdx = map.IndexOf(from);
+			var oldIdx = map.IndexOf (from);
 
 			var lowIndex = Math.Min (oldIdx, toIndex);
 			var highIndex = Math.Max (oldIdx, toIndex);

+ 199 - 0
UICatalog/KeyBindingsDialog.cs

@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Terminal.Gui;
+
+namespace UICatalog {
+
+
+	class KeyBindingsDialog : Dialog {
+
+
+		static Dictionary<Command,Key> CurrentBindings = new Dictionary<Command,Key>();
+		private Command[] commands;
+		private ListView commandsListView;
+		private Label keyLabel;
+
+		/// <summary>
+		/// Tracks views as they are created in UICatalog so that their keybindings can
+		/// be managed.
+		/// </summary>
+		private class ViewTracker {
+
+			public static ViewTracker Instance;
+
+			/// <summary>
+			/// All views seen so far and a bool to indicate if we have applied keybindings to them
+			/// </summary>
+			Dictionary<View, bool> knownViews = new Dictionary<View, bool> ();
+
+			private object lockKnownViews = new object ();
+			private Dictionary<Command, Key> keybindings;
+
+			public ViewTracker (View top)
+			{
+				RecordView (top);
+
+				// Refresh known windows
+				Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), (m) => {
+
+					lock (lockKnownViews) {
+						RecordView (Application.Top);
+
+						ApplyKeyBindingsToAllKnownViews ();
+					}
+
+					return true;
+				});
+			}
+
+			private void RecordView (View view)
+			{
+				if (!knownViews.ContainsKey (view)) {
+					knownViews.Add (view, false);
+				}
+
+				// may already have subviews that were added to it
+				// before we got to it
+				foreach (var sub in view.Subviews) {
+					RecordView (sub);
+				}
+
+				view.Added += RecordView;
+			}
+
+			internal static void Initialize ()
+			{
+				Instance = new ViewTracker (Application.Top);
+			}
+
+			internal void StartUsingNewKeyMap (Dictionary<Command, Key> currentBindings)
+			{
+				lock (lockKnownViews) {
+
+					// change our knowledge of what keys to bind
+					this.keybindings = currentBindings;
+
+					// Mark that we have not applied the key bindings yet to any views
+					foreach (var view in knownViews.Keys) {
+						knownViews [view] = false;
+					}
+				}
+			}
+
+			private void ApplyKeyBindingsToAllKnownViews ()
+			{
+				if(keybindings == null) {
+					return;
+				}
+
+				// Key is the view Value is whether we have already done it
+				foreach (var viewDone in knownViews) {
+
+					var view = viewDone.Key;
+					var done = viewDone.Value;
+
+					if (done) {
+						// we have already applied keybindings to this view
+						continue;
+					}
+
+					var supported = new HashSet<Command>(view.GetSupportedCommands ());
+
+					foreach (var kvp in keybindings) {
+						
+						// if the view supports the keybinding
+						if(supported.Contains(kvp.Key))
+						{
+							// if the key was bound to any other commands clear that
+							view.ClearKeybinding (kvp.Key);
+							view.AddKeyBinding (kvp.Value,kvp.Key);
+						}
+
+						// mark that we have done this view so don't need to set keybindings again on it
+						knownViews [view] = true;
+					}
+				}
+			}
+		}
+
+		public KeyBindingsDialog () : base("Keybindings", 50,10)
+		{
+			if(ViewTracker.Instance == null) {
+				ViewTracker.Initialize ();
+			}
+			
+			// known commands that views can support
+			commands = Enum.GetValues (typeof (Command)).Cast<Command>().ToArray();
+
+			commandsListView = new ListView (commands) {
+				Width = Dim.Percent (50),
+				Height = Dim.Percent (100) - 1,
+			};
+			commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged;
+			Add (commandsListView);
+
+			keyLabel = new Label () {
+				Text = "Key: None",
+				Width = Dim.Fill(),
+				X = Pos.Percent(50),
+				Y = 0
+			};
+			Add (keyLabel);
+
+			var btnChange = new Button ("Change") {
+				X = Pos.Percent (50),
+				Y = 1,
+			};
+			Add (btnChange);
+			btnChange.Clicked += RemapKey;
+
+			var close = new Button ("Ok");
+			close.Clicked += () => {
+				Application.RequestStop ();
+				ViewTracker.Instance.StartUsingNewKeyMap (CurrentBindings);
+			};
+			AddButton (close);
+
+			var cancel = new Button ("Cancel");
+			cancel.Clicked += ()=>Application.RequestStop();
+			AddButton (cancel);
+		}
+
+		private void RemapKey ()
+		{
+			var cmd = commands [commandsListView.SelectedItem];
+			Key? key = null;
+
+			// prompt user to hit a key
+			var dlg = new Dialog ("Enter Key");
+			dlg.KeyPress += (k) => {
+				key = k.KeyEvent.Key;
+				Application.RequestStop ();
+			};
+			Application.Run (dlg);
+
+			if(key.HasValue) {
+				CurrentBindings [cmd] = key.Value;
+				SetTextBoxToShowBinding (cmd);
+			}
+		}
+
+		private void SetTextBoxToShowBinding (Command cmd)
+		{
+			if (CurrentBindings.ContainsKey (cmd)) {
+				keyLabel.Text = "Key: " + CurrentBindings [cmd].ToString ();
+			} else {
+				keyLabel.Text = "Key: None";
+			}
+			SetNeedsDisplay ();
+		}
+
+		private void CommandsListView_SelectedItemChanged (ListViewItemEventArgs obj)
+		{
+			SetTextBoxToShowBinding ((Command)obj.Value);
+		}
+	}
+}

+ 1 - 1
UICatalog/Scenarios/BackgroundWorkerCollection.cs

@@ -367,7 +367,7 @@ namespace UICatalog.Scenarios {
 
 			private void OnReportClosed ()
 			{
-				if (Staging.StartStaging != null) {
+				if (Staging?.StartStaging != null) {
 					ReportClosed?.Invoke (this);
 				}
 				RequestStop ();

+ 18 - 18
UICatalog/Scenarios/BordersComparisons.cs

@@ -41,11 +41,6 @@ namespace UICatalog.Scenarios {
 				X = Pos.Center (),
 				Y = Pos.Center () - 3,
 			};
-			var tf2 = new TextField ("1234567890") {
-				X = Pos.AnchorEnd (10),
-				Y = Pos.AnchorEnd (1),
-				Width = 10
-			};
 			var tv = new TextView () {
 				Y = Pos.AnchorEnd (2),
 				Width = 10,
@@ -53,7 +48,12 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.Dialog,
 				Text = "1234567890"
 			};
-			win.Add (tf1, button, label, tf2, tv);
+			var tf2 = new TextField ("1234567890") {
+				X = Pos.AnchorEnd (10),
+				Y = Pos.AnchorEnd (1),
+				Width = 10
+			};
+			win.Add (tf1, button, label, tv, tf2);
 			top.Add (win);
 
 			var top2 = new Border.ToplevelContainer (new Rect (50, 5, 40, 20),
@@ -81,11 +81,6 @@ namespace UICatalog.Scenarios {
 				X = Pos.Center (),
 				Y = Pos.Center () - 3,
 			};
-			var tf4 = new TextField ("1234567890") {
-				X = Pos.AnchorEnd (10),
-				Y = Pos.AnchorEnd (1),
-				Width = 10
-			};
 			var tv2 = new TextView () {
 				Y = Pos.AnchorEnd (2),
 				Width = 10,
@@ -93,7 +88,12 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.Dialog,
 				Text = "1234567890"
 			};
-			top2.Add (tf3, button2, label2, tf4, tv2);
+			var tf4 = new TextField ("1234567890") {
+				X = Pos.AnchorEnd (10),
+				Y = Pos.AnchorEnd (1),
+				Width = 10
+			};
+			top2.Add (tf3, button2, label2, tv2, tf4);
 			top.Add (top2);
 
 			var frm = new FrameView (new Rect (95, 5, 40, 20), "Test3", null,
@@ -118,11 +118,6 @@ namespace UICatalog.Scenarios {
 				X = Pos.Center (),
 				Y = Pos.Center () - 3,
 			};
-			var tf6 = new TextField ("1234567890") {
-				X = Pos.AnchorEnd (10),
-				Y = Pos.AnchorEnd (1),
-				Width = 10
-			};
 			var tv3 = new TextView () {
 				Y = Pos.AnchorEnd (2),
 				Width = 10,
@@ -130,7 +125,12 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.Dialog,
 				Text = "1234567890"
 			};
-			frm.Add (tf5, button3, label3, tf6, tv3);
+			var tf6 = new TextField ("1234567890") {
+				X = Pos.AnchorEnd (10),
+				Y = Pos.AnchorEnd (1),
+				Width = 10
+			};
+			frm.Add (tf5, button3, label3, tv3, tf6);
 			top.Add (frm);
 
 			Application.Run ();

+ 20 - 1
UICatalog/Scenarios/Text.cs

@@ -1,5 +1,8 @@
-using System;
+using NStack;
+using System;
+using System.Linq;
 using System.Text;
+using System.Text.RegularExpressions;
 using Terminal.Gui;
 using Terminal.Gui.TextValidateProviders;
 
@@ -19,6 +22,14 @@ namespace UICatalog.Scenarios {
 				Width = Dim.Percent (50),
 				//ColorScheme = Colors.Dialog
 			};
+			textField.TextChanging += TextField_TextChanging;
+
+			void TextField_TextChanging (TextChangingEventArgs e)
+			{
+				textField.Autocomplete.AllSuggestions = Regex.Matches (e.NewText.ToString (), "\\w+")
+					.Select (s => s.Value)
+					.Distinct ().ToList ();
+			}
 			Win.Add (textField);
 
 			var labelMirroringTextField = new Label (textField.Text) {
@@ -40,6 +51,14 @@ namespace UICatalog.Scenarios {
 				ColorScheme = Colors.Dialog
 			};
 			textView.Text = s;
+			textView.DrawContent += TextView_DrawContent;
+
+			void TextView_DrawContent (Rect e)
+			{
+				textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+")
+					.Select (s => s.Value)
+					.Distinct ().ToList ();
+			}
 			Win.Add (textView);
 
 			var labelMirroringTextView = new Label (textView.Text) {

+ 186 - 0
UICatalog/Scenarios/TextViewAutocompletePopup.cs

@@ -0,0 +1,186 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "TextView Autocomplete Popup", Description: "Show five TextView Autocomplete Popup effects")]
+	[ScenarioCategory ("Controls")]
+	public class TextViewAutocompletePopup : Scenario {
+
+		TextView textViewTopLeft;
+		TextView textViewTopRight;
+		TextView textViewBottomLeft;
+		TextView textViewBottomRight;
+		TextView textViewCentered;
+		MenuItem miMultiline;
+		MenuItem miWrap;
+		StatusItem siMultiline;
+		StatusItem siWrap;
+		int height = 10;
+
+		public override void Setup ()
+		{
+			Win.Title = GetName ();
+			var width = 20;
+			var colorScheme = Colors.Dialog;
+			var text = " jamp jemp jimp jomp jump";
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_File", new MenuItem [] {
+					miMultiline =  new MenuItem ("_Multiline", "", () => Multiline()){CheckType = MenuItemCheckStyle.Checked},
+					miWrap =  new MenuItem ("_Word Wrap", "", () => WordWrap()){CheckType = MenuItemCheckStyle.Checked},
+					new MenuItem ("_Quit", "", () => Quit())
+				})
+			});
+			Top.Add (menu);
+
+			textViewTopLeft = new TextView () {
+				Width = width,
+				Height = height,
+				ColorScheme = colorScheme,
+				Text = text
+			};
+			textViewTopLeft.DrawContent += TextViewTopLeft_DrawContent;
+			Win.Add (textViewTopLeft);
+
+			textViewTopRight = new TextView () {
+				X = Pos.AnchorEnd (width),
+				Width = width,
+				Height = height,
+				ColorScheme = colorScheme,
+				Text = text
+			};
+			textViewTopRight.DrawContent += TextViewTopRight_DrawContent;
+			Win.Add (textViewTopRight);
+
+			textViewBottomLeft = new TextView () {
+				Y = Pos.AnchorEnd (height),
+				Width = width,
+				Height = height,
+				ColorScheme = colorScheme,
+				Text = text
+			};
+			textViewBottomLeft.DrawContent += TextViewBottomLeft_DrawContent;
+			Win.Add (textViewBottomLeft);
+
+			textViewBottomRight = new TextView () {
+				X = Pos.AnchorEnd (width),
+				Y = Pos.AnchorEnd (height),
+				Width = width,
+				Height = height,
+				ColorScheme = colorScheme,
+				Text = text
+			};
+			textViewBottomRight.DrawContent += TextViewBottomRight_DrawContent;
+			Win.Add (textViewBottomRight);
+
+			textViewCentered = new TextView () {
+				X = Pos.Center (),
+				Y = Pos.Center (),
+				Width = width,
+				Height = height,
+				ColorScheme = colorScheme,
+				Text = text
+			};
+			textViewCentered.DrawContent += TextViewCentered_DrawContent;
+			Win.Add (textViewCentered);
+
+			miMultiline.Checked = textViewTopLeft.Multiline;
+			miWrap.Checked = textViewTopLeft.WordWrap;
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
+				siMultiline = new StatusItem(Key.Null, "", null),
+				siWrap = new StatusItem(Key.Null, "", null)
+			});
+			Top.Add (statusBar);
+
+			Win.LayoutStarted += Win_LayoutStarted;
+		}
+
+		private void Win_LayoutStarted (View.LayoutEventArgs obj)
+		{
+			miMultiline.Checked = textViewTopLeft.Multiline;
+			miWrap.Checked = textViewTopLeft.WordWrap;
+			SetMultilineStatusText ();
+			SetWrapStatusText ();
+
+			if (miMultiline.Checked) {
+				height = 10;
+			} else {
+				height = 1;
+			}
+			textViewBottomLeft.Y = textViewBottomRight.Y = Pos.AnchorEnd (height);
+		}
+
+		private void SetMultilineStatusText ()
+		{
+			siMultiline.Title = $"Multiline: {miMultiline.Checked}";
+		}
+
+		private void SetWrapStatusText ()
+		{
+			siWrap.Title = $"WordWrap: {miWrap.Checked}";
+		}
+
+		private void SetAllSuggestions (TextView view)
+		{
+			view.Autocomplete.AllSuggestions = Regex.Matches (view.Text.ToString (), "\\w+")
+				.Select (s => s.Value)
+				.Distinct ().ToList ();
+		}
+
+		private void TextViewCentered_DrawContent (Rect obj)
+		{
+			SetAllSuggestions (textViewCentered);
+		}
+
+		private void TextViewBottomRight_DrawContent (Rect obj)
+		{
+			SetAllSuggestions (textViewBottomRight);
+		}
+
+		private void TextViewBottomLeft_DrawContent (Rect obj)
+		{
+			SetAllSuggestions (textViewBottomLeft);
+		}
+
+		private void TextViewTopRight_DrawContent (Rect obj)
+		{
+			SetAllSuggestions (textViewTopRight);
+		}
+
+		private void TextViewTopLeft_DrawContent (Rect obj)
+		{
+			SetAllSuggestions (textViewTopLeft);
+		}
+
+		private void Multiline ()
+		{
+			miMultiline.Checked = !miMultiline.Checked;
+			SetMultilineStatusText ();
+			textViewTopLeft.Multiline = miMultiline.Checked;
+			textViewTopRight.Multiline = miMultiline.Checked;
+			textViewBottomLeft.Multiline = miMultiline.Checked;
+			textViewBottomRight.Multiline = miMultiline.Checked;
+			textViewCentered.Multiline = miMultiline.Checked;
+		}
+
+		private void WordWrap ()
+		{
+			miWrap.Checked = !miWrap.Checked;
+			textViewTopLeft.WordWrap = miWrap.Checked;
+			textViewTopRight.WordWrap = miWrap.Checked;
+			textViewBottomLeft.WordWrap = miWrap.Checked;
+			textViewBottomRight.WordWrap = miWrap.Checked;
+			textViewCentered.WordWrap = miWrap.Checked;
+			miWrap.Checked = textViewTopLeft.WordWrap;
+			SetWrapStatusText ();
+		}
+
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
+	}
+}

+ 17 - 0
UICatalog/UICatalog.cs

@@ -313,6 +313,7 @@ namespace UICatalog {
 			menuItems.Add (CreateSizeStyle ());
 			menuItems.Add (CreateAlwaysSetPosition ());
 			menuItems.Add (CreateDisabledEnabledMouse ());
+			menuItems.Add (CreateKeybindings ());
 			return menuItems;
 		}
 
@@ -331,6 +332,22 @@ namespace UICatalog {
 
 			return menuItems.ToArray ();
 		}
+		private static MenuItem[] CreateKeybindings()
+		{
+
+			List<MenuItem> menuItems = new List<MenuItem> ();
+			var item = new MenuItem ();
+			item.Title = "Keybindings";
+			item.Action += () => {
+				var dlg = new KeyBindingsDialog ();
+				Application.Run (dlg);
+			};
+
+			menuItems.Add (null);
+			menuItems.Add (item);
+
+			return menuItems.ToArray ();
+		}
 
 		static MenuItem [] CreateAlwaysSetPosition ()
 		{

+ 9 - 0
UnitTests/ApplicationTests.cs

@@ -369,6 +369,15 @@ namespace Terminal.Gui.Core {
 
 			Application.Run (top);
 
+			// Replacing the defaults keys to avoid errors on others unit tests that are using it.
+			Application.AlternateForwardKey = Key.PageDown | Key.CtrlMask;
+			Application.AlternateBackwardKey = Key.PageUp | Key.CtrlMask;
+			Application.QuitKey = Key.Q | Key.CtrlMask;
+
+			Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey);
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
 		}

+ 108 - 10
UnitTests/AutocompleteTests.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Terminal.Gui;
 using Xunit;
@@ -9,21 +10,21 @@ using Xunit;
 namespace Terminal.Gui.Core {
 	public class AutocompleteTests {
 
-		[Fact][AutoInitShutdown]
-		public void Test_GenerateSuggestions_Simple()
+		[Fact]
+		public void Test_GenerateSuggestions_Simple ()
 		{
-			var ac = new Autocomplete ();
-			ac.AllSuggestions = new List<string> { "fish","const","Cobble"};
+			var ac = new TextViewAutocomplete ();
+			ac.AllSuggestions = new List<string> { "fish", "const", "Cobble" };
 
 			var tv = new TextView ();
 			tv.InsertText ("co");
 
-			ac.GenerateSuggestions (tv);
+			ac.HostControl = tv;
+			ac.GenerateSuggestions ();
 
 			Assert.Equal (2, ac.Suggestions.Count);
-			Assert.Equal ("const", ac.Suggestions[0]);
-			Assert.Equal ("Cobble", ac.Suggestions[1]);
-
+			Assert.Equal ("const", ac.Suggestions [0]);
+			Assert.Equal ("Cobble", ac.Suggestions [1]);
 		}
 
 		[Fact]
@@ -41,7 +42,7 @@ namespace Terminal.Gui.Core {
 				Focus = Application.Driver.MakeAttribute (Color.Black, Color.Cyan),
 			};
 
-			// should be seperate instance
+			// should be separate instance
 			Assert.NotSame (Colors.Menu, tv.Autocomplete.ColorScheme);
 
 			// with the values we set on it
@@ -50,8 +51,105 @@ namespace Terminal.Gui.Core {
 
 			Assert.Equal (Color.Black, tv.Autocomplete.ColorScheme.Focus.Foreground);
 			Assert.Equal (Color.Cyan, tv.Autocomplete.ColorScheme.Focus.Background);
+		}
 
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var tv = new TextView () {
+				Width = 10,
+				Height = 2,
+				Text = " Fortunately super feature."
+			};
+			var top = Application.Top;
+			top.Add (tv);
+			Application.Begin (top);
 
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			Assert.NotNull (tv.Autocomplete);
+			Assert.Empty (tv.Autocomplete.AllSuggestions);
+			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+				.Select (s => s.Value)
+				.Distinct ().ToList ();
+			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.AllSuggestions [0]);
+			Assert.Equal ("super", tv.Autocomplete.AllSuggestions [1]);
+			Assert.Equal ("feature", tv.Autocomplete.AllSuggestions [^1]);
+			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
+			top.Redraw (tv.Bounds);
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			top.Redraw (tv.Bounds);
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (1, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			top.Redraw (tv.Bounds);
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			top.Redraw (tv.Bounds);
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (1, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			top.Redraw (tv.Bounds);
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.Autocomplete.Visible);
+			top.Redraw (tv.Bounds);
+			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
+			Assert.False (tv.Autocomplete.Visible);
+			top.Redraw (tv.Bounds);
+			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.Reopen, new KeyModifiers ())));
+			Assert.Equal ($"F Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
+			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.SelectionKey, new KeyModifiers ())));
+			Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (11, 0), tv.CursorPosition);
+			Assert.Equal (2, tv.Autocomplete.Suggestions.Count);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]);
+			Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]);
+			Assert.Equal (0, tv.Autocomplete.SelectedIdx);
+			Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]);
+			Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ())));
+			Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text);
+			Assert.Equal (new Point (11, 0), tv.CursorPosition);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
 		}
 	}
-}
+}

+ 103 - 0
UnitTests/ButtonTests.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class ButtonTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var btn = new Button ();
+			Assert.Equal (string.Empty, btn.Text);
+			Assert.Equal ("[  ]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ());
+			Assert.False (btn.IsDefault);
+			Assert.Equal (TextAlignment.Centered, btn.TextAlignment);
+			Assert.Equal ('_', btn.HotKeySpecifier);
+			Assert.True (btn.CanFocus);
+			Assert.Equal (new Rect (0, 0, 4, 1), btn.Frame);
+			Assert.Equal (Key.Null, btn.HotKey);
+
+			btn = new Button ("Test", true);
+			Assert.Equal ("Test", btn.Text);
+			Assert.Equal ("[< Test >]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ());
+			Assert.True (btn.IsDefault);
+			Assert.Equal (TextAlignment.Centered, btn.TextAlignment);
+			Assert.Equal ('_', btn.HotKeySpecifier);
+			Assert.True (btn.CanFocus);
+			Assert.Equal (new Rect (0, 0, 10, 1), btn.Frame);
+			Assert.Equal (Key.Null, btn.HotKey);
+
+			btn = new Button (3, 4, "Test", true);
+			Assert.Equal ("Test", btn.Text);
+			Assert.Equal ("[< Test >]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ());
+			Assert.True (btn.IsDefault);
+			Assert.Equal (TextAlignment.Centered, btn.TextAlignment);
+			Assert.Equal ('_', btn.HotKeySpecifier);
+			Assert.True (btn.CanFocus);
+			Assert.Equal (new Rect (3, 4, 10, 1), btn.Frame);
+			Assert.Equal (Key.Null, btn.HotKey);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var clicked = false;
+			Button btn = new Button ("Test");
+			btn.Clicked += () => clicked = true;
+			Application.Top.Add (btn);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (Key.T, btn.HotKey);
+			Assert.False (btn.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ())));
+			Assert.False (clicked);
+			Assert.True (btn.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (clicked);
+			clicked = false;
+			Assert.False (btn.IsDefault);
+			Assert.False (btn.ProcessColdKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.False (clicked);
+			btn.IsDefault = true;
+			Assert.True (btn.ProcessColdKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.True (clicked);
+			clicked = false;
+			Assert.True (btn.ProcessColdKey (new KeyEvent (Key.AltMask | Key.T, new KeyModifiers ())));
+			Assert.True (clicked);
+			clicked = false;
+			Assert.True (btn.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.True (clicked);
+			clicked = false;
+			Assert.True (btn.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
+			Assert.True (clicked);
+			clicked = false;
+			Assert.True (btn.ProcessKey (new KeyEvent ((Key)'t', new KeyModifiers ())));
+			Assert.True (clicked);
+		}
+
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ChangeHotKey ()
+		{
+			var clicked = false;
+			Button btn = new Button ("Test");
+			btn.Clicked += () => clicked = true;
+			Application.Top.Add (btn);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (Key.T, btn.HotKey);
+			Assert.False (btn.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ())));
+			Assert.False (clicked);
+			Assert.True (btn.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (clicked);
+			clicked = false;
+
+			btn.HotKey = Key.E;
+			Assert.True (btn.ProcessHotKey (new KeyEvent (Key.E | Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (clicked);
+		}
+	}
+}

+ 63 - 0
UnitTests/CheckboxTests.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class CheckboxTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var ckb = new CheckBox ();
+			Assert.False (ckb.Checked);
+			Assert.Equal (string.Empty, ckb.Text);
+			Assert.True (ckb.CanFocus);
+			Assert.Equal (new Rect (0, 0, 4, 1), ckb.Frame);
+
+			ckb = new CheckBox ("Test", true);
+			Assert.True (ckb.Checked);
+			Assert.Equal ("Test", ckb.Text);
+			Assert.True (ckb.CanFocus);
+			Assert.Equal (new Rect (0, 0, 8, 1), ckb.Frame);
+
+			ckb = new CheckBox (1, 2, "Test");
+			Assert.False (ckb.Checked);
+			Assert.Equal ("Test", ckb.Text);
+			Assert.True (ckb.CanFocus);
+			Assert.Equal (new Rect (1, 2, 8, 1), ckb.Frame);
+
+			ckb = new CheckBox (3, 4, "Test", true);
+			Assert.True (ckb.Checked);
+			Assert.Equal ("Test", ckb.Text);
+			Assert.True (ckb.CanFocus);
+			Assert.Equal (new Rect (3, 4, 8, 1), ckb.Frame);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var isChecked = false;
+			CheckBox ckb = new CheckBox ("Test");
+			ckb.Toggled += (e) => isChecked = true;
+			Application.Top.Add (ckb);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (Key.Null, ckb.HotKey);
+			Assert.False (ckb.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ())));
+			Assert.False (isChecked);
+			ckb.Text = "_Test";
+			Assert.Equal (Key.T, ckb.HotKey);
+			Assert.True (ckb.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true })));
+			Assert.True (isChecked);
+			isChecked = false;
+			Assert.True (ckb.ProcessKey (new KeyEvent ((Key)' ', new KeyModifiers ())));
+			Assert.True (isChecked);
+			isChecked = false;
+			Assert.True (ckb.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
+			Assert.True (isChecked);
+		}
+	}
+}

+ 183 - 17
UnitTests/ComboBoxTests.cs

@@ -1,24 +1,190 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.Views {
-    public class ComboBoxTests {
-        [Fact]
-        [AutoInitShutdown]
-        public void EnsureKeyEventsDoNotCauseExceptions ()
-        {
-            var comboBox = new ComboBox ("0");
-
-            var source = Enumerable.Range (0, 15).Select (x => x.ToString ()).ToArray ();
-            comboBox.SetSource(source);
-
-            Application.Top.Add(comboBox);
-
-            foreach (var key in (Key [])Enum.GetValues (typeof(Key))) {
-                comboBox.ProcessKey (new KeyEvent (key, new KeyModifiers ()));
-            }
-        }
-    }
+	public class ComboBoxTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var cb = new ComboBox ();
+			Assert.Equal (string.Empty, cb.Text);
+			Assert.Null (cb.Source);
+
+			cb = new ComboBox ("Test");
+			Assert.Equal ("Test", cb.Text);
+			Assert.Null (cb.Source);
+
+			cb = new ComboBox (new Rect (1, 2, 10, 20), new List<string> () { "One", "Two", "Three" });
+			Assert.Equal (string.Empty, cb.Text);
+			Assert.NotNull (cb.Source);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void EnsureKeyEventsDoNotCauseExceptions ()
+		{
+			var comboBox = new ComboBox ("0");
+
+			var source = Enumerable.Range (0, 15).Select (x => x.ToString ()).ToArray ();
+			comboBox.SetSource (source);
+
+			Application.Top.Add (comboBox);
+
+			foreach (var key in (Key [])Enum.GetValues (typeof (Key))) {
+				Assert.Null (Record.Exception (() => comboBox.ProcessKey (new KeyEvent (key, new KeyModifiers ()))));
+			}
+		}
+
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			List<string> source = new List<string> () { "One", "Two", "Three" };
+			ComboBox cb = new ComboBox ();
+			cb.SetSource (source);
+			Application.Top.Add (cb);
+			Application.Top.FocusFirst ();
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal(string.Empty,cb.Text);
+			var opened = false;
+			cb.OpenSelectedItem += (_) => opened = true;
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.False (opened);
+			cb.Text = "Tw";
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.True (opened);
+			Assert.Equal ("Two", cb.Text);
+			cb.SetSource (null);
+			Assert.False (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // with no source also expand empty
+			Assert.True (cb.IsShow);
+			Assert.Equal (-1, cb.SelectedItem);
+			cb.SetSource(source);
+			cb.Text = "";
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // collapse
+			Assert.False (cb.IsShow);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // expand
+			Assert.True (cb.IsShow);
+			cb.Collapse ();
+			Assert.False (cb.IsShow);
+			Assert.True (cb.HasFocus);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); // losing focus
+			Assert.False (cb.IsShow);
+			Assert.False (cb.HasFocus);
+			Application.Top.FocusFirst (); // Gets focus again
+			Assert.False (cb.IsShow);
+			Assert.True (cb.HasFocus);
+			cb.Expand ();
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (2, cb.SelectedItem);
+			Assert.Equal ("Three", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ())));
+			Assert.False (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); // losing focus
+			Assert.False (cb.HasFocus);
+			Assert.False (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Application.Top.FocusFirst (); // Gets focus again
+			Assert.True (cb.HasFocus);
+			Assert.False (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.U | Key.CtrlMask, new KeyModifiers ())));
+			Assert.True (cb.HasFocus);
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+			Assert.Equal (3, cb.Source.Count);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Minus_One ()
+		{
+			var cb = new ComboBox ();
+			Application.Top.Add (cb);
+			Application.Top.FocusFirst ();
+			Assert.Null(cb.Source);
+			Assert.Equal (-1, cb.SelectedItem);
+			var source = new List<string> ();
+			cb.SetSource(source);
+			Assert.NotNull (cb.Source);
+			Assert.Equal (0, cb.Source.Count);
+			Assert.Equal (-1, cb.SelectedItem);
+			source.Add ("One");
+			Assert.Equal (1, cb.Source.Count);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ())));
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			source.Add ("Two");
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("One", cb.Text);
+			cb.Text = "T";
+			Assert.True (cb.IsShow);
+			Assert.Equal (0, cb.SelectedItem);
+			Assert.Equal ("T", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.False (cb.IsShow);
+			Assert.Equal (2, cb.Source.Count);
+			Assert.Equal (1, cb.SelectedItem);
+			Assert.Equal ("Two", cb.Text);
+			Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ())));
+			Assert.False (cb.IsShow);
+			Assert.Equal (1, cb.SelectedItem); // retains last accept selected item
+			Assert.Equal ("", cb.Text); // clear text
+			cb.SetSource(new List<string> ());
+			Assert.Equal (0, cb.Source.Count);
+			Assert.Equal (-1, cb.SelectedItem);
+			Assert.Equal ("", cb.Text);
+		}
+	}
 }

+ 98 - 0
UnitTests/DateFieldTests.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class DateFieldTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var df = new DateField ();
+			Assert.False (df.IsShortFormat);
+			Assert.Equal (DateTime.MinValue, df.Date);
+			Assert.Equal (1, df.CursorPosition);
+			Assert.Equal (new Rect (0, 0, 12, 1), df.Frame);
+
+			var date = DateTime.Now;
+			df = new DateField (date);
+			Assert.False (df.IsShortFormat);
+			Assert.Equal (date, df.Date);
+			Assert.Equal (1, df.CursorPosition);
+			Assert.Equal (new Rect (0, 0, 12, 1), df.Frame);
+
+			df = new DateField (1, 2, date);
+			Assert.False (df.IsShortFormat);
+			Assert.Equal (date, df.Date);
+			Assert.Equal (1, df.CursorPosition);
+			Assert.Equal (new Rect (1, 2, 12, 1), df.Frame);
+
+			df = new DateField (3, 4, date, true);
+			Assert.True (df.IsShortFormat);
+			Assert.Equal (date, df.Date);
+			Assert.Equal (1, df.CursorPosition);
+			Assert.Equal (new Rect (3, 4, 10, 1), df.Frame);
+
+			df.IsShortFormat = false;
+			Assert.Equal (new Rect (3, 4, 12, 1), df.Frame);
+			Assert.Equal (12, df.Width);
+		}
+
+		[Fact]
+		public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
+		{
+			var df = new DateField ();
+			Assert.Equal (1, df.CursorPosition);
+			df.CursorPosition = 0;
+			Assert.Equal (1, df.CursorPosition);
+			df.CursorPosition = 11;
+			Assert.Equal (10, df.CursorPosition);
+			df.IsShortFormat = true;
+			df.CursorPosition = 0;
+			Assert.Equal (1, df.CursorPosition);
+			df.CursorPosition = 9;
+			Assert.Equal (8, df.CursorPosition);
+		}
+
+		[Fact]
+		public void KeyBindings_Command ()
+		{
+			DateField df = new DateField (DateTime.Parse ("12/12/1971"));
+			df.ReadOnly = true;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			Assert.Equal (" 12/12/1971", df.Text);
+			df.ReadOnly = false;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (" 02/12/1971", df.Text);
+			df.CursorPosition = 4;
+			df.ReadOnly = true;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ())));
+			Assert.Equal (" 02/12/1971", df.Text);
+			df.ReadOnly = false;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Assert.Equal (" 02/02/1971", df.Text);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (1, df.CursorPosition);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (10, df.CursorPosition);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (1, df.CursorPosition);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (10, df.CursorPosition);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (9, df.CursorPosition);
+			Assert.True (df.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (10, df.CursorPosition);
+			Assert.False (df.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ())));
+			df.ReadOnly = true;
+			df.CursorPosition = 1;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ())));
+			Assert.Equal (" 02/02/1971", df.Text);
+			df.ReadOnly = false;
+			Assert.True (df.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ())));
+			Assert.Equal (" 12/02/1971", df.Text);
+		}
+	}
+}

+ 36 - 0
UnitTests/FrameViewTests.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class FrameViewTests {
+		[Fact]
+		public void Constuctors_Defaults ()
+		{
+			var fv = new FrameView ();
+			Assert.Equal (string.Empty, fv.Title);
+			Assert.Equal (string.Empty, fv.Text);
+			Assert.NotNull (fv.Border);
+			Assert.Single (fv.InternalSubviews);
+			Assert.Single (fv.Subviews);
+
+			fv = new FrameView ("Test");
+			Assert.Equal ("Test", fv.Title);
+			Assert.Equal (string.Empty, fv.Text);
+			Assert.NotNull (fv.Border);
+			Assert.Single (fv.InternalSubviews);
+			Assert.Single (fv.Subviews);
+
+			fv = new FrameView (new Rect (1, 2, 10, 20), "Test");
+			Assert.Equal ("Test", fv.Title);
+			Assert.Equal (string.Empty, fv.Text);
+			Assert.NotNull (fv.Border);
+			Assert.Single (fv.InternalSubviews);
+			Assert.Single (fv.Subviews);
+			Assert.Equal (new Rect (1, 2, 10, 20), fv.Frame);
+		}
+	}
+}

+ 58 - 0
UnitTests/HexViewTests.cs

@@ -395,5 +395,63 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("Test", Encoding.Default.GetString (readBuffer));
 			Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer));
 		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var hv = new HexView (LoadStream ()) { Width = 20, Height = 10 };
+			Application.Top.Add (hv);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (63, hv.Source.Length);
+			Assert.Equal (1, hv.Position);
+			Assert.Equal (4, hv.BytesPerLine);
+
+			// right side only needed to press one time
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (2, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (5, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (41, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent ('v' + Key.AltMask, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (41, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (64, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (4, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (37, hv.Position);
+
+			Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorUp | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (1, hv.Position);
+		}
 	}
 }

+ 34 - 1
UnitTests/KeyTests.cs

@@ -135,5 +135,38 @@ namespace Terminal.Gui.Core {
 				break;
 			}
 		}
+
+		[Fact]
+		public void Key_ToString ()
+		{
+			var k = Key.Y | Key.CtrlMask;
+			Assert.Equal ("Y, CtrlMask", k.ToString ());
+
+			k = Key.CtrlMask | Key.Y;
+			Assert.Equal ("Y, CtrlMask", k.ToString ());
+
+			k = Key.Space;
+			Assert.Equal ("Space", k.ToString ());
+
+			k = Key.Space | Key.D;
+			Assert.Equal ("d", k.ToString ());
+
+			k = (Key)'d';
+			Assert.Equal ("d", k.ToString ());
+
+			k = Key.d;
+			Assert.Equal ("d", k.ToString ());
+
+			k = Key.D;
+			Assert.Equal ("D", k.ToString ());
+
+			// In a console this will always returns Key.D
+			k = Key.D | Key.ShiftMask;
+			Assert.Equal ("D, ShiftMask", k.ToString ());
+
+			// In a console this will always returns Key.D
+			k = Key.d | Key.ShiftMask;
+			Assert.Equal ("d, ShiftMask", k.ToString ());
+		}
 	}
-}
+}

+ 88 - 0
UnitTests/ListViewTests.cs

@@ -0,0 +1,88 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class ListViewTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var lv = new ListView ();
+			Assert.Null (lv.Source);
+			Assert.True (lv.CanFocus);
+
+			lv = new ListView (new List<string> () { "One", "Two", "Three" });
+			Assert.NotNull (lv.Source);
+
+			lv = new ListView (new NewListDataSource());
+			Assert.NotNull (lv.Source);
+
+			lv = new ListView (new Rect (0, 1, 10, 20), new List<string> () { "One", "Two", "Three" });
+			Assert.NotNull (lv.Source);
+			Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame);
+
+			lv = new ListView (new Rect (0, 1, 10, 20), new NewListDataSource ());
+			Assert.NotNull (lv.Source);
+			Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame);
+		}
+
+		private class NewListDataSource : IListDataSource {
+			public int Count => throw new NotImplementedException ();
+
+			public int Length => throw new NotImplementedException ();
+
+			public bool IsMarked (int item)
+			{
+				throw new NotImplementedException ();
+			}
+
+			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
+			{
+				throw new NotImplementedException ();
+			}
+
+			public void SetMark (int item, bool value)
+			{
+				throw new NotImplementedException ();
+			}
+
+			public IList ToList ()
+			{
+				throw new NotImplementedException ();
+			}
+		}
+
+		[Fact]
+		public void KeyBindings_Command ()
+		{
+			List<string> source = new List<string> () { "One", "Two", "Three" };
+			ListView lv = new ListView (source) { Height = 2, AllowsMarking = true };
+			Assert.Equal (0, lv.SelectedItem);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (1, lv.SelectedItem);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (0, lv.SelectedItem);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (2, lv.SelectedItem);
+			Assert.Equal (2, lv.TopItem);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.Equal (0, lv.SelectedItem);
+			Assert.Equal (0, lv.TopItem);
+			Assert.False (lv.Source.IsMarked (lv.SelectedItem));
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
+			Assert.True (lv.Source.IsMarked (lv.SelectedItem));
+			var opened = false;
+			lv.OpenSelectedItem += (_) => opened = true;
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.True (opened);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (2, lv.SelectedItem);
+			Assert.True (lv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (0, lv.SelectedItem);
+		}
+	}
+}

+ 3 - 2
UnitTests/MenuTests.cs

@@ -183,7 +183,6 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("_Paste", miCurrent.Title);
 
 			for (int i = 2; i >= -1; i--) {
-				View view;
 				if (i == -1) {
 					Assert.False (mCurrent.MouseEvent (new MouseEvent () {
 						X = 10,
@@ -301,7 +300,9 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("_File", GetCurrentMenuBarItemTitle ());
 			Assert.Equal ("_New", GetCurrentMenuTitle ());
 
-			Assert.True (mCurrent.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.False (mCurrent.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.True (menu.IsMenuOpen);
+			Assert.True (Application.Top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
 			Assert.False (menu.IsMenuOpen);
 			Assert.Equal ("Closed", GetCurrentMenuBarItemTitle ());
 			Assert.Equal ("None", GetCurrentMenuTitle ());

+ 128 - 0
UnitTests/PosTests.cs

@@ -47,6 +47,134 @@ namespace Terminal.Gui.Core {
 			Assert.NotEqual (pos1, pos2);
 		}
 
+		[Fact]
+		[AutoInitShutdown]
+		public void AnchorEnd_Equal_Inside_Window ()
+		{
+			var viewWidth = 10;
+			var viewHeight = 1;
+			var tv = new TextView () {
+				X = Pos.AnchorEnd (viewWidth),
+				Y = Pos.AnchorEnd (viewHeight),
+				Width = viewWidth,
+				Height = viewHeight
+			};
+
+			var win = new Window ();
+
+			win.Add (tv);
+
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+
+			Assert.Equal (new Rect (0, 0, 80, 25), top.Frame);
+			Assert.Equal (new Rect (0, 0, 80, 25), win.Frame);
+			Assert.Equal (new Rect (1, 1, 78, 23), win.Subviews[0].Frame);
+			Assert.Equal ("ContentView()({X=1,Y=1,Width=78,Height=23})", win.Subviews [0].ToString());
+			Assert.Equal (new Rect (1, 1, 79, 24), new Rect (
+				win.Subviews[0].Frame.Left, win.Subviews [0].Frame.Top,
+				win.Subviews [0].Frame.Right, win.Subviews[0].Frame.Bottom));
+			Assert.Equal (new Rect (68, 22, 10, 1), tv.Frame);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void AnchorEnd_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel ()
+		{
+			var viewWidth = 10;
+			var viewHeight = 1;
+			var tv = new TextView () {
+				X = Pos.AnchorEnd (viewWidth),
+				Y = Pos.AnchorEnd (viewHeight),
+				Width = viewWidth,
+				Height = viewHeight
+			};
+
+			var win = new Window ();
+
+			win.Add (tv);
+
+			var menu = new MenuBar ();
+			var status = new StatusBar ();
+			var top = Application.Top;
+			top.Add (win, menu, status);
+			Application.Begin (top);
+
+			Assert.Equal (new Rect (0, 0, 80, 25), top.Frame);
+			Assert.Equal (new Rect (0, 0, 80, 1), menu.Frame);
+			Assert.Equal (new Rect (0, 24, 80, 1), status.Frame);
+			Assert.Equal (new Rect (0, 1, 80, 23), win.Frame);
+			Assert.Equal (new Rect (1, 1, 78, 21), win.Subviews [0].Frame);
+			Assert.Equal (new Rect (1, 1, 79, 22), new Rect (
+				win.Subviews [0].Frame.Left, win.Subviews [0].Frame.Top,
+				win.Subviews [0].Frame.Right, win.Subviews [0].Frame.Bottom));
+			Assert.Equal (new Rect (68, 20, 10, 1), tv.Frame);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Bottom_Equal_Inside_Window ()
+		{
+			var win = new Window ();
+
+			var label = new Label ("This should be the last line.") {
+				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				ColorScheme = Colors.Menu,
+				Width = Dim.Fill (),
+				X = Pos.Center (),
+				Y = Pos.Bottom (win) - 4  // two lines top border more two lines above border
+			};
+
+			win.Add (label);
+
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+
+			Assert.Equal (new Rect (0, 0, 80, 25), top.Frame);
+			Assert.Equal (new Rect (0, 0, 80, 25), win.Frame);
+			Assert.Equal (new Rect (1, 1, 78, 23), win.Subviews [0].Frame);
+			Assert.Equal ("ContentView()({X=1,Y=1,Width=78,Height=23})", win.Subviews [0].ToString ());
+			Assert.Equal (new Rect (0, 0, 80, 25), new Rect (
+				win.Frame.Left, win.Frame.Top,
+				win.Frame.Right, win.Frame.Bottom));
+			Assert.Equal (new Rect (0, 21, 78, 1), label.Frame);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Bottom_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel ()
+		{
+			var win = new Window ();
+
+			var label = new Label ("This should be the last line.") {
+				TextAlignment = Terminal.Gui.TextAlignment.Centered,
+				ColorScheme = Colors.Menu,
+				Width = Dim.Fill (),
+				X = Pos.Center (),
+				Y = Pos.Bottom (win) - 4  // two lines top border more two lines above border
+			};
+
+			win.Add (label);
+
+			var menu = new MenuBar ();
+			var status = new StatusBar ();
+			var top = Application.Top;
+			top.Add (win, menu, status);
+			Application.Begin (top);
+
+			Assert.Equal (new Rect (0, 0, 80, 25), top.Frame);
+			Assert.Equal (new Rect (0, 0, 80, 1), menu.Frame);
+			Assert.Equal (new Rect (0, 24, 80, 1), status.Frame);
+			Assert.Equal (new Rect (0, 1, 80, 23), win.Frame);
+			Assert.Equal (new Rect (1, 1, 78, 21), win.Subviews [0].Frame);
+			Assert.Equal (new Rect (0, 1, 80, 24), new Rect (
+				win.Frame.Left, win.Frame.Top,
+				win.Frame.Right, win.Frame.Bottom));
+			Assert.Equal (new Rect (0, 20, 78, 1), label.Frame);
+		}
+
 		[Fact]
 		public void AnchorEnd_Negative_Throws ()
 		{

+ 116 - 0
UnitTests/RadioGroupTests.cs

@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class RadioGroupTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var rg = new RadioGroup ();
+			Assert.True (rg.CanFocus);
+			Assert.Empty (rg.RadioLabels);
+			Assert.Equal (0, rg.X);
+			Assert.Equal (0, rg.Y);
+			Assert.Equal (0, rg.Width);
+			Assert.Equal (0, rg.Height);
+			Assert.Equal (0, rg.SelectedItem);
+
+			rg = new RadioGroup (new NStack.ustring [] { "Test" });
+			Assert.True (rg.CanFocus);
+			Assert.Single (rg.RadioLabels);
+			Assert.Equal (0, rg.X);
+			Assert.Equal (0, rg.Y);
+			Assert.Equal (7, rg.Width);
+			Assert.Equal (1, rg.Height);
+			Assert.Equal (0, rg.SelectedItem);
+
+			rg = new RadioGroup (new Rect (1, 2, 20, 5), new NStack.ustring [] { "Test" });
+			Assert.True (rg.CanFocus);
+			Assert.Single (rg.RadioLabels);
+			Assert.Equal (1, rg.X);
+			Assert.Equal (2, rg.Y);
+			Assert.Equal (20, rg.Width);
+			Assert.Equal (5, rg.Height);
+			Assert.Equal (0, rg.SelectedItem);
+
+			rg = new RadioGroup (1, 2, new NStack.ustring [] { "Test" });
+			Assert.True (rg.CanFocus);
+			Assert.Single (rg.RadioLabels);
+			Assert.Equal (1, rg.X);
+			Assert.Equal (2, rg.Y);
+			Assert.Equal (7, rg.Width);
+			Assert.Equal (1, rg.Height);
+			Assert.Equal (0, rg.SelectedItem);
+		}
+
+		[Fact]
+		public void Initialize_SelectedItem_With_Minus_One ()
+		{
+			var rg = new RadioGroup (new NStack.ustring [] { "Test" }, -1);
+			Assert.Equal (-1, rg.SelectedItem);
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
+			Assert.Equal (0, rg.SelectedItem);
+		}
+
+		[Fact]
+		public void DisplayMode_Width_Height_HorizontalSpace ()
+		{
+			var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" });
+			Assert.Equal (DisplayModeLayout.Vertical, rg.DisplayMode);
+			Assert.Equal (2, rg.RadioLabels.Length);
+			Assert.Equal (0, rg.X);
+			Assert.Equal (0, rg.Y);
+			Assert.Equal (11, rg.Width);
+			Assert.Equal (2, rg.Height);
+
+			rg.DisplayMode = DisplayModeLayout.Horizontal;
+			Assert.Equal (DisplayModeLayout.Horizontal, rg.DisplayMode);
+			Assert.Equal (2, rg.HorizontalSpace);
+			Assert.Equal (0, rg.X);
+			Assert.Equal (0, rg.Y);
+			Assert.Equal (16, rg.Width);
+			Assert.Equal (1, rg.Height);
+
+			rg.HorizontalSpace = 4;
+			Assert.Equal (DisplayModeLayout.Horizontal, rg.DisplayMode);
+			Assert.Equal (4, rg.HorizontalSpace);
+			Assert.Equal (0, rg.X);
+			Assert.Equal (0, rg.Y);
+			Assert.Equal (20, rg.Width);
+			Assert.Equal (1, rg.Height);
+		}
+
+		[Fact]
+		public void SelectedItemChanged_Event ()
+		{
+			var previousSelectedItem = -1;
+			var selectedItem = -1;
+			var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" });
+			rg.SelectedItemChanged += (e) => {
+				previousSelectedItem = e.PreviousSelectedItem;
+				selectedItem = e.SelectedItem;
+			};
+
+			rg.SelectedItem = 1;
+			Assert.Equal (0, previousSelectedItem);
+			Assert.Equal (selectedItem, rg.SelectedItem);
+		}
+
+		[Fact]
+		public void KeyBindings_Command ()
+		{
+			var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" });
+
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ())));
+			Assert.Equal (1, rg.SelectedItem);
+		}
+	}
+}

+ 9 - 1
UnitTests/ScenarioTests.cs

@@ -6,17 +6,21 @@ using System.Reflection;
 using Terminal.Gui;
 using UICatalog;
 using Xunit;
+using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui {
 	public class ScenarioTests {
-		public ScenarioTests ()
+		readonly ITestOutputHelper output;
+
+		public ScenarioTests (ITestOutputHelper output)
 		{
 #if DEBUG_IDISPOSABLE
 			Responder.Instances.Clear ();
 #endif
+			this.output = output;
 		}
 
 		int CreateInput (string input)
@@ -83,6 +87,10 @@ namespace Terminal.Gui {
 
 				// Shutdown must be called to safely clean up Application if Init has been called
 				Application.Shutdown ();
+				
+				if(abortCount != 0) {
+					output.WriteLine ($"Scenario {scenarioClass} had abort count of {abortCount}");
+				}
 
 				Assert.Equal (0, abortCount);
 				// # of key up events should match # of iterations

+ 175 - 0
UnitTests/ScrollViewTests.cs

@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class ScrollViewTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var sv = new ScrollView ();
+			Assert.True (sv.CanFocus);
+			Assert.Equal (new Rect (0, 0, 0, 0), sv.Frame);
+			Assert.Equal (Rect.Empty, sv.Frame);
+			Assert.Equal (0, sv.X);
+			Assert.Equal (0, sv.Y);
+			Assert.Equal (0, sv.Width);
+			Assert.Equal (0, sv.Height);
+			Assert.Equal (Point.Empty, sv.ContentOffset);
+			Assert.Equal (Size.Empty, sv.ContentSize);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.True (sv.KeepContentAlwaysInViewport);
+
+			sv = new ScrollView (new Rect (1, 2, 20, 10));
+			Assert.True (sv.CanFocus);
+			Assert.Equal (new Rect (1, 2, 20, 10), sv.Frame);
+			Assert.Equal (1, sv.X);
+			Assert.Equal (2, sv.Y);
+			Assert.Equal (20, sv.Width);
+			Assert.Equal (10, sv.Height);
+			Assert.Equal (Point.Empty, sv.ContentOffset);
+			Assert.Equal (Size.Empty, sv.ContentSize);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.True (sv.KeepContentAlwaysInViewport);
+		}
+
+		[Fact]
+		public void Adding_Views ()
+		{
+			var sv = new ScrollView (new Rect (0, 0, 20, 10)) {
+				ContentSize = new Size (30, 20)
+			};
+			sv.Add (new View () { Width = 10, Height = 5 },
+				new View () { X = 12, Y = 7, Width = 10, Height = 5 });
+
+			Assert.Equal (new Size (30, 20), sv.ContentSize);
+			Assert.Equal (2, sv.Subviews [0].Subviews.Count);
+		}
+
+		[Fact]
+		public void KeyBindings_Command ()
+		{
+			var sv = new ScrollView (new Rect (0, 0, 20, 10)) {
+				ContentSize = new Size (40, 20)
+			};
+			sv.Add (new View () { Width = 20, Height = 5 },
+				new View () { X = 22, Y = 7, Width = 10, Height = 5 });
+
+			Assert.True (sv.KeepContentAlwaysInViewport);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -1), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent ((Key)'v' | Key.AltMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (-1, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+
+			sv.KeepContentAlwaysInViewport = false;
+			Assert.False (sv.KeepContentAlwaysInViewport);
+			Assert.True (sv.AutoHideScrollBars);
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -1), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -10), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent ((Key)'v' | Key.AltMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -9), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (-1, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-20, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-19, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (-19, 0), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (-19, 0), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (new Point (-19, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (new Point (-19, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, -19), sv.ContentOffset);
+			Assert.True (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
+			Assert.False (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (-39, -19), sv.ContentOffset);
+		}
+	}
+}

+ 6 - 9
UnitTests/TableViewTests.cs

@@ -231,15 +231,10 @@ namespace Terminal.Gui.Views {
 			Assert.False (tableView.IsSelected (2, 2));
 		}
 
+		[AutoInitShutdown]
 		[Fact]
 		public void PageDown_ExcludesHeaders ()
 		{
-
-			var driver = new FakeDriver ();
-			Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
-			driver.Init (() => { });
-
-
 			var tableView = new TableView () {
 				Table = BuildTable (25, 50),
 				MultiSelect = true,
@@ -251,6 +246,11 @@ namespace Terminal.Gui.Views {
 			tableView.Style.ShowHorizontalHeaderUnderline = true;
 			tableView.Style.AlwaysShowHeaders = false;
 
+			// ensure that TableView has the input focus
+			Application.Top.Add (tableView);
+			Application.Top.FocusFirst ();
+			Assert.True (tableView.HasFocus);
+
 			Assert.Equal (0, tableView.RowOffset);
 
 			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
@@ -262,9 +262,6 @@ namespace Terminal.Gui.Views {
 			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
 
 			Assert.Equal (8, tableView.RowOffset);
-
-			// Shutdown must be called to safely clean up Application if Init has been called
-			Application.Shutdown ();
 		}
 
 		[Fact]

+ 4 - 0
UnitTests/TextFieldTests.cs

@@ -719,18 +719,22 @@ namespace Terminal.Gui.Views {
 			var tf = new TextField ("ABC");
 			tf.EnsureFocus ();
 			Assert.Equal ("ABC", tf.Text);
+			Assert.Equal (3, tf.CursorPosition);
 
 			// now delete the C
 			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
 			Assert.Equal ("AB", tf.Text);
+			Assert.Equal (2, tf.CursorPosition);
 
 			// then delete the B
 			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
 			Assert.Equal ("A", tf.Text);
+			Assert.Equal (1, tf.CursorPosition);
 
 			// then delete the A
 			tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()));
 			Assert.Equal ("", tf.Text);
+			Assert.Equal (0, tf.CursorPosition);
 		}
 
 		[Fact]

+ 463 - 11
UnitTests/TextViewTests.cs

@@ -1,5 +1,8 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
+using System.Text.RegularExpressions;
 using Xunit;
 using Xunit.Abstractions;
 
@@ -937,19 +940,21 @@ namespace Terminal.Gui.Views {
 			bool iterationsFinished = false;
 
 			while (!iterationsFinished) {
-				_textView.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()));
 				switch (iteration) {
 				case 0:
+					_textView.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ()));
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ($"{System.Environment.NewLine}This is the second line.", _textView.Text);
 					break;
 				case 1:
+					_textView.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("This is the second line.", _textView.Text);
 					break;
 				case 2:
+					_textView.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ()));
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("", _textView.Text);
@@ -974,19 +979,21 @@ namespace Terminal.Gui.Views {
 			bool iterationsFinished = false;
 
 			while (!iterationsFinished) {
-				_textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ()));
 				switch (iteration) {
 				case 0:
+					_textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ()));
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (1, _textView.CursorPosition.Y);
 					Assert.Equal ($"This is the first line.{System.Environment.NewLine}", _textView.Text);
 					break;
 				case 1:
+					_textView.ProcessKey (new KeyEvent (Key.Backspace | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (23, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("This is the first line.", _textView.Text);
 					break;
 				case 2:
+					_textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ()));
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("", _textView.Text);
@@ -1439,7 +1446,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (_textView.Multiline);
 			_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
 			Assert.Equal ("\tTAB to jump between text fields.", _textView.Text);
-			_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+			_textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 		}
 
@@ -1483,7 +1490,7 @@ namespace Terminal.Gui.Views {
 
 		[Fact]
 		[InitShutdown]
-		public void Multiline_Setting_Changes_AllowsReturn_And_AllowsTab_And_Height ()
+		public void Multiline_Setting_Changes_AllowsReturn_AllowsTab_Height_WordWrap ()
 		{
 			Assert.True (_textView.Multiline);
 			Assert.True (_textView.AllowsReturn);
@@ -1491,7 +1498,10 @@ namespace Terminal.Gui.Views {
 			Assert.True (_textView.AllowsTab);
 			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
 			Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ());
+			Assert.False (_textView.WordWrap);
 
+			_textView.WordWrap = true;
+			Assert.True (_textView.WordWrap);
 			_textView.Multiline = false;
 			Assert.False (_textView.Multiline);
 			Assert.False (_textView.AllowsReturn);
@@ -1499,7 +1509,10 @@ namespace Terminal.Gui.Views {
 			Assert.False (_textView.AllowsTab);
 			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
 			Assert.Equal ("Dim.Absolute(1)", _textView.Height.ToString ());
+			Assert.False (_textView.WordWrap);
 
+			_textView.WordWrap = true;
+			Assert.False (_textView.WordWrap);
 			_textView.Multiline = true;
 			Assert.True (_textView.Multiline);
 			Assert.True (_textView.AllowsReturn);
@@ -1507,6 +1520,7 @@ namespace Terminal.Gui.Views {
 			Assert.True (_textView.AllowsTab);
 			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
 			Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ());
+			Assert.False (_textView.WordWrap);
 		}
 
 		[Fact]
@@ -1532,7 +1546,7 @@ namespace Terminal.Gui.Views {
 				}
 				while (col > 0) {
 					col--;
-					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					_textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
 					leftCol = GetLeftCol (leftCol);
 					Assert.Equal (leftCol, _textView.LeftColumn);
@@ -1568,7 +1582,7 @@ namespace Terminal.Gui.Views {
 				Assert.Equal (leftCol, _textView.LeftColumn);
 				while (col > 0) {
 					col--;
-					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					_textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
 					leftCol = GetLeftCol (leftCol);
 					Assert.Equal (leftCol, _textView.LeftColumn);
@@ -1654,7 +1668,7 @@ namespace Terminal.Gui.Views {
 				}
 				while (col > 0) {
 					col--;
-					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					_textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
 					leftCol = GetLeftCol (leftCol);
 					Assert.Equal (leftCol, _textView.LeftColumn);
@@ -1710,7 +1724,7 @@ namespace Terminal.Gui.Views {
 				leftCol = GetLeftCol (leftCol);
 				while (col > 0) {
 					col--;
-					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					_textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()));
 					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
 					leftCol = GetLeftCol (leftCol);
 					Assert.Equal (leftCol, _textView.LeftColumn);
@@ -2042,7 +2056,7 @@ line.
 			var tv = new TextView () { Width = 10, Height = 10, BottomOffset = 1 };
 			tv.Text = text;
 
-			tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End, null));
+			tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End, new KeyModifiers ()));
 
 			Assert.Equal (4, tv.TopRow);
 			Assert.Equal (1, tv.BottomOffset);
@@ -2072,7 +2086,7 @@ line.
 			var tv = new TextView () { Width = 10, Height = 10, RightOffset = 1 };
 			tv.Text = text;
 
-			tv.ProcessKey (new KeyEvent (Key.End, null));
+			tv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()));
 
 			Assert.Equal (4, tv.LeftColumn);
 			Assert.Equal (1, tv.RightOffset);
@@ -2283,5 +2297,443 @@ line.
 			Assert.Equal (new Point (10, 0), tv.CursorPosition);
 			Assert.Equal (1, tv.LeftColumn);
 		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var text = "This is the first line.\nThis is the second line.\nThis is the third line.";
+			var tv = new TextView () {
+				Width = 10,
+				Height = 2,
+				Text = text
+			};
+			var top = Application.Top;
+			top.Add (tv);
+			Application.Begin (top);
+
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text);
+			Assert.Equal (3, tv.Lines);
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			Assert.False (tv.ReadOnly);
+			Assert.True (tv.CanFocus);
+
+			tv.CanFocus = false;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			tv.CanFocus = true;
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (2, tv.CurrentRow);
+			Assert.Equal (23, tv.CurrentColumn);
+			Assert.Equal (tv.CurrentColumn, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 2), tv.CursorPosition);
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.NotNull (tv.Autocomplete);
+			Assert.Empty (tv.Autocomplete.AllSuggestions);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text);
+			Assert.Equal (new Point (24, 2), tv.CursorPosition);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text);
+			Assert.Equal (new Point (23, 2), tv.CursorPosition);
+			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+				.Select (s => s.Value)
+				.Distinct ().ToList ();
+			Assert.Equal (7, tv.Autocomplete.AllSuggestions.Count);
+			Assert.Equal ("This", tv.Autocomplete.AllSuggestions [0]);
+			Assert.Equal ("is", tv.Autocomplete.AllSuggestions [1]);
+			Assert.Equal ("the", tv.Autocomplete.AllSuggestions [2]);
+			Assert.Equal ("first", tv.Autocomplete.AllSuggestions [3]);
+			Assert.Equal ("line", tv.Autocomplete.AllSuggestions [4]);
+			Assert.Equal ("second", tv.Autocomplete.AllSuggestions [5]);
+			Assert.Equal ("third", tv.Autocomplete.AllSuggestions [^1]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ())));
+			tv.Redraw (tv.Bounds);
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text);
+			Assert.Equal (new Point (24, 2), tv.CursorPosition);
+			Assert.Single (tv.Autocomplete.Suggestions);
+			Assert.Equal ("first", tv.Autocomplete.Suggestions [0]);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (28, 2), tv.CursorPosition);
+			Assert.Single (tv.Autocomplete.Suggestions);
+			Assert.Equal ("first", tv.Autocomplete.Suggestions [0]);
+			tv.Autocomplete.AllSuggestions = new List<string> ();
+			tv.Autocomplete.ClearSuggestions ();
+			Assert.Empty (tv.Autocomplete.AllSuggestions);
+			Assert.Empty (tv.Autocomplete.Suggestions);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ())));
+			Assert.Equal (24, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (24, 1), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (((int)'V' + Key.AltMask), new KeyModifiers ())));
+			Assert.Equal (23, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())));
+			Assert.Equal (24, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 1), tv.CursorPosition); // gets the previous length
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (28, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 2), tv.CursorPosition); // gets the previous length
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.PageUp | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (24, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 1), tv.CursorPosition); // gets the previous length
+			Assert.Equal (25, tv.SelectedLength);
+			Assert.Equal (".\nThis is the third line.", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.PageDown | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (28, tv.GetCurrentLine ().Count);
+			Assert.Equal (new Point (23, 2), tv.CursorPosition); // gets the previous length
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (Point.Empty, tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.N | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.P | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.Equal (24, tv.SelectedLength);
+			Assert.Equal ("This is the first line.\n", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (1, 0), tv.CursorPosition);
+			Assert.Equal (1, tv.SelectedLength);
+			Assert.Equal ("T", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			Assert.Equal ($"his is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal ($"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (21, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ())));
+			Assert.Equal ($"is is the first line{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (20, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (19, tv.SelectedLength);
+			Assert.Equal ("is is the first lin", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal ("is is the first lin", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal ("is is the first lin", Clipboard.Contents);
+			tv.CursorPosition = Point.Empty;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal ("is is the first lin", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal ("is is the first lin", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			tv.ReadOnly = true;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			tv.ReadOnly = false;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Space | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.Equal (19, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Space | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal (19, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			tv.SelectionStartColumn = 0;
+			Assert.True (tv.ProcessKey (new KeyEvent (((int)'C' + Key.AltMask), new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (19, tv.SelectedLength);
+			Assert.Equal ("is is the first lin", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (19, 0), tv.CursorPosition);
+			Assert.Equal (19, tv.SelectedLength);
+			Assert.Equal ("is is the first lin", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.True (tv.ProcessKey (new KeyEvent (((int)'W' + Key.AltMask), new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.Equal ("is is the first lin", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.Equal ("", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.X | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.Equal (0, tv.SelectionStartColumn);
+			Assert.Equal (0, tv.SelectionStartRow);
+			Assert.Equal ("", Clipboard.Contents);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (28, 2), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorLeft | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (12, 2), tv.CursorPosition);
+			Assert.Equal (6, tv.SelectedLength);
+			Assert.Equal ("third ", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent ((Key)((int)'B' + Key.AltMask), new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (8, 2), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (12, 2), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorRight | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			Assert.Equal (6, tv.SelectedLength);
+			Assert.Equal ("third ", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent ((Key)((int)'F' + Key.AltMask), new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (28, 2), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting); Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text);
+			Assert.Equal (new Point (28, 1), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.False (tv.Selecting); Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (18, 1), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.AllowsReturn);
+			tv.AllowsReturn = false;
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.False (tv.Selecting);
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.False (tv.AllowsReturn);
+			tv.AllowsReturn = true;
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (0, 1), tv.CursorPosition);
+			Assert.Equal (0, tv.SelectedLength);
+			Assert.Equal ("", tv.SelectedText);
+			Assert.False (tv.Selecting);
+			Assert.True (tv.AllowsReturn);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			Assert.Equal (43, tv.SelectedLength);
+			Assert.Equal ("This is the second line.\nThis is the third ", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Home | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (0, 0), tv.CursorPosition);
+			Assert.Equal (1, tv.SelectedLength);
+			Assert.Equal ("\n", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.T | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			Assert.Equal (44, tv.SelectedLength);
+			Assert.Equal ("\nThis is the second line.\nThis is the third ", tv.SelectedText);
+			Assert.True (tv.Selecting);
+			Assert.True (tv.Used);
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.InsertChar, new KeyModifiers ())));
+			Assert.False (tv.Used);
+			Assert.True (tv.AllowsTab);
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			tv.AllowsTab = false;
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.False (tv.AllowsTab);
+			tv.AllowsTab = true;
+			Assert.Equal (new Point (18, 2), tv.CursorPosition);
+			Assert.True (tv.Selecting);
+			tv.Selecting = false;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t", tv.Text);
+			Assert.True (tv.AllowsTab);
+			tv.AllowsTab = false;
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t", tv.Text);
+			Assert.False (tv.AllowsTab);
+			tv.AllowsTab = true;
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text);
+			Assert.True (tv.AllowsTab);
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ())));
+			Assert.False (tv.ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ())));
+			Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())));
+			Assert.False (tv.ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ())));
+		}
 	}
-}
+}

+ 98 - 0
UnitTests/TimeFieldTests.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Terminal.Gui.Views {
+	public class TimeFieldTests {
+		[Fact]
+		public void Constructors_Defaults ()
+		{
+			var tf = new TimeField ();
+			Assert.False (tf.IsShortFormat);
+			Assert.Equal (TimeSpan.MinValue, tf.Time);
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
+
+			var time = DateTime.Now.TimeOfDay;
+			tf = new TimeField (time);
+			Assert.False (tf.IsShortFormat);
+			Assert.Equal (time, tf.Time);
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
+
+			tf = new TimeField (1, 2, time);
+			Assert.False (tf.IsShortFormat);
+			Assert.Equal (time, tf.Time);
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame);
+
+			tf = new TimeField (3, 4, time, true);
+			Assert.True (tf.IsShortFormat);
+			Assert.Equal (time, tf.Time);
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame);
+
+			tf.IsShortFormat = false;
+			Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame);
+			Assert.Equal (10, tf.Width);
+		}
+
+		[Fact]
+		public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
+		{
+			var tf = new TimeField ();
+			Assert.Equal (1, tf.CursorPosition);
+			tf.CursorPosition = 0;
+			Assert.Equal (1, tf.CursorPosition);
+			tf.CursorPosition = 9;
+			Assert.Equal (8, tf.CursorPosition);
+			tf.IsShortFormat = true;
+			tf.CursorPosition = 0;
+			Assert.Equal (1, tf.CursorPosition);
+			tf.CursorPosition = 6;
+			Assert.Equal (5, tf.CursorPosition);
+		}
+
+		[Fact]
+		public void KeyBindings_Command ()
+		{
+			TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19"));
+			tf.ReadOnly = true;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ())));
+			Assert.Equal (" 12:12:19", tf.Text);
+			tf.ReadOnly = false;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (" 02:12:19", tf.Text);
+			tf.CursorPosition = 4;
+			tf.ReadOnly = true;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ())));
+			Assert.Equal (" 02:12:19", tf.Text);
+			tf.ReadOnly = false;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Assert.Equal (" 02:02:19", tf.Text);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ())));
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())));
+			Assert.Equal (8, tf.CursorPosition);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (1, tf.CursorPosition);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (8, tf.CursorPosition);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (7, tf.CursorPosition);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (8, tf.CursorPosition);
+			Assert.False (tf.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ())));
+			tf.ReadOnly = true;
+			tf.CursorPosition = 1;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ())));
+			Assert.Equal (" 02:02:19", tf.Text);
+			tf.ReadOnly = false;
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ())));
+			Assert.Equal (" 12:02:19", tf.Text);
+		}
+	}
+}

+ 332 - 1
UnitTests/ToplevelTests.cs

@@ -316,5 +316,336 @@ namespace Terminal.Gui.Core {
 			win.MouseEvent (new MouseEvent () { X = 6, Y = 0, Flags = MouseFlags.Button1Pressed });
 			Assert.Null (Toplevel.dragPosition);
 		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command ()
+		{
+			var isRunning = false;
+
+			var win1 = new Window ("Win1") { Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W1 = new Label ("Enter text in TextField on Win1:");
+			var tf1W1 = new TextField ("Text1 on Win1") { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () };
+			var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Y = Pos.Bottom (lblTf1W1) + 1 };
+			var tvW1 = new TextView () { X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" };
+			var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Y = Pos.Bottom (lblTvW1) + 1 };
+			var tf2W1 = new TextField ("Text2 on Win1") { X = Pos.Left (tf1W1), Width = Dim.Fill () };
+			win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1);
+
+			var win2 = new Window ("Win2") { X = Pos.Right (win1) + 1, Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W2 = new Label ("Enter text in TextField on Win2:");
+			var tf1W2 = new TextField ("Text1 on Win2") { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () };
+			var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Y = Pos.Bottom (lblTf1W2) + 1 };
+			var tvW2 = new TextView () { X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" };
+			var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Y = Pos.Bottom (lblTvW2) + 1 };
+			var tf2W2 = new TextField ("Text2 on Win2") { X = Pos.Left (tf1W2), Width = Dim.Fill () };
+			win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2);
+
+			var top = Application.Top;
+			top.Add (win1, win2);
+			top.Loaded += () => isRunning = true;
+			top.Closing += (_) => isRunning = false;
+			Application.Begin (top);
+			top.Running = true;
+
+			Assert.Equal (new Rect (0, 0, 40, 25), win1.Frame);
+			Assert.Equal (new Rect (41, 0, 40, 25), win2.Frame);
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf1W1, top.MostFocused);
+
+			Assert.True (isRunning);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.QuitKey, new KeyModifiers ())));
+			Assert.False (isRunning);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			Assert.False (top.Focused.ProcessKey (new KeyEvent (Key.F5, new KeyModifiers ())));
+
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf1W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf1W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.I | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf1W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win2, top.Focused);
+			Assert.Equal (tf1W2, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ())));
+			Assert.Equal (win2, top.Focused);
+			Assert.Equal (tf1W2, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf1W1, top.MostFocused);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.Equal (new Point (0, 0), tvW1.CursorPosition);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tvW1, top.MostFocused);
+			Assert.Equal (new Point (16, 1), tvW1.CursorPosition);
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, top.Focused);
+			Assert.Equal (tf2W1, top.MostFocused);
+
+			Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.L | Key.CtrlMask, new KeyModifiers ())));
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void KeyBindings_Command_With_MdiTop ()
+		{
+			var top = Application.Top;
+			Assert.Null (Application.MdiTop);
+			top.IsMdiContainer = true;
+			Assert.Equal (Application.Top, Application.MdiTop);
+
+			var isRunning = true;
+
+			var win1 = new Window ("Win1") { Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W1 = new Label ("Enter text in TextField on Win1:");
+			var tf1W1 = new TextField ("Text1 on Win1") { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () };
+			var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Y = Pos.Bottom (lblTf1W1) + 1 };
+			var tvW1 = new TextView () { X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" };
+			var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Y = Pos.Bottom (lblTvW1) + 1 };
+			var tf2W1 = new TextField ("Text2 on Win1") { X = Pos.Left (tf1W1), Width = Dim.Fill () };
+			win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1);
+
+			var win2 = new Window ("Win2") { Width = Dim.Percent (50f), Height = Dim.Fill () };
+			var lblTf1W2 = new Label ("Enter text in TextField on Win2:");
+			var tf1W2 = new TextField ("Text1 on Win2") { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () };
+			var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Y = Pos.Bottom (lblTf1W2) + 1 };
+			var tvW2 = new TextView () { X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" };
+			var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Y = Pos.Bottom (lblTvW2) + 1 };
+			var tf2W2 = new TextField ("Text2 on Win2") { X = Pos.Left (tf1W2), Width = Dim.Fill () };
+			win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2);
+
+			win1.Closing += (_) => isRunning = false;
+			Assert.Null (top.Focused);
+			Assert.Equal (top, Application.Current);
+			Assert.True (top.IsCurrentTop);
+			Assert.Equal (top, Application.MdiTop);
+			Application.Begin (win1);
+			Assert.Equal (new Rect (0, 0, 40, 25), win1.Frame);
+			Assert.NotEqual (top, Application.Current);
+			Assert.False (top.IsCurrentTop);
+			Assert.Equal (win1, Application.Current);
+			Assert.True (win1.IsCurrentTop);
+			Assert.True (win1.IsMdiChild);
+			Assert.Null (top.Focused);
+			Assert.Null (top.MostFocused);
+			Assert.Equal (win1.Subviews [0], win1.Focused);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.Single (Application.MdiChildes);
+			Application.Begin (win2);
+			Assert.Equal (new Rect (0, 0, 40, 25), win2.Frame);
+			Assert.NotEqual (top, Application.Current);
+			Assert.False (top.IsCurrentTop);
+			Assert.Equal (win2, Application.Current);
+			Assert.True (win2.IsCurrentTop);
+			Assert.True (win2.IsMdiChild);
+			Assert.Null (top.Focused);
+			Assert.Null (top.MostFocused);
+			Assert.Equal (win2.Subviews [0], win2.Focused);
+			Assert.Equal (tf1W2, win2.MostFocused);
+			Assert.Equal (2, Application.MdiChildes.Count);
+
+			Application.ShowChild (win1);
+			Assert.Equal (win1, Application.Current);
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			win1.Running = true;
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.QuitKey, new KeyModifiers ())));
+			Assert.False (isRunning);
+			Assert.False (win1.Running);
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			Assert.False (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.F5, new KeyModifiers ())));
+
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.True (win1.IsCurrentTop);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf2W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.I | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf2W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf2W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win2, Application.MdiChildes [0]);
+			Assert.Equal (tf1W2, win2.MostFocused);
+			tf2W2.SetFocus ();
+			Assert.True (tf2W2.HasFocus);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ())));
+			Assert.Equal (win2, Application.MdiChildes [0]);
+			Assert.Equal (tf2W2, win2.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf1W1, win1.MostFocused);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.Equal (new Point (0, 0), tvW1.CursorPosition);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tvW1, win1.MostFocused);
+			Assert.Equal (new Point (16, 1), tvW1.CursorPosition);
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (win1, Application.MdiChildes [0]);
+			Assert.Equal (tf2W1, win1.MostFocused);
+
+			Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.L | Key.CtrlMask, new KeyModifiers ())));
+		}
+
+		[Fact]
+		public void Added_Event_Should_Not_Be_Used_To_Initialize_Toplevel_Events ()
+		{
+			Key alternateForwardKey = default;
+			Key alternateBackwardKey = default;
+			Key quitKey = default;
+			var wasAdded = false;
+
+			var view = new View ();
+			view.Added += View_Added;
+
+			void View_Added (View obj)
+			{
+				Assert.Throws<NullReferenceException> (() => Application.Top.AlternateForwardKeyChanged += (e) => alternateForwardKey = e);
+				Assert.Throws<NullReferenceException> (() => Application.Top.AlternateBackwardKeyChanged += (e) => alternateBackwardKey = e);
+				Assert.Throws<NullReferenceException> (() => Application.Top.QuitKeyChanged += (e) => quitKey = e);
+				Assert.False (wasAdded);
+				wasAdded = true;
+				view.Added -= View_Added;
+			}
+
+			var win = new Window ();
+			win.Add (view);
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+			var top = Application.Top;
+			top.Add (win);
+
+			Assert.True (wasAdded);
+
+			Application.Shutdown ();
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void AlternateForwardKeyChanged_AlternateBackwardKeyChanged_QuitKeyChanged_Events ()
+		{
+			Key alternateForwardKey = default;
+			Key alternateBackwardKey = default;
+			Key quitKey = default;
+
+			var view = new View ();
+			view.Initialized += View_Initialized;
+
+			void View_Initialized (object sender, EventArgs e)
+			{
+				Application.Top.AlternateForwardKeyChanged += (e) => alternateForwardKey = e;
+				Application.Top.AlternateBackwardKeyChanged += (e) => alternateBackwardKey = e;
+				Application.Top.QuitKeyChanged += (e) => quitKey = e;
+			}
+
+			var win = new Window ();
+			win.Add (view);
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+
+			Assert.Equal (Key.Null, alternateForwardKey);
+			Assert.Equal (Key.Null, alternateBackwardKey);
+			Assert.Equal (Key.Null, quitKey);
+
+			Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey);
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+
+			Application.AlternateForwardKey = Key.A;
+			Application.AlternateBackwardKey = Key.B;
+			Application.QuitKey = Key.C;
+
+			Assert.Equal (Key.PageDown | Key.CtrlMask, alternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, alternateBackwardKey);
+			Assert.Equal (Key.Q | Key.CtrlMask, quitKey);
+
+			Assert.Equal (Key.A, Application.AlternateForwardKey);
+			Assert.Equal (Key.B, Application.AlternateBackwardKey);
+			Assert.Equal (Key.C, Application.QuitKey);
+
+			// Replacing the defaults keys to avoid errors on others unit tests that are using it.
+			Application.AlternateForwardKey = Key.PageDown | Key.CtrlMask;
+			Application.AlternateBackwardKey = Key.PageUp | Key.CtrlMask;
+			Application.QuitKey = Key.Q | Key.CtrlMask;
+
+			Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey);
+			Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey);
+			Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey);
+		}
 	}
-}
+}

+ 25 - 0
UnitTests/ViewTests.cs

@@ -1574,5 +1574,30 @@ namespace Terminal.Gui.Views {
 				return runesCount;
 			}
 		}
+
+		[Fact]
+		public void GetTopSuperView_Test ()
+		{
+			var v1 = new View ();
+			var fv1 = new FrameView ();
+			fv1.Add (v1);
+			var tf1 = new TextField ();
+			var w1 = new Window ();
+			w1.Add (fv1, tf1);
+			var top1 = new Toplevel ();
+			top1.Add (w1);
+
+			var v2 = new View ();
+			var fv2 = new FrameView ();
+			fv2.Add (v2);
+			var tf2 = new TextField ();
+			var w2 = new Window ();
+			w2.Add (fv2, tf2);
+			var top2 = new Toplevel ();
+			top2.Add (w2);
+
+			Assert.Equal (top1, v1.GetTopSuperView ());
+			Assert.Equal (top2, v2.GetTopSuperView ());
+		}
 	}
 }

Some files were not shown because too many files changed in this diff