浏览代码

Fixes #1210. Added AllowsReturn, AllowsTab and Multiline into the TextView. (#1249)

* Fixes #1234. Setting Handled to true in the KeyPress event avoids ProcessKey from running.

* Using literals values in Assert.Equal.

* Can't use numbers variables on the left side of an Assert.Equal/NotEqual, it must be literal (Linux only).

* Fixes #1241. Added SendKeys feature to the ConsoleDriver.

* Fixes SendKeys unit test.

* Changed Key.Null to '\0' (null string).

* Fixes the space bar when there are another previous char.

* Now the drivers are reading up to 255 from ASCII table.

* Fixes CursesDriver and WindowsDriver SendKeys.

* Fixes NetDriver SendKeys.

* Fixed unit test.

* Fixes #1210.  Added AllowsReturn, AllowsTab and Multiline into the TextView.

* Added a Single Line and Multiline toggle to the Text.cs scenario.

* Removing comment.
BDisp 4 年之前
父节点
当前提交
c27da1b982

+ 24 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -674,12 +674,14 @@ namespace Terminal.Gui {
 			//}
 		}
 
+		Action<KeyEvent> keyHandler;
 		Action<MouseEvent> mouseHandler;
 
 		public override void PrepareToRun (MainLoop mainLoop, Action<KeyEvent> keyHandler, Action<KeyEvent> keyDownHandler, Action<KeyEvent> keyUpHandler, Action<MouseEvent> mouseHandler)
 		{
 			// Note: Curses doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called
 			Curses.timeout (0);
+			this.keyHandler = keyHandler;
 			this.mouseHandler = mouseHandler;
 
 			var mLoop = mainLoop.Driver as UnixMainLoop;
@@ -946,6 +948,28 @@ namespace Terminal.Gui {
 		{
 			return false;
 		}
+
+		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+		{
+			Key k;
+
+			if ((shift || alt || control)
+				&& keyChar - (int)Key.Space >= (uint)Key.A && keyChar - (int)Key.Space <= (uint)Key.Z) {
+				k = (Key)(keyChar - (uint)Key.Space);
+			} else {
+				k = (Key)keyChar;
+			}
+			if (shift) {
+				k |= Key.ShiftMask;
+			}
+			if (alt) {
+				k |= Key.AltMask;
+			}
+			if (control) {
+				k |= Key.CtrlMask;
+			}
+			keyHandler (new KeyEvent (k, MapKeyModifiers (k)));
+		}
 	}
 
 	internal static class Platform {

+ 1 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs

@@ -798,7 +798,7 @@ namespace Terminal.Gui {
 			if (MockKeyPresses.Count > 0) {
 				return MockKeyPresses.Pop();
 			} else {
-				return new ConsoleKeyInfo ('~', ConsoleKey.Oem3, false,false,false);
+				return new ConsoleKeyInfo ('\0', (ConsoleKey)'\0', false,false,false);
 			}
 		}
 

+ 55 - 38
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -282,7 +282,7 @@ namespace Terminal.Gui {
 			case ConsoleKey.Enter:
 				return MapKeyModifiers (keyInfo, Key.Enter);
 			case ConsoleKey.Spacebar:
-				return MapKeyModifiers (keyInfo, Key.Space);
+				return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar);
 			case ConsoleKey.Backspace:
 				return MapKeyModifiers (keyInfo, Key.Backspace);
 			case ConsoleKey.Delete:
@@ -348,6 +348,9 @@ namespace Terminal.Gui {
 
 				return (Key)((uint)Key.F1 + delta);
 			}
+			if (keyInfo.KeyChar != 0) {
+				return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar));
+			}
 
 			return (Key)(0xffffffff);
 		}
@@ -367,55 +370,46 @@ namespace Terminal.Gui {
 			return keyMod != Key.Null ? keyMod | key : key;
 		}
 
+		Action<KeyEvent> keyHandler;
+		Action<KeyEvent> keyUpHandler;
+
 		public override void PrepareToRun (MainLoop mainLoop, Action<KeyEvent> keyHandler, Action<KeyEvent> keyDownHandler, Action<KeyEvent> keyUpHandler, Action<MouseEvent> mouseHandler)
 		{
-			// Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called
-			(mainLoop.Driver as FakeMainLoop).KeyPressed = delegate (ConsoleKeyInfo consoleKey) {
-				var map = MapKey (consoleKey);
-				if (map == (Key)0xffffffff)
-					return;
-
-				if (keyModifiers == null)
-					keyModifiers = new KeyModifiers ();
-				switch (consoleKey.Modifiers) {
-				case ConsoleModifiers.Alt:
-					keyModifiers.Alt = true;
-					break;
-				case ConsoleModifiers.Shift:
-					keyModifiers.Shift = true;
-					break;
-				case ConsoleModifiers.Control:
-					keyModifiers.Ctrl = true;
-					break;
-				}
+			this.keyHandler = keyHandler;
+			this.keyUpHandler = keyUpHandler;
 
-				keyHandler (new KeyEvent (map, keyModifiers));
-				keyUpHandler (new KeyEvent (map, keyModifiers));
-				keyModifiers = new KeyModifiers ();
-			};
+			// Note: Net doesn't support keydown/up events and thus any passed keyDown/UpHandlers will never be called
+			(mainLoop.Driver as FakeMainLoop).KeyPressed += (consoleKey) => ProcessInput (consoleKey);
 		}
 
-		public override Attribute GetAttribute ()
+		void ProcessInput (ConsoleKeyInfo consoleKey)
 		{
-			return currentAttribute;
-		}
+			var map = MapKey (consoleKey);
+			if (map == (Key)0xffffffff)
+				return;
 
-		#region Unused
-		public override void SetColors (ConsoleColor foreground, ConsoleColor background)
-		{
-		}
+			if (keyModifiers == null) {
+				keyModifiers = new KeyModifiers ();
+			}
 
-		public override void SetColors (short foregroundColorId, short backgroundColorId)
-		{
-			throw new NotImplementedException ();
-		}
+			if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Alt)) {
+				keyModifiers.Alt = true;
+			}
+			if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Shift)) {
+				keyModifiers.Shift = true;
+			}
+			if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Control)) {
+				keyModifiers.Ctrl = true;
+			}
 
-		public override void CookMouse ()
-		{
+			keyHandler (new KeyEvent (map, keyModifiers));
+			keyUpHandler (new KeyEvent (map, keyModifiers));
+			keyModifiers = new KeyModifiers ();
 		}
 
-		public override void UncookMouse ()
+		public override Attribute GetAttribute ()
 		{
+			return currentAttribute;
 		}
 
 		/// <inheritdoc/>
@@ -438,6 +432,29 @@ namespace Terminal.Gui {
 			return false;
 		}
 
+		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+		{
+			ProcessInput (new ConsoleKeyInfo (keyChar, key, shift, alt, control));
+		}
+
+		#region Unused
+		public override void SetColors (ConsoleColor foreground, ConsoleColor background)
+		{
+		}
+
+		public override void SetColors (short foregroundColorId, short backgroundColorId)
+		{
+			throw new NotImplementedException ();
+		}
+
+		public override void CookMouse ()
+		{
+		}
+
+		public override void UncookMouse ()
+		{
+		}
+
 		#endregion
 #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
 	}

+ 24 - 7
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -226,7 +226,7 @@ namespace Terminal.Gui {
 		void GetConsoleInputType (ConsoleKeyInfo consoleKeyInfo)
 		{
 			InputResult inputResult = new InputResult {
-				EventType = EventType.key
+				EventType = EventType.Key
 			};
 			ConsoleKeyInfo newConsoleKeyInfo = consoleKeyInfo;
 			ConsoleKey key = 0;
@@ -280,7 +280,7 @@ namespace Terminal.Gui {
 				newConsoleKeyInfo = consoleKeyInfo;
 				break;
 			}
-			if (inputResult.EventType == EventType.key) {
+			if (inputResult.EventType == EventType.Key) {
 				inputResult.ConsoleKeyInfo = newConsoleKeyInfo;
 			} else {
 				inputResult.MouseEvent = mouseEvent;
@@ -440,7 +440,7 @@ namespace Terminal.Gui {
 				GetMouseEvent (cki);
 				return;
 			}
-			if (inputResult.EventType == EventType.key) {
+			if (inputResult.EventType == EventType.Key) {
 				inputResult.ConsoleKeyInfo = newConsoleKeyInfo;
 			} else {
 				inputResult.MouseEvent = mouseEvent;
@@ -1010,7 +1010,7 @@ namespace Terminal.Gui {
 		}
 
 		public enum EventType {
-			key = 1,
+			Key = 1,
 			Mouse = 2,
 			WindowSize = 3,
 			WindowPosition = 4
@@ -1441,7 +1441,7 @@ namespace Terminal.Gui {
 			case ConsoleKey.Enter:
 				return MapKeyModifiers (keyInfo, Key.Enter);
 			case ConsoleKey.Spacebar:
-				return MapKeyModifiers (keyInfo, Key.Space);
+				return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar);
 			case ConsoleKey.Backspace:
 				return MapKeyModifiers (keyInfo, Key.Backspace);
 			case ConsoleKey.Delete:
@@ -1506,7 +1506,7 @@ namespace Terminal.Gui {
 				return (Key)((uint)Key.F1 + delta);
 			}
 			if (keyInfo.KeyChar != 0) {
-				return (Key)((uint)keyInfo.KeyChar);
+				return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar));
 			}
 
 			return (Key)(0xffffffff);
@@ -1557,7 +1557,7 @@ namespace Terminal.Gui {
 		void ProcessInput (NetEvents.InputResult inputEvent)
 		{
 			switch (inputEvent.EventType) {
-			case NetEvents.EventType.key:
+			case NetEvents.EventType.Key:
 				var map = MapKey (inputEvent.ConsoleKeyInfo);
 				if (map == (Key)0xffffffff) {
 					return;
@@ -1744,6 +1744,23 @@ namespace Terminal.Gui {
 		{
 			return false;
 		}
+
+		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+		{
+			NetEvents.InputResult input = new NetEvents.InputResult ();
+			ConsoleKey ck;
+			if (char.IsLetter (keyChar)) {
+				ck = key;
+			} else {
+				ck = (ConsoleKey)'\0';
+			}
+			input.EventType = NetEvents.EventType.Key;
+			input.ConsoleKeyInfo = new ConsoleKeyInfo (keyChar, ck, shift, alt, control);
+
+			try {
+				ProcessInput (input);
+			} catch (OverflowException) { }
+		}
 		#endregion
 
 		//

+ 64 - 10
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -116,23 +116,23 @@ namespace Terminal.Gui {
 				var err = Marshal.GetLastWin32Error ();
 				if (err != 0) {
 					throw new System.ComponentModel.Win32Exception (err);
-				}				
+				}
 				visibility = Gui.CursorVisibility.Default;
 
 				return false;
 			}
 
-			if (!info.bVisible)        
+			if (!info.bVisible)
 				visibility = CursorVisibility.Invisible;
-			else if (info.dwSize > 50) 
+			else if (info.dwSize > 50)
 				visibility = CursorVisibility.Box;
-			else                       
+			else
 				visibility = CursorVisibility.Underline;
 
 			return true;
 		}
 
-		public bool EnsureCursorVisibility () 
+		public bool EnsureCursorVisibility ()
 		{
 			if (initialCursorVisibility.HasValue && pendingCursorVisibility.HasValue && SetCursorVisibility (pendingCursorVisibility.Value)) {
 				pendingCursorVisibility = null;
@@ -161,11 +161,11 @@ namespace Terminal.Gui {
 
 			if (currentCursorVisibility.HasValue == false || currentCursorVisibility.Value != visibility) {
 				ConsoleCursorInfo info = new ConsoleCursorInfo {
-					dwSize   =  (uint) visibility & 0x00FF,
-					bVisible = ((uint) visibility & 0xFF00) != 0
+					dwSize = (uint)visibility & 0x00FF,
+					bVisible = ((uint)visibility & 0xFF00) != 0
 				};
 
-				if (!SetConsoleCursorInfo (ScreenBuffer, ref info)) 
+				if (!SetConsoleCursorInfo (ScreenBuffer, ref info))
 					return false;
 
 				currentCursorVisibility = visibility;
@@ -1063,7 +1063,7 @@ namespace Terminal.Gui {
 			case ConsoleKey.Enter:
 				return MapKeyModifiers (keyInfo, Key.Enter);
 			case ConsoleKey.Spacebar:
-				return MapKeyModifiers (keyInfo, Key.Space);
+				return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar);
 			case ConsoleKey.Backspace:
 				return MapKeyModifiers (keyInfo, Key.Backspace);
 			case ConsoleKey.Delete:
@@ -1154,7 +1154,7 @@ namespace Terminal.Gui {
 				return (Key)((uint)Key.F1 + delta);
 			}
 			if (keyInfo.KeyChar != 0) {
-				return (Key)((uint)keyInfo.KeyChar);
+				return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar));
 			}
 
 			return (Key)(0xffffffff);
@@ -1419,6 +1419,60 @@ namespace Terminal.Gui {
 		public override void CookMouse ()
 		{
 		}
+
+		public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control)
+		{
+			WindowsConsole.InputRecord input = new WindowsConsole.InputRecord ();
+			input.EventType = WindowsConsole.EventType.Key;
+
+			WindowsConsole.KeyEventRecord keyEvent = new WindowsConsole.KeyEventRecord ();
+			keyEvent.bKeyDown = true;
+			WindowsConsole.ControlKeyState controlKey = new WindowsConsole.ControlKeyState ();
+			if (shift) {
+				controlKey |= WindowsConsole.ControlKeyState.ShiftPressed;
+				keyEvent.UnicodeChar = '\0';
+				keyEvent.wVirtualKeyCode = 16;
+			}
+			if (alt) {
+				controlKey |= WindowsConsole.ControlKeyState.LeftAltPressed;
+				controlKey |= WindowsConsole.ControlKeyState.RightAltPressed;
+				keyEvent.UnicodeChar = '\0';
+				keyEvent.wVirtualKeyCode = 18;
+			}
+			if (control) {
+				controlKey |= WindowsConsole.ControlKeyState.LeftControlPressed;
+				controlKey |= WindowsConsole.ControlKeyState.RightControlPressed;
+				keyEvent.UnicodeChar = '\0';
+				keyEvent.wVirtualKeyCode = 17;
+			}
+			keyEvent.dwControlKeyState = controlKey;
+
+			input.KeyEvent = keyEvent;
+
+			if (shift || alt || control) {
+				ProcessInput (input);
+			}
+
+			keyEvent.UnicodeChar = keyChar;
+			if ((shift || alt || control)
+				&& (key >= ConsoleKey.A && key <= ConsoleKey.Z
+				|| key >= ConsoleKey.D0 && key <= ConsoleKey.D9)) {
+				keyEvent.wVirtualKeyCode = (ushort)key;
+			} else {
+				keyEvent.wVirtualKeyCode = '\0';
+			}
+
+			input.KeyEvent = keyEvent;
+
+			try {
+				ProcessInput (input);
+			} catch (OverflowException) { }
+			finally {
+				keyEvent.bKeyDown = false;
+				input.KeyEvent = keyEvent;
+				ProcessInput (input);
+			}
+		}
 		#endregion
 	}
 

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

@@ -739,6 +739,16 @@ namespace Terminal.Gui {
 		/// <param name="backgroundColorId">Background color identifier.</param>
 		public abstract void SetColors (short foregroundColorId, short backgroundColorId);
 
+		/// <summary>
+		/// Allows sending keys without typing on a keyboard.
+		/// </summary>
+		/// <param name="keyChar">The character key.</param>
+		/// <param name="key">The key.</param>
+		/// <param name="shift">If shift key is sending.</param>
+		/// <param name="alt">If alt key is sending.</param>
+		/// <param name="control">If control key is sending.</param>
+		public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control);
+
 		/// <summary>
 		/// Set the handler when the terminal is resized.
 		/// </summary>

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

@@ -75,7 +75,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The key code representing null or empty
 		/// </summary>
-		Null = 0,
+		Null = '\0',
 
 		/// <summary>
 		/// The key code for the user pressing the return key.

+ 62 - 16
Terminal.Gui/Core/TextFormatter.cs

@@ -273,7 +273,7 @@ namespace Terminal.Gui {
 		/// <remarks>
 		/// <para>
 		/// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
-		/// <see cref="Format(ustring, int, bool, bool, bool)"/> will be called internally. 
+		/// <see cref="Format(ustring, int, bool, bool, bool, int)"/> will be called internally. 
 		/// </para>
 		/// </remarks>
 		public List<ustring> Lines {
@@ -371,6 +371,7 @@ namespace Terminal.Gui {
 		/// <param name="width">The width to contain the text to</param>
 		/// <param name="preserveTrailingSpaces">If <c>true</c>, the wrapped text will keep the trailing spaces.
 		///  If <c>false</c>, the trailing spaces will be trimmed.</param>
+		/// <param name="tabWidth">The tab width.</param>
 		/// <returns>Returns a list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -380,7 +381,7 @@ namespace Terminal.Gui {
 		/// This method strips Newline ('\n' and '\r\n') sequences before processing.
 		/// </para>
 		/// </remarks>
-		public static List<ustring> WordWrap (ustring text, int width, bool preserveTrailingSpaces = false)
+		public static List<ustring> WordWrap (ustring text, int width, bool preserveTrailingSpaces = false, int tabWidth = 0)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -394,16 +395,58 @@ namespace Terminal.Gui {
 			}
 
 			var runes = StripCRLF (text).ToRuneList ();
+			if (!preserveTrailingSpaces) {
+				while ((end = start + width) < runes.Count) {
+					while (runes [end] != ' ' && end > start)
+						end--;
+					if (end == start)
+						end = start + width;
+					lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+					start = end;
+					if (runes [end] == ' ') {
+						start++;
+					}
+				}
+			} else {
+				while ((end = start) < runes.Count) {
+					end = GetNextWhiteSpace (start, width);
+					lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+					start = end;
+				}
+			}
 
-			while ((end = start + width) < runes.Count) {
-				while (runes [end] != ' ' && end > start)
-					end -= 1;
-				if (end == start)
-					end = start + width;
-				lines.Add (ustring.Make (runes.GetRange (start, end - start)));
-				start = end;
-				if (runes [end] == ' ' && !preserveTrailingSpaces) {
-					start++;
+			int GetNextWhiteSpace (int from, int cWidth, int cLength = 0)
+			{
+				var to = from;
+				var length = cLength;
+
+				while (length < cWidth && to < runes.Count) {
+					var rune = runes [to];
+					length += Rune.ColumnWidth (rune);
+					if (rune == ' ') {
+						if (length == cWidth) {
+							return to + 1;
+						} else if (length > cWidth) {
+							return to;
+						} else {
+							return GetNextWhiteSpace (to + 1, cWidth, length);
+						}
+					} else if (rune == '\t') {
+						length += tabWidth + 1;
+						if (length == tabWidth && tabWidth > cWidth) {
+							return to + 1;
+						} else if (length > cWidth && tabWidth > cWidth) {
+							return to;
+						} else {
+							return GetNextWhiteSpace (to + 1, cWidth, length);
+						}
+					}
+					to++;
+				}
+				if (cLength > 0 && to < runes.Count && runes [to] != ' ') {
+					return from;
+				} else {
+					return to;
 				}
 			}
 
@@ -502,6 +545,7 @@ namespace Terminal.Gui {
 		/// <param name="talign">Specifies how the text will be aligned horizontally.</param>
 		/// <param name="wordWrap">If <c>true</c>, the text will be wrapped to new lines as need. If <c>false</c>, forces text to fit a single line. Line breaks are converted to spaces. The text will be clipped to <c>width</c></param>
 		/// <param name="preserveTrailingSpaces">If <c>true</c> and 'wordWrap' also true, the wrapped text will keep the trailing spaces. If <c>false</c>, the trailing spaces will be trimmed.</param>
+		/// <param name="tabWidth">The tab width.</param>
 		/// <returns>A list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -514,9 +558,9 @@ namespace Terminal.Gui {
 		/// If <c>width</c> is int.MaxValue, the text will be formatted to the maximum width possible. 
 		/// </para>
 		/// </remarks>
-		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false)
+		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0)
 		{
-			return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces);
+			return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth);
 		}
 
 		/// <summary>
@@ -527,6 +571,7 @@ namespace Terminal.Gui {
 		/// <param name="justify">Specifies whether the text should be justified.</param>
 		/// <param name="wordWrap">If <c>true</c>, the text will be wrapped to new lines as need. If <c>false</c>, forces text to fit a single line. Line breaks are converted to spaces. The text will be clipped to <c>width</c></param>
 		/// <param name="preserveTrailingSpaces">If <c>true</c> and 'wordWrap' also true, the wrapped text will keep the trailing spaces. If <c>false</c>, the trailing spaces will be trimmed.</param>
+		/// <param name="tabWidth">The tab width.</param>
 		/// <returns>A list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -539,7 +584,8 @@ namespace Terminal.Gui {
 		/// If <c>width</c> is int.MaxValue, the text will be formatted to the maximum width possible. 
 		/// </para>
 		/// </remarks>
-		public static List<ustring> Format (ustring text, int width, bool justify, bool wordWrap, bool preserveTrailingSpaces = false)
+		public static List<ustring> Format (ustring text, int width, bool justify, bool wordWrap,
+			bool preserveTrailingSpaces = false, int tabWidth = 0)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("width cannot be negative");
@@ -566,7 +612,7 @@ namespace Terminal.Gui {
 			for (int i = 0; i < runeCount; i++) {
 				Rune c = runes [i];
 				if (c == '\n') {
-					var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces);
+					var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth);
 					foreach (var line in wrappedLines) {
 						lineResult.Add (ClipAndJustify (line, width, justify));
 					}
@@ -576,7 +622,7 @@ namespace Terminal.Gui {
 					lp = i + 1;
 				}
 			}
-			foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces)) {
+			foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth)) {
 				lineResult.Add (ClipAndJustify (line, width, justify));
 			}
 

+ 7 - 4
Terminal.Gui/Views/TextField.cs

@@ -200,7 +200,7 @@ namespace Terminal.Gui {
 				if (idx == point)
 					break;
 				var cols = Rune.ColumnWidth (text [idx]);
-				col = TextModel.SetCol (col, Frame.Width - 1, cols);
+				TextModel.SetCol (ref col, Frame.Width - 1, cols);
 			}
 			Move (col, 0);
 		}
@@ -234,7 +234,9 @@ namespace Terminal.Gui {
 				if (col + cols <= width) {
 					Driver.AddRune ((Rune)(Secret ? '*' : rune));
 				}
-				col = TextModel.SetCol (col, width, cols);
+				if (!TextModel.SetCol (ref col, width, cols)) {
+					break;
+				}
 				if (idx + 1 < tcount && col + Rune.ColumnWidth (text [idx + 1]) > width) {
 					break;
 				}
@@ -256,7 +258,7 @@ namespace Terminal.Gui {
 			} else if (first + point - (Frame.Width + offB) == 0 ||
 				  TextModel.DisplaySize (text, first, point).size >= Frame.Width + offB) {
 				first = Math.Max (TextModel.CalculateLeftColumn (text, first,
-					point, Frame.Width - 1 + offB, point), 0);
+					point, Frame.Width + offB), 0);
 			}
 			SetNeedsDisplay ();
 		}
@@ -399,7 +401,7 @@ namespace Terminal.Gui {
 			case Key.CursorUp | Key.ShiftMask | Key.CtrlMask:
 			case (Key)((int)'B' + Key.ShiftMask | Key.AltMask):
 				if (point > 0) {
-					int x = Math.Min (start > -1 && start > point ? start : point, text.Count - 1);
+					int x = Math.Min (start > -1 && start > point ? start : point, text.Count);
 					if (x > 0) {
 						int sbw = WordBackward (x);
 						if (sbw != -1)
@@ -895,6 +897,7 @@ namespace Terminal.Gui {
 			point = selStart + cbTxt.RuneCount;
 			ClearAllSelection ();
 			SetNeedsDisplay ();
+			Adjust ();
 		}
 
 		/// <summary>

+ 274 - 89
Terminal.Gui/Views/TextView.cs

@@ -181,6 +181,9 @@ namespace Terminal.Gui {
 		public void RemoveLine (int pos)
 		{
 			if (lines.Count > 0) {
+				if (lines.Count == 1 && lines [0].Count == 0) {
+					return;
+				}
 				lines.RemoveAt (pos);
 			}
 		}
@@ -190,12 +193,15 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="first">The first line.</param>
 		/// <param name="last">The last line.</param>
-		public int GetMaxVisibleLine (int first, int last)
+		/// <param name="tabWidth">The tab width.</param>
+		public int GetMaxVisibleLine (int first, int last, int tabWidth)
 		{
 			int maxLength = 0;
 			last = last < lines.Count ? last : lines.Count;
 			for (int i = first; i < last; i++) {
-				var l = GetLine (i).Count;
+				var line = GetLine (i);
+				var tabSum = line.Sum (r => r == '\t' ? tabWidth - 1 : 0);
+				var l = line.Count + tabSum;
 				if (l > maxLength) {
 					maxLength = l;
 				}
@@ -204,16 +210,17 @@ namespace Terminal.Gui {
 			return maxLength;
 		}
 
-		internal static int SetCol (int col, int width, int cols)
+		internal static bool SetCol (ref int col, int width, int cols)
 		{
 			if (col + cols <= width) {
 				col += cols;
+				return true;
 			}
 
-			return col;
+			return false;
 		}
 
-		internal static int GetColFromX (List<Rune> t, int start, int x)
+		internal static int GetColFromX (List<Rune> t, int start, int x, int tabWidth = 0)
 		{
 			if (x < 0) {
 				return x;
@@ -223,6 +230,9 @@ namespace Terminal.Gui {
 			for (int i = start; i < t.Count; i++) {
 				var r = t [i];
 				size += Rune.ColumnWidth (r);
+				if (r == '\t' && tabWidth > 0) {
+					size += tabWidth + 1;
+				}
 				if (i == pX || (size > pX)) {
 					return i - start;
 				}
@@ -231,7 +241,8 @@ namespace Terminal.Gui {
 		}
 
 		// Returns the size and length in a range of the string.
-		internal static (int size, int length) DisplaySize (List<Rune> t, int start = -1, int end = -1, bool checkNextRune = true)
+		internal static (int size, int length) DisplaySize (List<Rune> t, int start = -1, int end = -1,
+			bool checkNextRune = true, int tabWidth = 0)
 		{
 			if (t == null || t.Count == 0) {
 				return (0, 0);
@@ -244,39 +255,59 @@ namespace Terminal.Gui {
 				var rune = t [i];
 				size += Rune.ColumnWidth (rune);
 				len += Rune.RuneLen (rune);
-				if (checkNextRune && i == tcount - 1 && t.Count > tcount && Rune.ColumnWidth (t [i + 1]) > 1) {
-					size += Rune.ColumnWidth (t [i + 1]);
-					len += Rune.RuneLen (t [i + 1]);
+				if (rune == '\t' && tabWidth > 0) {
+					size += tabWidth + 1;
+					len += tabWidth - 1;
+				}
+				if (checkNextRune && i == tcount - 1 && t.Count > tcount
+					&& IsWideRune (t [i + 1], tabWidth, out int s, out int l)) {
+					size += s;
+					len += l;
+				}
+			}
+
+			bool IsWideRune (Rune r, int tWidth, out int s, out int l)
+			{
+				s = Rune.ColumnWidth (r);
+				l = Rune.RuneLen (r);
+				if (r == '\t' && tWidth > 0) {
+					s += tWidth + 1;
+					l += tWidth - 1;
 				}
+
+				return s > 1;
 			}
+
 			return (size, len);
 		}
 
 		// Returns the left column in a range of the string.
-		internal static int CalculateLeftColumn (List<Rune> t, int start, int end, int width, int currentColumn)
+		internal static int CalculateLeftColumn (List<Rune> t, int start, int end, int width, int tabWidth = 0)
 		{
-			if (t == null) {
+			if (t == null || t.Count == 0) {
 				return 0;
 			}
-			(var dSize, _) = TextModel.DisplaySize (t, start, end);
-			if (dSize < width) {
-				return start;
-			}
 			int size = 0;
 			int tcount = end > t.Count - 1 ? t.Count - 1 : end;
 			int col = 0;
-			for (int i = tcount; i > start; i--) {
+
+			for (int i = tcount; i >= 0; i--) {
 				var rune = t [i];
-				var s = Rune.ColumnWidth (rune);
-				size += s;
-				if (size >= dSize - width) {
-					col = tcount - i + start;
-					if (start == 0 || col == start || (currentColumn == t.Count && (currentColumn - col > width))) {
+				size += Rune.ColumnWidth (rune);
+				if (rune == '\t') {
+					size += tabWidth + 1;
+				}
+				if (size > width) {
+					if (end == t.Count) {
 						col++;
 					}
 					break;
+				} else if (end < t.Count && col > 0 && start < end && col == start) {
+					break;
 				}
+				col = i;
 			}
+
 			return col;
 		}
 
@@ -479,7 +510,7 @@ namespace Terminal.Gui {
 		}
 
 		public TextModel WrapModel (int width, out int nRow, out int nCol, out int nStartRow, out int nStartCol,
-			int row = 0, int col = 0, int startRow = 0, int startCol = 0)
+			int row = 0, int col = 0, int startRow = 0, int startCol = 0, int tabWidth = 0)
 		{
 			frameWidth = width;
 
@@ -500,7 +531,8 @@ namespace Terminal.Gui {
 			for (int i = 0; i < Model.Count; i++) {
 				var line = Model.GetLine (i);
 				var wrappedLines = ToListRune (
-					TextFormatter.Format (ustring.Make (line), width, TextAlignment.Left, true, true));
+					TextFormatter.Format (ustring.Make (line), width,
+					TextAlignment.Left, true, true, tabWidth));
 				int sumColWidth = 0;
 				for (int j = 0; j < wrappedLines.Count; j++) {
 					var wrapLine = wrappedLines [j];
@@ -841,6 +873,10 @@ namespace Terminal.Gui {
 		bool wordWrap;
 		WordWrapManager wrapManager;
 		bool continuousFind;
+		int tabWidth = 4;
+		bool allowsTab = true;
+		bool allowsReturn = true;
+		bool multiline = true;
 
 		/// <summary>
 		/// Raised when the <see cref="Text"/> of the <see cref="TextView"/> changes.
@@ -939,7 +975,8 @@ namespace Terminal.Gui {
 					out int nRow, out int nCol,
 					out int nStartRow, out int nStartCol,
 					currentRow, currentColumn,
-					selectionStartRow, selectionStartColumn);
+					selectionStartRow, selectionStartColumn,
+					tabWidth);
 				currentRow = nRow;
 				currentColumn = nCol;
 				selectionStartRow = nStartRow;
@@ -961,7 +998,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the maximum visible length line.
 		/// </summary>
-		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height);
+		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth);
 
 		/// <summary>
 		/// Gets the  number of lines.
@@ -1074,6 +1111,104 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int RightOffset { get; set; }
 
+		/// <summary>
+		/// Gets or sets a value indicating whether pressing ENTER in a <see cref="TextView"/>
+		/// creates a new line of text in the view or activates the default button for the toplevel.
+		/// </summary>
+		public bool AllowsReturn {
+			get => allowsReturn;
+			set {
+				allowsReturn = value;
+				if (allowsReturn && !multiline) {
+					Multiline = true;
+				}
+				if (!allowsReturn && multiline) {
+					Multiline = false;
+					AllowsTab = false;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets a value indicating whether pressing the TAB key in a <see cref="TextView"/>
+		/// types a TAB character in the view instead of moving the focus to the next view in the tab order.
+		/// </summary>
+		public bool AllowsTab {
+			get => allowsTab;
+			set {
+				allowsTab = value;
+				if (allowsTab && tabWidth == 0) {
+					tabWidth = 4;
+				}
+				if (allowsTab && !multiline) {
+					Multiline = true;
+				}
+				if (!allowsTab && multiline) {
+					Multiline = false;
+				}
+				if (!allowsTab && tabWidth > 0) {
+					tabWidth = 0;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets a value indicating the number of whitespace when pressing the TAB key.
+		/// </summary>
+		public int TabWidth {
+			get => tabWidth;
+			set {
+				tabWidth = Math.Max (value, 0);
+				if (tabWidth == 0 && AllowsTab) {
+					AllowsTab = false;
+				}
+				if (tabWidth > 0 && !AllowsTab) {
+					AllowsTab = true;
+				}
+			}
+		}
+
+		Dim savedHeight = null;
+
+		/// <summary>
+		/// Gets or sets a value indicating whether this <see cref="TextView"/> is a multiline text view.
+		/// </summary>
+		public bool Multiline {
+			get => multiline;
+			set {
+				multiline = value;
+				if (multiline && !allowsTab) {
+					AllowsTab = true;
+				}
+				if (multiline && !allowsReturn) {
+					AllowsReturn = true;
+				}
+
+				if (!multiline) {
+					AllowsReturn = false;
+					AllowsTab = false;
+					currentColumn = 0;
+					currentRow = 0;
+					savedHeight = Height;
+					var lyout = LayoutStyle;
+					if (LayoutStyle == LayoutStyle.Computed) {
+						LayoutStyle = LayoutStyle.Absolute;
+					}
+					Height = 1;
+					LayoutStyle = lyout;
+					SetNeedsDisplay ();
+				} else if (multiline && savedHeight != null) {
+					var lyout = LayoutStyle;
+					if (LayoutStyle == LayoutStyle.Computed) {
+						LayoutStyle = LayoutStyle.Absolute;
+					}
+					Height = savedHeight;
+					LayoutStyle = lyout;
+					SetNeedsDisplay ();
+				}
+			}
+		}
+
 		int GetSelectedLength ()
 		{
 			return SelectedText.Length;
@@ -1168,18 +1303,22 @@ namespace Terminal.Gui {
 			if (line.Count > 0) {
 				retreat = Math.Max (SpecialRune (line [Math.Min (Math.Max (currentColumn - leftColumn - 1, 0), line.Count - 1)])
 					? 1 : 0, 0);
-				for (int idx = leftColumn < 0 ? 0 : leftColumn; idx < line.Count; idx++) {
-					if (idx == currentColumn)
+
+				for (int idx = leftColumn; idx < line.Count; idx++) {
+					if (idx >= currentColumn)
 						break;
 					var cols = Rune.ColumnWidth (line [idx]);
-					col += cols - 1;
+					if (line [idx] == '\t' && TabWidth > 0) {
+						cols += TabWidth + 1;
+					}
+					TextModel.SetCol (ref col, Frame.Width, cols);
 				}
 			}
-			var ccol = currentColumn - leftColumn + retreat + col;
-			if (leftColumn <= currentColumn && ccol < Frame.Width
-				&& topRow <= currentRow && currentRow - topRow < Frame.Height) {
+			col += retreat;
+			if ((col >= leftColumn || col < Frame.Width)
+				&& topRow <= currentRow && currentRow - topRow + BottomOffset < Frame.Height) {
 				ResetCursorVisibility ();
-				Move (ccol, currentRow - topRow);
+				Move (col, currentRow - topRow);
 			} else {
 				SaveCursorVisibility ();
 			}
@@ -1514,53 +1653,61 @@ namespace Terminal.Gui {
 		{
 			ColorNormal ();
 
-			int bottom = bounds.Bottom;
-			int right = bounds.Right;
-			for (int row = bounds.Top; row < bottom; row++) {
-				int textLine = topRow + row;
-				if (textLine >= model.Count) {
-					ColorNormal ();
-					ClearRegion (bounds.Left, row, bounds.Right, row + 1);
-					continue;
-				}
-				var line = model.GetLine (textLine);
+			var offB = OffSetBackground ();
+			int right = Frame.Width + offB.width + RightOffset;
+			int bottom = Frame.Height + offB.height + BottomOffset;
+			var row = 0;
+			for (int idxRow = topRow; idxRow < model.Count; idxRow++) {
+				var line = model.GetLine (idxRow);
 				int lineRuneCount = line.Count;
-				if (line.Count < bounds.Left) {
-					ClearRegion (bounds.Left, row, bounds.Right, row + 1);
-					continue;
-				}
-
-				Move (bounds.Left, row);
 				var col = 0;
-				for (int idx = bounds.Left; idx < right; idx++) {
-					var lineCol = leftColumn + idx;
-					var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol];
+
+				Move (0, idxRow);
+				for (int idxCol = leftColumn; idxCol < lineRuneCount; idxCol++) {
+					var rune = idxCol >= lineRuneCount ? ' ' : line [idxCol];
 					var cols = Rune.ColumnWidth (rune);
-					if (lineCol < line.Count && selecting && PointInSelection (idx + leftColumn, row + topRow)) {
+					if (idxCol < line.Count && selecting && PointInSelection (idxCol, idxRow)) {
 						ColorSelection ();
-					} else if (lineCol == currentColumn && textLine == currentRow && !selecting && !Used
-						&& HasFocus && lineCol < lineRuneCount) {
+					} else if (idxCol == currentColumn && idxRow == currentRow && !selecting && !Used
+						&& HasFocus && idxCol < lineRuneCount) {
 						ColorUsed ();
 					} else {
 						ColorNormal ();
 					}
 
-					if (!SpecialRune (rune)) {
+					if (rune == '\t' && TabWidth > 0) {
+						cols += TabWidth + 1;
+						if (col + cols > right) {
+							cols = right - col;
+						}
+						for (int i = 0; i < cols; i++) {
+							if (col + i < right) {
+								AddRune (col + i, row, ' ');
+							}
+						}
+					} else if (!SpecialRune (rune)) {
 						AddRune (col, row, rune);
 					} else {
 						col++;
 					}
-					col = TextModel.SetCol (col, bounds.Right, cols);
-					if (idx + 1 < lineRuneCount && col + Rune.ColumnWidth (line [idx + 1]) > right) {
+					if (!TextModel.SetCol (ref col, bounds.Right, cols)) {
+						break;
+					}
+					if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth (line [idxCol + 1]) > right) {
 						break;
-					} else if (idx == lineRuneCount - 1) {
-						ColorNormal ();
-						for (int i = col; i < right; i++) {
-							Driver.AddRune (' ');
-						}
 					}
 				}
+				if (col < right) {
+					ColorNormal ();
+					ClearRegion (col, row, right, row + 1);
+				}
+				row++;
 			}
+			if (row < bottom) {
+				ColorNormal ();
+				ClearRegion (bounds.Left, row, right, bottom);
+			}
+
 			PositionCursor ();
 		}
 
@@ -1720,15 +1867,21 @@ namespace Terminal.Gui {
 			var offB = OffSetBackground ();
 			var line = GetCurrentLine ();
 			bool need = !NeedDisplay.IsEmpty || wrapNeeded;
+			var tSize = TextModel.DisplaySize (line, -1, -1, false, TabWidth);
+			var dSize = TextModel.DisplaySize (line, leftColumn, currentColumn, true, TabWidth);
 			if (!wordWrap && currentColumn < leftColumn) {
 				leftColumn = currentColumn;
 				need = true;
-			} else if (!wordWrap && (currentColumn - leftColumn + RightOffset > Frame.Width + offB.width ||
-				TextModel.DisplaySize (line, leftColumn, currentColumn).size + RightOffset >= Frame.Width + offB.width)) {
-				leftColumn = Math.Max (TextModel.CalculateLeftColumn (line, leftColumn,
-					currentColumn + RightOffset, Frame.Width - 1 + offB.width, currentColumn), 0);
+			} else if (!wordWrap && (currentColumn - leftColumn + RightOffset > Frame.Width + offB.width
+				|| dSize.size + RightOffset >= Frame.Width + offB.width)) {
+				leftColumn = TextModel.CalculateLeftColumn (line, leftColumn, currentColumn,
+					Frame.Width + offB.width - RightOffset, TabWidth);
 				need = true;
+			} else if (dSize.size + RightOffset < Frame.Width + offB.width
+				&& tSize.size + RightOffset < Frame.Width + offB.width) {
+				leftColumn = 0;
 			}
+
 			if (currentRow < topRow) {
 				topRow = currentRow;
 				need = true;
@@ -1775,7 +1928,7 @@ namespace Terminal.Gui {
 			if (isRow) {
 				topRow = Math.Max (idx > model.Count - 1 ? model.Count - 1 : idx, 0);
 			} else if (!wordWrap) {
-				var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height + RightOffset);
+				var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height + RightOffset, TabWidth);
 				leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0);
 			}
 			SetNeedsDisplay ();
@@ -1845,7 +1998,7 @@ namespace Terminal.Gui {
 					StopSelecting ();
 				}
 				int nPageDnShift = Frame.Height - 1;
-				if (currentRow < model.Count) {
+				if (currentRow > 0 && currentRow < model.Count) {
 					if (columnTrack == -1)
 						columnTrack = currentColumn;
 					currentRow = (currentRow + nPageDnShift) > model.Count
@@ -1926,7 +2079,7 @@ namespace Terminal.Gui {
 					}
 				}
 				Adjust ();
-				break;
+				return true;
 
 			case Key.B | Key.CtrlMask:
 			case Key.CursorLeft:
@@ -1975,6 +2128,7 @@ namespace Terminal.Gui {
 					StopSelecting ();
 				}
 				currentColumn = 0;
+				leftColumn = 0;
 				Adjust ();
 				break;
 			case Key.DeleteChar:
@@ -2000,9 +2154,8 @@ namespace Terminal.Gui {
 				}
 				currentLine = GetCurrentLine ();
 				currentColumn = currentLine.Count;
-				int pcol = leftColumn;
 				Adjust ();
-				break;
+				return true;
 
 			case Key.K | Key.CtrlMask: // kill-to-end
 			case Key.DeleteChar | Key.CtrlMask | Key.ShiftMask:
@@ -2201,6 +2354,9 @@ namespace Terminal.Gui {
 				break;
 
 			case Key.Enter:
+				if (!AllowsReturn) {
+					return false;
+				}
 				if (isReadOnly)
 					break;
 				currentLine = GetCurrentLine ();
@@ -2259,34 +2415,61 @@ namespace Terminal.Gui {
 				SetNeedsDisplay ();
 				break;
 
-			default:
-				// Ignore control characters and other special keys
-				if (kb.Key < Key.Space || kb.Key > Key.CharMask)
+			case Key _ when ShortcutHelper.GetModifiersKey (kb) == Key.Tab:
+				if (!AllowsTab) {
 					return false;
-				//So that special keys like tab can be processed
-				if (isReadOnly)
-					return true;
-				if (selecting) {
-					ClearSelectedRegion ();
 				}
-				if (Used) {
-					Insert ((uint)kb.Key);
-					currentColumn++;
-					if (currentColumn >= leftColumn + Frame.Width) {
-						leftColumn++;
-						SetNeedsDisplay ();
+				InsertText (new KeyEvent ((Key)'\t', null));
+				break;
+
+			case Key _ when (ShortcutHelper.GetModifiersKey (kb) == (Key.BackTab | Key.ShiftMask)):
+				if (!AllowsTab) {
+					return false;
+				}
+				if (currentColumn > 0) {
+					currentLine = GetCurrentLine ();
+					if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') {
+						currentLine.RemoveAt (currentColumn - 1);
+						currentColumn--;
 					}
-				} else {
-					Insert ((uint)kb.Key);
-					currentColumn++;
 				}
 				break;
+
+			default:
+				// Ignore control characters and other special keys
+				if (kb.Key < Key.Space || kb.Key > Key.CharMask)
+					return false;
+
+				InsertText (kb);
+				break;
 			}
 			DoNeededAction ();
 
 			return true;
 		}
 
+		bool InsertText (KeyEvent kb)
+		{
+			//So that special keys like tab can be processed
+			if (isReadOnly)
+				return true;
+			if (selecting) {
+				ClearSelectedRegion ();
+			}
+			if (Used) {
+				Insert ((uint)kb.Key);
+				currentColumn++;
+				if (currentColumn >= leftColumn + Frame.Width) {
+					leftColumn++;
+					SetNeedsDisplay ();
+				}
+			} else {
+				Insert ((uint)kb.Key);
+				currentColumn++;
+			}
+			return true;
+		}
+
 		///<inheritdoc/>
 		public override bool OnKeyUp (KeyEvent kb)
 		{
@@ -2544,7 +2727,9 @@ namespace Terminal.Gui {
 		public void MoveHome ()
 		{
 			currentRow = 0;
+			topRow = 0;
 			currentColumn = 0;
+			leftColumn = 0;
 			TrackColumn ();
 			PositionCursor ();
 		}
@@ -2871,7 +3056,7 @@ namespace Terminal.Gui {
 					currentRow = Math.Max (ev.Y + topRow, 0);
 				}
 				r = GetCurrentLine ();
-				var idx = TextModel.GetColFromX (r, leftColumn, Math.Max (ev.X, 0));
+				var idx = TextModel.GetColFromX (r, leftColumn, Math.Max (ev.X, 0), TabWidth);
 				if (idx - leftColumn >= r.Count + RightOffset) {
 					currentColumn = Math.Max (r.Count - leftColumn + RightOffset, 0);
 				} else {

+ 127 - 0
UICatalog/Scenarios/SendKeys.cs

@@ -0,0 +1,127 @@
+using System;
+using Terminal.Gui;
+
+namespace UICatalog {
+	[ScenarioMetadata (Name: "SendKeys", Description: "SendKeys sample - Send key combinations.")]
+	[ScenarioCategory ("Input")]
+	class SendKeys : Scenario {
+		public override void Setup ()
+		{
+			var label = new Label ("Insert the text to send:") {
+				X = Pos.Center (),
+				Y = Pos.Center () - 6
+			};
+			Win.Add (label);
+
+			var txtInput = new TextField ("MockKeyPresses") {
+				X = Pos.Center (),
+				Y = Pos.Center () - 5,
+				Width = 20
+			};
+			Win.Add (txtInput);
+
+			var ckbShift = new CheckBox ("Shift") {
+				X = Pos.Center (),
+				Y = Pos.Center () - 4
+			};
+			Win.Add (ckbShift);
+
+			var ckbAlt = new CheckBox ("Alt") {
+				X = Pos.Center (),
+				Y = Pos.Center () - 3
+			};
+			Win.Add (ckbAlt);
+
+			var ckbControl = new CheckBox ("Control") {
+				X = Pos.Center (),
+				Y = Pos.Center () - 2
+			};
+			Win.Add (ckbControl);
+
+			label = new Label ("Result keys:") {
+				X = Pos.Center (),
+				Y = Pos.Center () + 1
+			};
+			Win.Add (label);
+
+			var txtResult = new TextField () {
+				X = Pos.Center (),
+				Y = Pos.Center () + 2,
+				Width = 20,
+			};
+			Win.Add (txtResult);
+
+			var rKeys = "";
+			var rControlKeys = "";
+			var IsShift = false;
+			var IsAlt = false;
+			var IsCtrl = false;
+
+			txtResult.KeyPress += (e) => {
+				rKeys += (char)e.KeyEvent.Key;
+				if (!IsShift && e.KeyEvent.IsShift) {
+					rControlKeys += " Shift ";
+					IsShift = true;
+				}
+				if (!IsAlt && e.KeyEvent.IsAlt) {
+					rControlKeys += " Alt ";
+					IsAlt = true;
+				}
+				if (!IsCtrl && e.KeyEvent.IsCtrl) {
+					rControlKeys += " Ctrl ";
+					IsCtrl = true;
+				}
+			};
+
+			var lblShippedKeys = new Label () {
+				X = Pos.Center (),
+				Y = Pos.Center () + 3,
+				AutoSize = true
+			};
+			Win.Add (lblShippedKeys);
+
+			var lblShippedControlKeys = new Label () {
+				X = Pos.Center (),
+				Y = Pos.Center () + 5,
+				AutoSize = true
+			};
+			Win.Add (lblShippedControlKeys);
+
+			var button = new Button ("Process keys") {
+				X = Pos.Center (),
+				Y = Pos.Center () + 7,
+				IsDefault = true
+			};
+			Win.Add (button);
+
+			void ProcessInput ()
+			{
+				rKeys = "";
+				rControlKeys = "";
+				txtResult.Text = "";
+				IsShift = false;
+				IsAlt = false;
+				IsCtrl = false;
+				txtResult.SetFocus ();
+				foreach (var r in txtInput.Text.ToString ()) {
+					var ck = char.IsLetter (r)
+						? (ConsoleKey)char.ToUpper (r) : (ConsoleKey)r;
+					Application.Driver.SendKeys (r, ck, ckbShift.Checked,
+						ckbAlt.Checked, ckbControl.Checked);
+				}
+				lblShippedKeys.Text = rKeys;
+				lblShippedControlKeys.Text = rControlKeys;
+				txtInput.SetFocus ();
+			}
+
+			button.Clicked += () => ProcessInput ();
+
+			Win.KeyPress += (e) => {
+				if (e.KeyEvent.Key == Key.Enter) {
+					ProcessInput ();
+					e.Handled = true;
+				}
+			};
+		}
+	}
+}

+ 7 - 0
UICatalog/Scenarios/Text.cs

@@ -54,6 +54,13 @@ namespace UICatalog {
 				labelMirroringTextView.Text = textView.Text;
 			};
 
+			var btnMultiline = new Button ("Toggle Multiline") {
+				X = Pos.Right (textView) + 1,
+				Y = Pos.Top (textView) + 1
+			};
+			btnMultiline.Clicked += () => textView.Multiline = !textView.Multiline;
+			Win.Add (btnMultiline);
+
 			// BUGBUG: 531 - TAB doesn't go to next control from HexView
 			var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (s))) {
 				X = 1,

+ 162 - 1
UnitTests/ConsoleDriverTests.cs

@@ -1,8 +1,10 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Terminal.Gui;
 using Xunit;
 
-// Alais Console to MockConsole so we don't accidentally use Console
+// Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui.ConsoleDrivers {
@@ -66,5 +68,164 @@ namespace Terminal.Gui.ConsoleDrivers {
 			Assert.Equal (ConsoleColor.Black, Console.BackgroundColor);
 			driver.End ();
 		}
+
+		[Fact]
+		public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+			var view = new View ();
+			var count = 0;
+			var wasKeyPressed = false;
+
+			view.KeyPress += (e) => {
+				wasKeyPressed = true;
+			};
+			top.Add (view);
+
+			Application.Iteration += () => {
+				count++;
+				if (count == 10) {
+					Application.RequestStop ();
+				}
+			};
+
+			Application.Run ();
+
+			Assert.False (wasKeyPressed);
+		}
+
+		[Fact]
+		public void FakeDriver_MockKeyPresses ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var text = "MockKeyPresses";
+			var mKeys = new Stack<ConsoleKeyInfo> ();
+			foreach (var r in text.Reverse ()) {
+				var ck = char.IsLetter (r) ? (ConsoleKey)char.ToUpper (r) : (ConsoleKey)r;
+				var cki = new ConsoleKeyInfo (r, ck, false, false, false);
+				mKeys.Push (cki);
+			}
+			FakeConsole.MockKeyPresses = mKeys;
+
+			var top = Application.Top;
+			var view = new View ();
+			var rText = "";
+			var idx = 0;
+
+			view.KeyPress += (e) => {
+				Assert.Equal (text [idx], (char)e.KeyEvent.Key);
+				rText += (char)e.KeyEvent.Key;
+				Assert.Equal (rText, text.Substring (0, idx + 1));
+				e.Handled = true;
+				idx++;
+			};
+			top.Add (view);
+
+			Application.Iteration += () => {
+				if (mKeys.Count == 0) {
+					Application.RequestStop ();
+				}
+			};
+
+			Application.Run ();
+
+			Assert.Equal ("MockKeyPresses", rText);
+		}
+
+		[Fact]
+		public void SendKeys_Test ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+			var view = new View ();
+			var shift = false; var alt = false; var control = false;
+			Key key = default;
+			Key lastKey = default;
+			List<Key> keyEnums = GetKeys ();
+			int i = 0;
+			int idxKey = 0;
+			var PushIterations = 0;
+			var PopIterations = 0;
+
+			List<Key> GetKeys ()
+			{
+				List<Key> keys = new List<Key> ();
+
+				foreach (Key k in Enum.GetValues (typeof (Key))) {
+					if ((uint)k <= 0xff) {
+						keys.Add (k);
+					} else if ((uint)k > 0xff) {
+						break;
+					}
+				}
+
+				return keys;
+			}
+
+			view.KeyPress += (e) => {
+				e.Handled = true;
+				PopIterations++;
+				var rMk = new KeyModifiers () {
+					Shift = e.KeyEvent.IsShift,
+					Alt = e.KeyEvent.IsAlt,
+					Ctrl = e.KeyEvent.IsCtrl
+				};
+				lastKey = ShortcutHelper.GetModifiersKey (new KeyEvent (e.KeyEvent.Key, rMk));
+				Assert.Equal (key, lastKey);
+			};
+			top.Add (view);
+
+			Application.Iteration += () => {
+				switch (i) {
+				case 0:
+					SendKeys ();
+					break;
+				case 1:
+					shift = true;
+					SendKeys ();
+					break;
+				case 2:
+					alt = true;
+					SendKeys ();
+					break;
+				case 3:
+					control = true;
+					SendKeys ();
+					break;
+				}
+				if (PushIterations == keyEnums.Count * 4) {
+					Application.RequestStop ();
+				}
+			};
+
+			void SendKeys ()
+			{
+				var k = keyEnums [idxKey];
+				var c = (char)k;
+				var ck = char.IsLetter (c) ? (ConsoleKey)char.ToUpper (c) : (ConsoleKey)c;
+				var mk = new KeyModifiers () {
+					Shift = shift,
+					Alt = alt,
+					Ctrl = control
+				};
+				key = ShortcutHelper.GetModifiersKey (new KeyEvent (k, mk));
+				Application.Driver.SendKeys (c, ck, shift, alt, control);
+				PushIterations++;
+				if (idxKey + 1 < keyEnums.Count) {
+					idxKey++;
+				} else {
+					idxKey = 0;
+					i++;
+				}
+			}
+
+			Application.Run ();
+
+			Assert.Equal (key, lastKey);
+		}
 	}
 }

+ 178 - 93
UnitTests/TextFormatterTests.cs

@@ -1861,8 +1861,156 @@ namespace Terminal.Gui.Core {
 			Assert.Equal ("e", wrappedLines [2].ToString ());
 			Assert.Equal ("n", wrappedLines [3].ToString ());
 			Assert.Equal (".", wrappedLines [^1].ToString ());
+		}
+
+		[Fact]
+		public void WordWrap_preserveTrailingSpaces ()
+		{
+			var text = ustring.Empty;
+			int maxWidth = 1;
+			int expectedClippedWidth = 1;
+
+			List<ustring> wrappedLines;
+
+			text = "A sentence has words.";
+			maxWidth = 14;
+			expectedClippedWidth = 14;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+			Assert.Equal (" words.", wrappedLines [1].ToString ());
+			Assert.True (wrappedLines.Count == 2);
+
+			maxWidth = 3;
+			expectedClippedWidth = 3;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A ", wrappedLines [0].ToString ());
+			Assert.Equal ("sen", wrappedLines [1].ToString ());
+			Assert.Equal ("ten", wrappedLines [2].ToString ());
+			Assert.Equal ("ce ", wrappedLines [3].ToString ());
+			Assert.Equal ("has", wrappedLines [4].ToString ());
+			Assert.Equal (" ", wrappedLines [5].ToString ());
+			Assert.Equal ("wor", wrappedLines [6].ToString ());
+			Assert.Equal ("ds.", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 8);
+
+			maxWidth = 2;
+			expectedClippedWidth = 2;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A ", wrappedLines [0].ToString ());
+			Assert.Equal ("se", wrappedLines [1].ToString ());
+			Assert.Equal ("nt", wrappedLines [2].ToString ());
+			Assert.Equal ("en", wrappedLines [3].ToString ());
+			Assert.Equal ("ce", wrappedLines [4].ToString ());
+			Assert.Equal (" ", wrappedLines [5].ToString ());
+			Assert.Equal ("ha", wrappedLines [6].ToString ());
+			Assert.Equal ("s ", wrappedLines [7].ToString ());
+			Assert.Equal ("wo", wrappedLines [8].ToString ());
+			Assert.Equal ("rd", wrappedLines [9].ToString ());
+			Assert.Equal ("s.", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 11);
+
+			maxWidth = 1;
+			expectedClippedWidth = 1;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A", wrappedLines [0].ToString ());
+			Assert.Equal (" ", wrappedLines [1].ToString ());
+			Assert.Equal ("s", wrappedLines [2].ToString ());
+			Assert.Equal ("e", wrappedLines [3].ToString ());
+			Assert.Equal ("n", wrappedLines [4].ToString ());
+			Assert.Equal ("t", wrappedLines [5].ToString ());
+			Assert.Equal ("e", wrappedLines [6].ToString ());
+			Assert.Equal ("n", wrappedLines [7].ToString ());
+			Assert.Equal ("c", wrappedLines [8].ToString ());
+			Assert.Equal ("e", wrappedLines [9].ToString ());
+			Assert.Equal (" ", wrappedLines [10].ToString ());
+			Assert.Equal (".", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == text.Length);
+		}
 
+		[Fact]
+		public void WordWrap_preserveTrailingSpaces_With_Tab ()
+		{
+			var text = ustring.Empty;
+			int maxWidth = 1;
+			int expectedClippedWidth = 1;
+
+			List<ustring> wrappedLines;
+
+			text = "A sentence\t\t\t has words.";
+			var tabWidth = 4;
+			maxWidth = 14;
+			expectedClippedWidth = 11;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true, tabWidth);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence\t", wrappedLines [0].ToString ());
+			Assert.Equal ("\t\t has ", wrappedLines [1].ToString ());
+			Assert.Equal ("words.", wrappedLines [2].ToString ());
+			Assert.True (wrappedLines.Count == 3);
+
+			maxWidth = 3;
+			expectedClippedWidth = 3;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true, tabWidth);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A ", wrappedLines [0].ToString ());
+			Assert.Equal ("sen", wrappedLines [1].ToString ());
+			Assert.Equal ("ten", wrappedLines [2].ToString ());
+			Assert.Equal ("ce", wrappedLines [3].ToString ());
+			Assert.Equal ("\t", wrappedLines [4].ToString ());
+			Assert.Equal ("\t", wrappedLines [5].ToString ());
+			Assert.Equal ("\t", wrappedLines [6].ToString ());
+			Assert.Equal (" ", wrappedLines [7].ToString ());
+			Assert.Equal ("has", wrappedLines [8].ToString ());
+			Assert.Equal (" ", wrappedLines [9].ToString ());
+			Assert.Equal ("wor", wrappedLines [10].ToString ());
+			Assert.Equal ("ds.", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 12);
+
+			maxWidth = 2;
+			expectedClippedWidth = 2;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true, tabWidth);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A ", wrappedLines [0].ToString ());
+			Assert.Equal ("se", wrappedLines [1].ToString ());
+			Assert.Equal ("nt", wrappedLines [2].ToString ());
+			Assert.Equal ("en", wrappedLines [3].ToString ());
+			Assert.Equal ("ce", wrappedLines [4].ToString ());
+			Assert.Equal ("\t", wrappedLines [5].ToString ());
+			Assert.Equal ("\t", wrappedLines [6].ToString ());
+			Assert.Equal ("\t", wrappedLines [7].ToString ());
+			Assert.Equal (" ", wrappedLines [8].ToString ());
+			Assert.Equal ("ha", wrappedLines [9].ToString ());
+			Assert.Equal ("s ", wrappedLines [10].ToString ());
+			Assert.Equal ("wo", wrappedLines [11].ToString ());
+			Assert.Equal ("rd", wrappedLines [12].ToString ());
+			Assert.Equal ("s.", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 14);
+
+			maxWidth = 1;
+			expectedClippedWidth = 1;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true, tabWidth);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A", wrappedLines [0].ToString ());
+			Assert.Equal (" ", wrappedLines [1].ToString ());
+			Assert.Equal ("s", wrappedLines [2].ToString ());
+			Assert.Equal ("e", wrappedLines [3].ToString ());
+			Assert.Equal ("n", wrappedLines [4].ToString ());
+			Assert.Equal ("t", wrappedLines [5].ToString ());
+			Assert.Equal ("e", wrappedLines [6].ToString ());
+			Assert.Equal ("n", wrappedLines [7].ToString ());
+			Assert.Equal ("c", wrappedLines [8].ToString ());
+			Assert.Equal ("e", wrappedLines [9].ToString ());
+			Assert.Equal ("\t", wrappedLines [10].ToString ());
+			Assert.Equal ("\t", wrappedLines [11].ToString ());
+			Assert.Equal ("\t", wrappedLines [12].ToString ());
+			Assert.Equal (" ", wrappedLines [13].ToString ());
+			Assert.Equal (".", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == text.Length);
 		}
+
 		[Fact]
 		public void ReplaceHotKeyWithTag ()
 		{
@@ -2307,115 +2455,52 @@ namespace Terminal.Gui.Core {
 		}
 
 		[Fact]
-		public void Format_WordWrap_Keep_End_Spaces ()
+		public void Format_WordWrap_preserveTrailingSpaces ()
 		{
 			ustring text = " A sentence has words. \n This is the second Line - 2. ";
 
 			// With preserveTrailingSpaces = false by default.
 			var list1 = TextFormatter.Format (text, 4, TextAlignment.Left, true);
 			ustring wrappedText1 = ustring.Empty;
-			var idx = 0;
+			Assert.Equal (" A", list1 [0].ToString ());
+			Assert.Equal ("sent", list1 [1].ToString ());
+			Assert.Equal ("ence", list1 [2].ToString ());
+			Assert.Equal ("has", list1 [3].ToString ());
+			Assert.Equal ("word", list1 [4].ToString ());
+			Assert.Equal ("s. ", list1 [5].ToString ());
+			Assert.Equal (" Thi", list1 [6].ToString ());
+			Assert.Equal ("s is", list1 [7].ToString ());
+			Assert.Equal ("the", list1 [8].ToString ());
+			Assert.Equal ("seco", list1 [9].ToString ());
+			Assert.Equal ("nd", list1 [10].ToString ());
+			Assert.Equal ("Line", list1 [11].ToString ());
+			Assert.Equal ("- 2.", list1 [^1].ToString ());
 			foreach (var txt in list1) {
 				wrappedText1 += txt;
-				switch (idx) {
-				case 0:
-					Assert.Equal (" A", txt);
-					break;
-				case 1:
-					Assert.Equal ("sent", txt);
-					break;
-				case 2:
-					Assert.Equal ("ence", txt);
-					break;
-				case 3:
-					Assert.Equal ("has", txt);
-					break;
-				case 4:
-					Assert.Equal ("word", txt);
-					break;
-				case 5:
-					Assert.Equal ("s. ", txt);
-					break;
-				case 6:
-					Assert.Equal (" Thi", txt);
-					break;
-				case 7:
-					Assert.Equal ("s is", txt);
-					break;
-				case 8:
-					Assert.Equal ("the", txt);
-					break;
-				case 9:
-					Assert.Equal ("seco", txt);
-					break;
-				case 10:
-					Assert.Equal ("nd", txt);
-					break;
-				case 11:
-					Assert.Equal ("Line", txt);
-					break;
-				case 12:
-					Assert.Equal ("- 2.", txt);
-					break;
-				}
-				idx++;
 			}
 			Assert.Equal (" Asentencehaswords.  This isthesecondLine- 2.", wrappedText1);
 
 			// With preserveTrailingSpaces = true.
 			var list2 = TextFormatter.Format (text, 4, TextAlignment.Left, true, true);
 			ustring wrappedText2 = ustring.Empty;
-			idx = 0;
+			Assert.Equal (" A ", list2 [0].ToString ());
+			Assert.Equal ("sent", list2 [1].ToString ());
+			Assert.Equal ("ence", list2 [2].ToString ());
+			Assert.Equal (" has", list2 [3].ToString ());
+			Assert.Equal (" ", list2 [4].ToString ());
+			Assert.Equal ("word", list2 [5].ToString ());
+			Assert.Equal ("s. ", list2 [6].ToString ());
+			Assert.Equal (" ", list2 [7].ToString ());
+			Assert.Equal ("This", list2 [8].ToString ());
+			Assert.Equal (" is ", list2 [9].ToString ());
+			Assert.Equal ("the ", list2 [10].ToString ());
+			Assert.Equal ("seco", list2 [11].ToString ());
+			Assert.Equal ("nd ", list2 [12].ToString ());
+			Assert.Equal ("Line", list2 [13].ToString ());
+			Assert.Equal (" - ", list2 [14].ToString ());
+			Assert.Equal ("2. ", list2 [^1].ToString ());
 			foreach (var txt in list2) {
 				wrappedText2 += txt;
-				switch (idx) {
-				case 0:
-					Assert.Equal (" A", txt);
-					break;
-				case 1:
-					Assert.Equal (" sen", txt);
-					break;
-				case 2:
-					Assert.Equal ("tenc", txt);
-					break;
-				case 3:
-					Assert.Equal ("e", txt);
-					break;
-				case 4:
-					Assert.Equal (" has", txt);
-					break;
-				case 5:
-					Assert.Equal (" wor", txt);
-					break;
-				case 6:
-					Assert.Equal ("ds. ", txt);
-					break;
-				case 7:
-					Assert.Equal (" Thi", txt);
-					break;
-				case 8:
-					Assert.Equal ("s is", txt);
-					break;
-				case 9:
-					Assert.Equal (" the", txt);
-					break;
-				case 10:
-					Assert.Equal (" sec", txt);
-					break;
-				case 11:
-					Assert.Equal ("ond", txt);
-					break;
-				case 12:
-					Assert.Equal (" Lin", txt);
-					break;
-				case 13:
-					Assert.Equal ("e -", txt);
-					break;
-				case 14:
-					Assert.Equal (" 2. ", txt);
-					break;
-				}
-				idx++;
 			}
 			Assert.Equal (" A sentence has words.  This is the second Line - 2. ", wrappedText2);
 		}

+ 401 - 25
UnitTests/TextViewTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using Xunit;
 
 namespace Terminal.Gui.Views {
@@ -900,7 +901,7 @@ namespace Terminal.Gui.Views {
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("", _textView.Text);
-					_textView.Paste ();
+					_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 					Assert.Equal ("This is the second line.", _textView.Text);
 					break;
 				default:
@@ -936,7 +937,7 @@ namespace Terminal.Gui.Views {
 					Assert.Equal (0, _textView.CursorPosition.X);
 					Assert.Equal (0, _textView.CursorPosition.Y);
 					Assert.Equal ("", _textView.Text);
-					_textView.Paste ();
+					_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 					Assert.Equal ("This is the first line.", _textView.Text);
 					break;
 				default:
@@ -1197,9 +1198,9 @@ namespace Terminal.Gui.Views {
 		{
 			_textView.SelectionStartColumn = 0;
 			_textView.SelectionStartRow = 0;
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("", _textView.SelectedText);
-			_textView.Cut ();
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Cut
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
@@ -1209,9 +1210,9 @@ namespace Terminal.Gui.Views {
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
 			_textView.CursorPosition = new Point (24, 0);
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
-			_textView.Cut ();
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Cut
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
@@ -1221,15 +1222,15 @@ namespace Terminal.Gui.Views {
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
 			_textView.CursorPosition = new Point (24, 0);
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
-			_textView.Cut ();
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Cut
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 		}
 
@@ -1239,25 +1240,25 @@ namespace Terminal.Gui.Views {
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
 			_textView.CursorPosition = new Point (24, 0);
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 			_textView.SelectionStartColumn = 0;
 			_textView.SelectionStartRow = 0;
 			Assert.True (_textView.Selecting);
 			_textView.Selecting = false;
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ("TAB to jump between texttext fields.", _textView.Text);
 			_textView.SelectionStartColumn = 24;
 			_textView.SelectionStartRow = 0;
-			_textView.Cut ();
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Cut
 			Assert.Equal ("", _textView.SelectedText);
 			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
 			_textView.SelectionStartColumn = 0;
 			_textView.SelectionStartRow = 0;
 			Assert.True (_textView.Selecting);
 			_textView.Selecting = false;
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ("TAB to jump between texttext fields.", _textView.Text);
 		}
 
@@ -1268,16 +1269,16 @@ namespace Terminal.Gui.Views {
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
 			_textView.CursorPosition = new Point (24, 0);
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
-			_textView.Cut (); // Selecting is set to false after Cut.
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Selecting is set to false after Cut.
 			Assert.Equal ("", _textView.SelectedText);
 			_textView.ReadOnly = false;
 			Assert.False (_textView.Selecting);
 			_textView.Selecting = true; // Needed to set Selecting to true.
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
-			_textView.Cut ();
+			_textView.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ())); // Cut
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
@@ -1287,9 +1288,9 @@ namespace Terminal.Gui.Views {
 			_textView.SelectionStartColumn = 20;
 			_textView.SelectionStartRow = 0;
 			_textView.CursorPosition = new Point (24, 0);
-			_textView.Copy ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
 			Assert.Equal ("text", _textView.SelectedText);
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ("", _textView.SelectedText);
 		}
 
@@ -1343,17 +1344,392 @@ namespace Terminal.Gui.Views {
 		{
 			_textView.Text = "This is the first line.\nThis is the second line.\n";
 			_textView.CursorPosition = new Point (0, _textView.Lines - 1);
-			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ()));
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}{Environment.NewLine}", _textView.Text);
 			_textView.CursorPosition = new Point (3, 1);
-			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ()));
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ())); // Copy
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the second line.{Environment.NewLine}{Environment.NewLine}", _textView.Text);
 			Assert.Equal (new Point (3, 2), _textView.CursorPosition);
-			_textView.Paste ();
+			_textView.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ())); // Paste
 			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the second line.{Environment.NewLine}{Environment.NewLine}", _textView.Text);
 			Assert.Equal (new Point (3, 3), _textView.CursorPosition);
 		}
+
+		[Fact]
+		public void TabWidth_Setting_To_Zero_Changes_AllowsTab_To_False_If_True ()
+		{
+			Assert.Equal (4, _textView.TabWidth);
+			Assert.True (_textView.AllowsTab);
+			Assert.True (_textView.AllowsReturn);
+			Assert.True (_textView.Multiline);
+			_textView.TabWidth = -1;
+			Assert.Equal (0, _textView.TabWidth);
+			Assert.False (_textView.AllowsTab);
+			Assert.False (_textView.AllowsReturn);
+			Assert.False (_textView.Multiline);
+			_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+			_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+			Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+		}
+
+		[Fact]
+		public void AllowsTab_Setting_To_True_Changes_TabWidth_To_Default_If_It_Is_Zero ()
+		{
+			_textView.TabWidth = 0;
+			Assert.Equal (0, _textView.TabWidth);
+			Assert.False (_textView.AllowsTab);
+			Assert.False (_textView.AllowsReturn);
+			Assert.False (_textView.Multiline);
+			_textView.AllowsTab = true;
+			Assert.True (_textView.AllowsTab);
+			Assert.Equal (4, _textView.TabWidth);
+			Assert.True (_textView.AllowsReturn);
+			Assert.True (_textView.Multiline);
+		}
+
+		[Fact]
+		public void AllowsReturn_Setting_To_True_Changes_Multiline_To_True_If_It_Is_False ()
+		{
+			Assert.True (_textView.AllowsReturn);
+			Assert.True (_textView.Multiline);
+			Assert.Equal (4, _textView.TabWidth);
+			Assert.True (_textView.AllowsTab);
+			_textView.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
+			Assert.Equal (Environment.NewLine +
+				"TAB to jump between text fields.", _textView.Text);
+
+			_textView.AllowsReturn = false;
+			Assert.False (_textView.AllowsReturn);
+			Assert.False (_textView.Multiline);
+			Assert.Equal (0, _textView.TabWidth);
+			Assert.False (_textView.AllowsTab);
+			_textView.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
+			Assert.Equal (Environment.NewLine +
+				"TAB to jump between text fields.", _textView.Text);
+		}
+
+		[Fact]
+		public void Multiline_Setting_Changes_AllowsReturn_And_AllowsTab_And_Height ()
+		{
+			Assert.True (_textView.Multiline);
+			Assert.True (_textView.AllowsReturn);
+			Assert.Equal (4, _textView.TabWidth);
+			Assert.True (_textView.AllowsTab);
+			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
+			Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ());
+
+			_textView.Multiline = false;
+			Assert.False (_textView.Multiline);
+			Assert.False (_textView.AllowsReturn);
+			Assert.Equal (0, _textView.TabWidth);
+			Assert.False (_textView.AllowsTab);
+			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
+			Assert.Equal ("Dim.Absolute(1)", _textView.Height.ToString ());
+
+			_textView.Multiline = true;
+			Assert.True (_textView.Multiline);
+			Assert.True (_textView.AllowsReturn);
+			Assert.Equal (4, _textView.TabWidth);
+			Assert.True (_textView.AllowsTab);
+			Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ());
+			Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ());
+		}
+
+		[Fact]
+		public void Tab_Test_Follow_By_BackTab ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				_textView.Text = "";
+				var col = 0;
+				var leftCol = 0;
+				var tabWidth = _textView.TabWidth;
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		[Fact]
+		public void BackTab_Test_Follow_By_Tab ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				_textView.Text = "";
+				for (int i = 0; i < 100; i++) {
+					_textView.Text += "\t";
+				}
+				var col = 100;
+				var tabWidth = _textView.TabWidth;
+				var leftCol = _textView.LeftColumn;
+				_textView.MoveEnd ();
+				Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+				leftCol = GetLeftCol (leftCol);
+				Assert.Equal (leftCol, _textView.LeftColumn);
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		[Fact]
+		public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				_textView.Text = "";
+				var col = 0;
+				var leftCol = 0;
+				var tabWidth = _textView.TabWidth;
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		[Fact]
+		public void Tab_Test_Follow_By_BackTab_With_Text ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				var col = 0;
+				var leftCol = 0;
+				Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+				Assert.Equal (leftCol, _textView.LeftColumn);
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		[Fact]
+		public void Tab_Test_Follow_By_Home_And_Then_Follow_By_End_And_Then_Follow_By_BackTab_With_Text ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				var col = 0;
+				var leftCol = 0;
+				Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+				Assert.Equal (leftCol, _textView.LeftColumn);
+				Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+				Assert.Equal (32, _textView.Text.Length);
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				_textView.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()));
+				col = 0;
+				Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+				leftCol = 0;
+				Assert.Equal (leftCol, _textView.LeftColumn);
+
+				_textView.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()));
+				col = _textView.Text.Length;
+				Assert.Equal (132, _textView.Text.Length);
+				Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+				leftCol = GetLeftCol (leftCol);
+				Assert.Equal (leftCol, _textView.LeftColumn);
+				var txt = _textView.Text;
+				while (col - 1 > 0 && txt [col - 1] != '\t') {
+					col--;
+				}
+				_textView.CursorPosition = new Point (col, 0);
+				leftCol = GetLeftCol (leftCol);
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+				Assert.Equal (32, _textView.Text.Length);
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		[Fact]
+		public void Tab_Test_Follow_By_CursorLeft_And_Then_Follow_By_CursorRight_With_Text ()
+		{
+			Application.Top.Add (_textView);
+
+			Application.Iteration += () => {
+				var width = _textView.Bounds.Width - 1;
+				Assert.Equal (30, width + 1);
+				Assert.Equal (10, _textView.Height);
+				Assert.Equal ("TAB to jump between text fields.", _textView.Text);
+				var col = 0;
+				var leftCol = 0;
+				var tabWidth = _textView.TabWidth;
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				Assert.Equal (132, _textView.Text.Length);
+				while (col > 0) {
+					col--;
+					_textView.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+				while (col < 100) {
+					col++;
+					_textView.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()));
+					Assert.Equal (new Point (col, 0), _textView.CursorPosition);
+					leftCol = GetLeftCol (leftCol);
+					Assert.Equal (leftCol, _textView.LeftColumn);
+				}
+
+				Application.Top.Remove (_textView);
+				Application.RequestStop ();
+			};
+
+			Application.Run ();
+		}
+
+		private int GetLeftCol (int start)
+		{
+			var lines = _textView.Text.Split (Environment.NewLine);
+			if (lines == null || lines.Length == 0) {
+				return 0;
+			}
+			if (start == _textView.LeftColumn) {
+				return start;
+			}
+			if (_textView.LeftColumn == _textView.CurrentColumn) {
+				return _textView.CurrentColumn;
+			}
+			var cCol = _textView.CurrentColumn;
+			var line = lines [_textView.CurrentRow];
+			var lCount = cCol > line.Length - 1 ? line.Length - 1 : cCol;
+			var width = _textView.Frame.Width;
+			var tabWidth = _textView.TabWidth;
+			var sumLength = 0;
+			var col = 0;
+
+			for (int i = lCount; i >= 0; i--) {
+				var r = line [i];
+				sumLength += Rune.ColumnWidth (r);
+				if (r == '\t') {
+					sumLength += tabWidth + 1;
+				}
+				if (sumLength > width) {
+					if (cCol == line.Length) {
+						col++;
+					}
+					break;
+				} else if (cCol < line.Length && col > 0 && start < cCol && col == start) {
+					break;
+				}
+				col = i;
+			}
+
+			return col;
+		}
 	}
 }