Bladeren bron

Merge branch 'develop' into csv-editor-fix

Tig 2 jaren geleden
bovenliggende
commit
cab89417b8

+ 236 - 192
Terminal.Gui/Views/TextView.cs

@@ -1,28 +1,4 @@
-//
 // TextView.cs: multi-line text editing
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-// 
-// TODO:
-// In ReadOnly mode backspace/space behave like pageup/pagedown
-// Attributed text on spans
-// Replace insertion with Insert method
-// String accumulation (Control-k, control-k is not preserving the last new line, see StringToRunes
-// Alt-D, Alt-Backspace
-// API to set the cursor position
-// API to scroll to a particular place
-// keybindings to go to top/bottom
-// public API to insert, remove ranges
-// Add word forward/word backwards commands
-// Save buffer API
-// Mouse
-//
-// Desirable:
-//   Move all the text manipulation into the TextModel
-
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -33,6 +9,7 @@ using System.Text;
 using System.Threading;
 using NStack;
 using Terminal.Gui.Resources;
+using static Terminal.Gui.Graphs.PathAnnotation;
 using Rune = System.Rune;
 
 namespace Terminal.Gui {
@@ -742,6 +719,7 @@ namespace Terminal.Gui {
 			historyTextItems.Clear ();
 			idxHistoryText = -1;
 			originalText = text;
+			OnChangeText (null);
 		}
 
 		public bool IsDirty (ustring text)
@@ -1037,120 +1015,119 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	///   Multi-line text editing <see cref="View"/>
+	///  Multi-line text editing <see cref="View"/>.
 	/// </summary>
 	/// <remarks>
-	///   <para>
-	///     <see cref="TextView"/> provides a multi-line text editor. Users interact
-	///     with it with the standard Emacs commands for movement or the arrow
-	///     keys. 
-	///   </para> 
-	///   <list type="table"> 
-	///     <listheader>
-	///       <term>Shortcut</term>
-	///       <description>Action performed</description>
-	///     </listheader>
-	///     <item>
-	///        <term>Left cursor, Control-b</term>
-	///        <description>
-	///          Moves the editing point left.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Right cursor, Control-f</term>
-	///        <description>
-	///          Moves the editing point right.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-b</term>
-	///        <description>
-	///          Moves one word back.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-f</term>
-	///        <description>
-	///          Moves one word forward.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Up cursor, Control-p</term>
-	///        <description>
-	///          Moves the editing point one line up.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Down cursor, Control-n</term>
-	///        <description>
-	///          Moves the editing point one line down
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Home key, Control-a</term>
-	///        <description>
-	///          Moves the cursor to the beginning of the line.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>End key, Control-e</term>
-	///        <description>
-	///          Moves the cursor to the end of the line.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-Home</term>
-	///        <description>
-	///          Scrolls to the first line and moves the cursor there.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-End</term>
-	///        <description>
-	///          Scrolls to the last line and moves the cursor there.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Delete, Control-d</term>
-	///        <description>
-	///          Deletes the character in front of the cursor.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Backspace</term>
-	///        <description>
-	///          Deletes the character behind the cursor.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-k</term>
-	///        <description>
-	///          Deletes the text until the end of the line and replaces the kill buffer
-	///          with the deleted text.   You can paste this text in a different place by
-	///          using Control-y.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-y</term>
-	///        <description>
-	///           Pastes the content of the kill ring into the current position.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Alt-d</term>
-	///        <description>
-	///           Deletes the word above the cursor and adds it to the kill ring.  You 
-	///           can paste the contents of the kill ring with Control-y.
-	///        </description>
-	///     </item>
-	///     <item>
-	///        <term>Control-q</term>
-	///        <description>
-	///          Quotes the next input character, to prevent the normal processing of
-	///          key handling to take place.
-	///        </description>
-	///     </item>
-	///   </list>
+	///  <para>
+	///   <see cref="TextView"/> provides a multi-line text editor. Users interact
+	///   with it with the standard Windows, Mac, and Linux (Emacs) commands. 
+	///  </para> 
+	///  <list type="table"> 
+	///   <listheader>
+	///    <term>Shortcut</term>
+	///    <description>Action performed</description>
+	///   </listheader>
+	///   <item>
+	///    <term>Left cursor, Control-b</term>
+	///    <description>
+	///     Moves the editing point left.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Right cursor, Control-f</term>
+	///    <description>
+	///     Moves the editing point right.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-b</term>
+	///    <description>
+	///     Moves one word back.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-f</term>
+	///    <description>
+	///     Moves one word forward.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Up cursor, Control-p</term>
+	///    <description>
+	///     Moves the editing point one line up.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Down cursor, Control-n</term>
+	///    <description>
+	///     Moves the editing point one line down
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Home key, Control-a</term>
+	///    <description>
+	///     Moves the cursor to the beginning of the line.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>End key, Control-e</term>
+	///    <description>
+	///     Moves the cursor to the end of the line.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-Home</term>
+	///    <description>
+	///     Scrolls to the first line and moves the cursor there.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-End</term>
+	///    <description>
+	///     Scrolls to the last line and moves the cursor there.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Delete, Control-d</term>
+	///    <description>
+	///     Deletes the character in front of the cursor.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Backspace</term>
+	///    <description>
+	///     Deletes the character behind the cursor.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-k</term>
+	///    <description>
+	///     Deletes the text until the end of the line and replaces the kill buffer
+	///     with the deleted text. You can paste this text in a different place by
+	///     using Control-y.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-y</term>
+	///    <description>
+	///      Pastes the content of the kill ring into the current position.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Alt-d</term>
+	///    <description>
+	///      Deletes the word above the cursor and adds it to the kill ring. You 
+	///      can paste the contents of the kill ring with Control-y.
+	///    </description>
+	///   </item>
+	///   <item>
+	///    <term>Control-q</term>
+	///    <description>
+	///     Quotes the next input character, to prevent the normal processing of
+	///     key handling to take place.
+	///    </description>
+	///   </item>
+	///  </list>
 	/// </remarks>
 	public class TextView : View {
 		TextModel model = new TextModel ();
@@ -1172,10 +1149,24 @@ namespace Terminal.Gui {
 		CultureInfo currentCulture;
 
 		/// <summary>
-		/// Raised when the <see cref="Text"/> of the <see cref="TextView"/> changes.
+		/// Raised when the <see cref="Text"/> property of the <see cref="TextView"/> changes.
 		/// </summary>
+		/// <remarks>
+		/// The <see cref="Text"/> property of <see cref="TextView"/> only changes when it is explicitly
+		/// set, not as the user types. To be notified as the user changes the contents of the TextView
+		/// see <see cref="IsDirty"/>.
+		/// </remarks>
 		public event Action TextChanged;
 
+		/// <summary>
+		///  Raised when the contents of the <see cref="TextView"/> are changed. 
+		/// </summary>
+		/// <remarks>
+		/// Unlike the <see cref="TextChanged"/> event, this event is raised whenever the user types or
+		/// otherwise changes the contents of the <see cref="TextView"/>.
+		/// </remarks>
+		public Action<ContentsChangedEventArgs> ContentsChanged;
+
 		/// <summary>
 		/// Invoked with the unwrapped <see cref="CursorPosition"/>.
 		/// </summary>
@@ -1183,22 +1174,12 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Provides autocomplete context menu based on suggestions at the current cursor
-		/// position.  Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
+		/// position. Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
 		/// </summary>
 		public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
 
-#if false
-		/// <summary>
-		///   Changed event, raised when the text has clicked.
-		/// </summary>
-		/// <remarks>
-		///   Client code can hook up to this event, it is
-		///   raised when the text in the entry changes.
-		/// </remarks>
-		public Action Changed;
-#endif
 		/// <summary>
-		///   Initializes a <see cref="TextView"/> on the specified area, with absolute position and size.
+		///  Initializes a <see cref="TextView"/> on the specified area, with absolute position and size.
 		/// </summary>
 		/// <remarks>
 		/// </remarks>
@@ -1208,8 +1189,8 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///   Initializes a <see cref="TextView"/> on the specified area, 
-		///   with dimensions controlled with the X, Y, Width and Height properties.
+		///  Initializes a <see cref="TextView"/> on the specified area, 
+		///  with dimensions controlled with the X, Y, Width and Height properties.
 		/// </summary>
 		public TextView () : base ()
 		{
@@ -1404,48 +1385,56 @@ namespace Terminal.Gui {
 
 		private void Model_LinesLoaded ()
 		{
-			historyText.Clear (Text);
+			// This call is not needed. Model_LinesLoaded gets invoked when
+			// model.LoadString (value) is called. LoadString is called from one place
+			// (Text.set) and historyText.Clear() is called immediately after.
+			// If this call happens, HistoryText_ChangeText will get called multiple times
+			// when Text is set, which is wrong.
+			//historyText.Clear (Text);
 		}
 
 		private void HistoryText_ChangeText (HistoryText.HistoryTextItem obj)
 		{
 			SetWrapModel ();
 
-			var startLine = obj.CursorPosition.Y;
+			if (obj != null) {
+				var startLine = obj.CursorPosition.Y;
 
-			if (obj.RemovedOnAdded != null) {
-				int offset;
-				if (obj.IsUndoing) {
-					offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
-				} else {
-					offset = obj.RemovedOnAdded.Lines.Count - 1;
-				}
-				for (int i = 0; i < offset; i++) {
-					if (Lines > obj.RemovedOnAdded.CursorPosition.Y) {
-						model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
+				if (obj.RemovedOnAdded != null) {
+					int offset;
+					if (obj.IsUndoing) {
+						offset = Math.Max (obj.RemovedOnAdded.Lines.Count - obj.Lines.Count, 1);
 					} else {
-						break;
+						offset = obj.RemovedOnAdded.Lines.Count - 1;
+					}
+					for (int i = 0; i < offset; i++) {
+						if (Lines > obj.RemovedOnAdded.CursorPosition.Y) {
+							model.RemoveLine (obj.RemovedOnAdded.CursorPosition.Y);
+						} else {
+							break;
+						}
 					}
 				}
-			}
 
-			for (int i = 0; i < obj.Lines.Count; i++) {
-				if (i == 0) {
-					model.ReplaceLine (startLine, obj.Lines [i]);
-				} else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed)
-						|| !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) {
-					model.AddLine (startLine, obj.Lines [i]);
-				} else if (Lines > obj.CursorPosition.Y + 1) {
-					model.RemoveLine (obj.CursorPosition.Y + 1);
+				for (int i = 0; i < obj.Lines.Count; i++) {
+					if (i == 0) {
+						model.ReplaceLine (startLine, obj.Lines [i]);
+					} else if ((obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Removed)
+							|| !obj.IsUndoing && obj.LineStatus == HistoryText.LineStatus.Added) {
+						model.AddLine (startLine, obj.Lines [i]);
+					} else if (Lines > obj.CursorPosition.Y + 1) {
+						model.RemoveLine (obj.CursorPosition.Y + 1);
+					}
+					startLine++;
 				}
-				startLine++;
-			}
 
-			CursorPosition = obj.FinalCursorPosition;
+				CursorPosition = obj.FinalCursorPosition;
+			}
 
 			UpdateWrapModel ();
-
+			
 			Adjust ();
+			OnContentsChanged ();
 		}
 
 		void TextView_Initialized (object sender, EventArgs e)
@@ -1454,6 +1443,7 @@ namespace Terminal.Gui {
 
 			Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged;
 			Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged;
+			OnContentsChanged ();
 		}
 
 		void Top_AlternateBackwardKeyChanged (Key obj)
@@ -1480,9 +1470,11 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///   Sets or gets the text in the <see cref="TextView"/>.
+		///  Sets or gets the text in the <see cref="TextView"/>.
 		/// </summary>
 		/// <remarks>
+		/// The <see cref="TextChanged"/> event is fired whenever this property is set. Note, however,
+		/// that Text is not set by <see cref="TextView"/> as the user types.
 		/// </remarks>
 		public override ustring Text {
 			get {
@@ -1559,12 +1551,12 @@ namespace Terminal.Gui {
 		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height, TabWidth);
 
 		/// <summary>
-		/// Gets the  number of lines.
+		/// Gets the number of lines.
 		/// </summary>
 		public int Lines => model.Count;
 
 		/// <summary>
-		///    Sets or gets the current cursor position.
+		///  Sets or gets the current cursor position.
 		/// </summary>
 		public Point CursorPosition {
 			get => new Point (currentColumn, currentRow);
@@ -1828,7 +1820,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Loads the contents of the file into the  <see cref="TextView"/>.
+		/// Loads the contents of the file into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if file was loaded, <c>false</c> otherwise.</returns>
 		/// <param name="path">Path to the file to load.</param>
@@ -1845,12 +1837,13 @@ namespace Terminal.Gui {
 				UpdateWrapModel ();
 				SetNeedsDisplay ();
 				Adjust ();
+				OnContentsChanged ();
 			}
 			return res;
 		}
 
 		/// <summary>
-		/// Loads the contents of the stream into the  <see cref="TextView"/>.
+		/// Loads the contents of the stream into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if stream was loaded, <c>false</c> otherwise.</returns>
 		/// <param name="stream">Stream to load the contents from.</param>
@@ -1859,10 +1852,11 @@ namespace Terminal.Gui {
 			model.LoadStream (stream);
 			ResetPosition ();
 			SetNeedsDisplay ();
+			OnContentsChanged ();
 		}
 
 		/// <summary>
-		/// Closes the contents of the stream into the  <see cref="TextView"/>.
+		/// Closes the contents of the stream into the <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if stream was closed, <c>false</c> otherwise.</returns>
 		public bool CloseFile ()
@@ -1874,7 +1868,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		///    Gets the current cursor row.
+		///  Gets the current cursor row.
 		/// </summary>
 		public int CurrentRow => currentRow;
 
@@ -1885,7 +1879,7 @@ namespace Terminal.Gui {
 		public int CurrentColumn => currentColumn;
 
 		/// <summary>
-		///   Positions the cursor on the current row and column
+		///  Positions the cursor on the current row and column
 		/// </summary>
 		public override void PositionCursor ()
 		{
@@ -1936,7 +1930,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Sets the driver to the default color for the control where no text is being rendered.  Defaults to <see cref="ColorScheme.Normal"/>.
+		/// Sets the driver to the default color for the control where no text is being rendered. Defaults to <see cref="ColorScheme.Normal"/>.
 		/// </summary>
 		protected virtual void SetNormalColor ()
 		{
@@ -1945,7 +1939,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Normal"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1957,7 +1951,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Focus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1969,7 +1963,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.Focus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -1987,7 +1981,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Sets the <see cref="View.Driver"/> to an appropriate color for rendering the given <paramref name="idx"/> of the
-		/// current <paramref name="line"/>.  Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
+		/// current <paramref name="line"/>. Override to provide custom coloring by calling <see cref="ConsoleDriver.SetAttribute(Attribute)"/>
 		/// Defaults to <see cref="ColorScheme.HotFocus"/>.
 		/// </summary>
 		/// <param name="line"></param>
@@ -2000,7 +1994,7 @@ namespace Terminal.Gui {
 		bool isReadOnly = false;
 
 		/// <summary>
-		/// Gets or sets whether the  <see cref="TextView"/> is in read-only mode or not
+		/// Gets or sets whether the <see cref="TextView"/> is in read-only mode or not
 		/// </summary>
 		/// <value>Boolean value(Default false)</value>
 		public bool ReadOnly {
@@ -2504,6 +2498,12 @@ namespace Terminal.Gui {
 
 				InsertText (new KeyEvent () { Key = key });
 			}
+
+			if (NeedDisplay.IsEmpty) {
+				PositionCursor ();
+			} else {
+				Adjust ();
+			}
 		}
 
 		void Insert (Rune rune)
@@ -2521,6 +2521,7 @@ namespace Terminal.Gui {
 			if (!wrapNeeded) {
 				SetNeedsDisplay (new Rect (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0)));
 			}
+
 		}
 
 		ustring StringFromRunes (List<Rune> runes)
@@ -2584,6 +2585,8 @@ namespace Terminal.Gui {
 
 				UpdateWrapModel ();
 
+				OnContentsChanged ();
+
 				return;
 			}
 
@@ -2690,6 +2693,42 @@ namespace Terminal.Gui {
 			OnUnwrappedCursorPosition ();
 		}
 
+		/// <summary>
+		/// Event arguments for events for when the contents of the TextView change. E.g. the <see cref="ContentsChanged"/> event.
+		/// </summary>
+		public class ContentsChangedEventArgs : EventArgs {
+			/// <summary>
+			/// Creates a new <see cref="ContentsChanged"/> instance.
+			/// </summary>
+			/// <param name="currentRow">Contains the row where the change occurred.</param>
+			/// <param name="currentColumn">Contains the column where the change occured.</param>
+			public ContentsChangedEventArgs (int currentRow, int currentColumn)
+			{
+				Row = currentRow;
+				Col = currentColumn;
+			}
+
+			/// <summary>
+			/// 
+			/// Contains the row where the change occurred.
+			/// </summary>
+			public int Row { get; private set; }
+
+			/// <summary>
+			/// Contains the column where the change occurred.
+			/// </summary>
+			public int Col { get; private set; }
+		}
+
+		/// <summary>
+		/// Called when the contents of the TextView change. E.g. when the user types text or deletes text. Raises
+		/// the <see cref="ContentsChanged"/> event.
+		/// </summary>
+		public virtual void OnContentsChanged ()
+		{
+			ContentsChanged?.Invoke (new ContentsChangedEventArgs (CurrentRow, CurrentColumn));
+		}
+
 		(int width, int height) OffSetBackground ()
 		{
 			int w = 0;
@@ -2708,7 +2747,7 @@ namespace Terminal.Gui {
 		/// will scroll the <see cref="TextView"/> to display the specified column at the left if <paramref name="isRow"/> is false.
 		/// </summary>
 		/// <param name="idx">Row that should be displayed at the top or Column that should be displayed at the left,
-		///  if the value is negative it will be reset to zero</param>
+		/// if the value is negative it will be reset to zero</param>
 		/// <param name="isRow">If true (default) the <paramref name="idx"/> is a row, column otherwise.</param>
 		public void ScrollTo (int idx, bool isRow = true)
 		{
@@ -3178,6 +3217,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 
 			DoNeededAction ();
+			OnContentsChanged ();
 			return true;
 		}
 
@@ -3674,6 +3714,7 @@ namespace Terminal.Gui {
 				HistoryText.LineStatus.Replaced);
 
 			UpdateWrapModel ();
+			OnContentsChanged ();
 
 			return true;
 		}
@@ -3883,6 +3924,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 			selecting = false;
 			DoNeededAction ();
+			OnContentsChanged ();
 		}
 
 		/// <summary>
@@ -3913,6 +3955,7 @@ namespace Terminal.Gui {
 
 				historyText.Add (new List<List<Rune>> () { new List<Rune> (GetCurrentLine ()) }, CursorPosition,
 					HistoryText.LineStatus.Replaced);
+				OnContentsChanged ();
 			} else {
 				if (selecting) {
 					ClearRegion ();
@@ -4423,6 +4466,7 @@ namespace Terminal.Gui {
 		}
 	}
 
+
 	/// <summary>
 	/// Renders an overlay on another view at a given point that allows selecting
 	/// from a range of 'autocomplete' options.

+ 1 - 1
Terminal.Gui/Windows/FileDialog.cs

@@ -41,7 +41,7 @@ namespace Terminal.Gui {
 			if (allowedFileTypes == null)
 				return true;
 			foreach (var ft in allowedFileTypes)
-				if (fsi.Name.EndsWith (ft) || ft == ".*")
+				if (fsi.Name.EndsWith (ft, StringComparison.InvariantCultureIgnoreCase) || ft == ".*")
 					return true;
 			return false;
 		}

+ 353 - 45
UICatalog/Scenarios/CharacterMap.cs

@@ -1,12 +1,14 @@
 #define DRAW_CONTENT
 //#define BASE_DRAW_CONTENT
 
+using Microsoft.VisualBasic;
 using NStack;
 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using Terminal.Gui;
+using Terminal.Gui.Resources;
 using Rune = System.Rune;
 
 namespace UICatalog.Scenarios {
@@ -31,51 +33,67 @@ namespace UICatalog.Scenarios {
 				Height = Dim.Fill (),
 			};
 
-			var radioItems = new (ustring radioLabel, int start, int end) [] {
-				CreateRadio("ASCII Control Characters", 0x00, 0x1F),
-				CreateRadio("C0 Control Characters", 0x80, 0x9f),
-				CreateRadio("Hangul Jamo", 0x1100, 0x11ff),	// This is where wide chars tend to start
-				CreateRadio("Currency Symbols", 0x20A0, 0x20CF),
-				CreateRadio("Letter-like Symbols", 0x2100, 0x214F),
-				CreateRadio("Arrows", 0x2190, 0x21ff),
-				CreateRadio("Mathematical symbols", 0x2200, 0x22ff),
-				CreateRadio("Miscellaneous Technical", 0x2300, 0x23ff),
-				CreateRadio("Box Drawing & Geometric Shapes", 0x2500, 0x25ff),
-				CreateRadio("Miscellaneous Symbols", 0x2600, 0x26ff),
-				CreateRadio("Dingbats", 0x2700, 0x27ff),
-				CreateRadio("Braille", 0x2800, 0x28ff),
-				CreateRadio("Miscellaneous Symbols & Arrows", 0x2b00, 0x2bff),
-				CreateRadio("Alphabetic Pres. Forms", 0xFB00, 0xFb4f),
-				CreateRadio("Cuneiform Num. and Punct.", 0x12400, 0x1240f),
-				CreateRadio("Chess Symbols", 0x1FA00, 0x1FA0f),
-				CreateRadio("End", CharMap.MaxCodePointVal - 16, CharMap.MaxCodePointVal),
+			var jumpLabel = new Label ("Jump To Glyph:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) };
+			Win.Add (jumpLabel);
+			var jumpEdit = new TextField () { X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, };
+			Win.Add (jumpEdit);
+			var unicodeLabel = new Label ("") { X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap) };
+			Win.Add (unicodeLabel);
+			jumpEdit.TextChanged += (s) => {
+				uint result = 0;
+				if (jumpEdit.Text.Length == 0) return;
+				try {
+					result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 10);
+				} catch (OverflowException) {
+					unicodeLabel.Text = $"Invalid (overflow)";
+					return;
+				} catch (FormatException) {
+					try {
+						result = Convert.ToUInt32 (jumpEdit.Text.ToString (), 16);
+					} catch (OverflowException) {
+						unicodeLabel.Text = $"Invalid (overflow)";
+						return;
+					} catch (FormatException) {
+						unicodeLabel.Text = $"Invalid (can't parse)";
+						return;
+					}
+				}
+				unicodeLabel.Text = $"U+{result:x4}";
+				_charMap.SelectedGlyph = result;
 			};
-			(ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end)
+
+			var radioItems = new (ustring radioLabel, uint start, uint end) [UnicodeRange.Ranges.Count];
+
+			for (var i = 0; i < UnicodeRange.Ranges.Count; i++) {
+				var range = UnicodeRange.Ranges [i];
+				radioItems [i] = CreateRadio (range.Category, range.Start, range.End);
+			}
+			(ustring radioLabel, uint start, uint end) CreateRadio (ustring title, uint start, uint end)
 			{
 				return ($"{title} (U+{start:x5}-{end:x5})", start, end);
 			}
 
 			Win.Add (_charMap);
-			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) };
+			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Bottom (jumpLabel) + 1 };
 			Win.Add (label);
 
-			var jumpList = new RadioGroup (radioItems.Select (t => t.radioLabel).ToArray ()) {
-				X = Pos.X (label),
+			var jumpList = new ListView (radioItems.Select (t => t.radioLabel).ToArray ()) {
+				X = Pos.X (label) + 1,
 				Y = Pos.Bottom (label),
-				Width = radioItems.Max (r => r.radioLabel.Length) + 3,
-				SelectedItem = 8
+				Width = radioItems.Max (r => r.radioLabel.Length) + 2,
+				Height = Dim.Fill(1),
+				SelectedItem = 0
 			};
 			jumpList.SelectedItemChanged += (args) => {
-				_charMap.Start = radioItems [args.SelectedItem].start;
+				_charMap.StartGlyph = radioItems [jumpList.SelectedItem].start;
 			};
 
 			Win.Add (jumpList);
 
-			jumpList.Refresh ();
-			jumpList.SetFocus ();
+			//jumpList.Refresh ();
+			_charMap.SetFocus ();
 
 			_charMap.Width = Dim.Fill () - jumpList.Width;
-
 		}
 	}
 
@@ -85,23 +103,50 @@ namespace UICatalog.Scenarios {
 		/// Specifies the starting offset for the character map. The default is 0x2500 
 		/// which is the Box Drawing characters.
 		/// </summary>
-		public int Start {
+		public uint StartGlyph {
 			get => _start;
 			set {
 				_start = value;
-				ContentOffset = new Point (0, _start / 16);
+				_selected = value;
+				ContentOffset = new Point (0, (int)(_start / 16));
 				SetNeedsDisplay ();
 			}
 		}
 
-		int _start = 0x2500;
+		/// <summary>
+		/// Specifies the starting offset for the character map. The default is 0x2500 
+		/// which is the Box Drawing characters.
+		/// </summary>
+		public uint SelectedGlyph {
+			get => _selected;
+			set {
+				_selected = value;
+				int row = (int)_selected / 16;
+				int height = (Bounds.Height / ROW_HEIGHT) - 1;
+				if (row + ContentOffset.Y < 0) {
+					// Moving up.
+					ContentOffset = new Point (0, row);
+				} else if (row + ContentOffset.Y >= height) {
+					// Moving down.
+					ContentOffset = new Point (0, Math.Min (row, (row - height) + 1));
+
+				} else {
+					//ContentOffset = new Point (0, Math.Min (row, (row - height) - 1));
+				}
+
+				SetNeedsDisplay ();
+			}
+		}
+
+		uint _start = 0;
+		uint _selected = 0;
 
 		public const int COLUMN_WIDTH = 3;
 		public const int ROW_HEIGHT = 1;
 
-		public static int MaxCodePointVal => 0x10FFFF;
+		public static uint MaxCodePointVal => 0x10FFFF;
 
-		public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length;
+		public static int RowLabelWidth => $"U+{MaxCodePointVal:x5}".Length + 1; 
 		public static int RowWidth => RowLabelWidth + (COLUMN_WIDTH * 16);
 
 		public CharMap ()
@@ -109,7 +154,7 @@ namespace UICatalog.Scenarios {
 			ColorScheme = Colors.Dialog;
 			CanFocus = true;
 
-			ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16 + 1);
+			ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePointVal / 16 + 1));
 			ShowVerticalScrollIndicator = true;
 			ShowHorizontalScrollIndicator = false;
 			LayoutComplete += (args) => {
@@ -124,12 +169,61 @@ namespace UICatalog.Scenarios {
 			};
 			DrawContent += CharMap_DrawContent;
 
-			AddCommand (Command.ScrollUp, () => { ScrollUp (1); return true; });
-			AddCommand (Command.ScrollDown, () => { ScrollDown (1); return true; });
-			AddCommand (Command.ScrollLeft, () => { ScrollLeft (1); return true; });
-			AddCommand (Command.ScrollRight, () => { ScrollRight (1); return true; });
-			AddCommand (Command.PageUp, () => ScrollUp (Bounds.Height - 1));
-			AddCommand (Command.PageDown, () => ScrollDown (Bounds.Height - 1));
+			AddCommand (Command.ScrollUp, () => {
+				if (SelectedGlyph >= 16) {
+					SelectedGlyph = SelectedGlyph - 16;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollDown, () => {
+				if (SelectedGlyph < MaxCodePointVal - 16) {
+					SelectedGlyph = SelectedGlyph + 16;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollLeft, () => {
+				if (SelectedGlyph > 0) {
+					SelectedGlyph--;
+				}
+				return true;
+			});
+			AddCommand (Command.ScrollRight, () => {
+				if (SelectedGlyph < MaxCodePointVal - 1) {
+					SelectedGlyph++;
+				}
+				return true;
+			});
+			AddCommand (Command.PageUp, () => {
+				var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16;
+				SelectedGlyph -= Math.Min(page, SelectedGlyph);
+				return true;
+			});
+			AddCommand (Command.PageDown, () => {
+				var page = (uint)(Bounds.Height / ROW_HEIGHT - 1) * 16;
+				SelectedGlyph += Math.Min(page, MaxCodePointVal -SelectedGlyph);
+				return true;
+			});
+			AddCommand (Command.TopHome, () => {
+				SelectedGlyph = 0;
+				return true;
+			});
+			AddCommand (Command.BottomEnd, () => {
+				SelectedGlyph = MaxCodePointVal;
+				return true;
+			});
+
+			MouseClick += Handle_MouseClick;
+			Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
+		}
+
+		private void CopyValue ()
+		{
+			Clipboard.Contents = $"U+{SelectedGlyph:x5}";
+		}
+
+		private void CopyGlyph ()
+		{
+			Clipboard.Contents = $"{new Rune (SelectedGlyph)}";
 		}
 
 		private void CharMap_DrawContent (Rect viewport)
@@ -150,8 +244,6 @@ namespace UICatalog.Scenarios {
 					Driver.AddStr ($" {hexDigit:x} ");
 				}
 			}
-			//Move (RowWidth, 0);
-			//Driver.AddRune (' ');
 
 			var firstColumnX = viewport.X + RowLabelWidth;
 			for (int row = -ContentOffset.Y, y = 0; row <= (-ContentOffset.Y) + (Bounds.Height / ROW_HEIGHT); row++, y += ROW_HEIGHT) {
@@ -159,10 +251,11 @@ namespace UICatalog.Scenarios {
 				Driver.SetAttribute (GetNormalColor ());
 				Move (firstColumnX, y + 1);
 				Driver.AddStr (new string (' ', 16 * COLUMN_WIDTH));
-				if (val < MaxCodePointVal) {
+				if (val <= MaxCodePointVal) {
 					Driver.SetAttribute (GetNormalColor ());
 					for (int col = 0; col < 16; col++) {
-						var rune = new Rune ((uint)((uint)val + col));
+						uint glyph = (uint)((uint)val + col);
+						var rune = new Rune (glyph);
 						//if (rune >= 0x00D800 && rune <= 0x00DFFF) {
 						//	if (col == 0) {
 						//		Driver.AddStr ("Reserved for surrogate pairs.");
@@ -170,21 +263,236 @@ namespace UICatalog.Scenarios {
 						//	continue;
 						//}						
 						Move (firstColumnX + (col * COLUMN_WIDTH) + 1, y + 1);
+						if (glyph == SelectedGlyph) {
+							Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+						} else {
+							Driver.SetAttribute (GetNormalColor ());
+						}
 						Driver.AddRune (rune);
 					}
 					Move (0, y + 1);
 					Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.Focus);
-					var rowLabel = $"U+{val / 16:x4}x ";
+					var rowLabel = $"U+{val / 16:x5}_ ";
 					Driver.AddStr (rowLabel);
 				}
 			}
 			Driver.Clip = oldClip;
 		}
 
+		ContextMenu _contextMenu = new ContextMenu ();
+		void Handle_MouseClick (MouseEventArgs args)
+		{
+			var me = args.MouseEvent;
+			if (me.Flags == MouseFlags.ReportMousePosition || (me.Flags != MouseFlags.Button1Clicked &&
+				me.Flags != MouseFlags.Button1DoubleClicked &&
+				me.Flags != _contextMenu.MouseFlags)) {
+				return;
+			}
+
+			if (me.X < RowLabelWidth) {
+				return;
+			}
+
+			if (me.Y < 1) {
+				return;
+			}
+
+			var row = (me.Y - 1);
+			var col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH;
+			uint val = (uint)((((uint)row - (uint)ContentOffset.Y) * 16) + col);
+			if (val > MaxCodePointVal) {
+				return;
+			}
+
+			if (me.Flags == MouseFlags.Button1Clicked) {
+				SelectedGlyph = (uint)val;
+				return;
+			}
+
+			if (me.Flags == MouseFlags.Button1DoubleClicked) {
+				SelectedGlyph = (uint)val;
+				MessageBox.Query ("Glyph", $"{new Rune (val)} U+{SelectedGlyph:x4}", "Ok");
+				return;
+			}
+
+			if (me.Flags == _contextMenu.MouseFlags) {
+				SelectedGlyph = (uint)val;
+				_contextMenu = new ContextMenu (me.X + 1, me.Y + 1,
+					new MenuBarItem (new MenuItem [] {
+					new MenuItem ("_Copy Glyph", "", () => CopyGlyph (), null, null, Key.C | Key.CtrlMask),
+					new MenuItem ("Copy _Value", "", () => CopyValue (), null, null, Key.C | Key.ShiftMask | Key.CtrlMask),
+					}) {
+
+					}
+				);
+				_contextMenu.Show ();
+			}
+		}
+
 		protected override void Dispose (bool disposing)
 		{
 			DrawContent -= CharMap_DrawContent;
 			base.Dispose (disposing);
 		}
 	}
+
+	class UnicodeRange {
+		public uint Start;
+		public uint End;
+		public string Category;
+		public UnicodeRange (uint start, uint end, string category)
+		{
+			this.Start = start;
+			this.End = end;
+			this.Category = category;
+		}
+			
+		public static List<UnicodeRange> Ranges = new List<UnicodeRange> {
+			new UnicodeRange (0x0000, 0x001F, "ASCII Control Characters"),
+			new UnicodeRange (0x0080, 0x009F, "C0 Control Characters"),
+			new UnicodeRange(0x1100, 0x11ff,"Hangul Jamo"),	// This is where wide chars tend to start
+			new UnicodeRange(0x20A0, 0x20CF,"Currency Symbols"),
+			new UnicodeRange(0x2100, 0x214F,"Letterlike Symbols"),
+			new UnicodeRange(0x2160, 0x218F, "Roman Numerals"),
+			new UnicodeRange(0x2190, 0x21ff,"Arrows" ),
+			new UnicodeRange(0x2200, 0x22ff,"Mathematical symbols"),
+			new UnicodeRange(0x2300, 0x23ff,"Miscellaneous Technical"),
+			new UnicodeRange(0x24B6, 0x24e9,"Circled Latin Capital Letters"), 
+			new UnicodeRange(0x1F130, 0x1F149,"Squared Latin Capital Letters"), 
+			new UnicodeRange(0x2500, 0x25ff,"Box Drawing & Geometric Shapes"),
+			new UnicodeRange(0x2600, 0x26ff,"Miscellaneous Symbols"),
+			new UnicodeRange(0x2700, 0x27ff,"Dingbats"),
+			new UnicodeRange(0x2800, 0x28ff,"Braille"),
+			new UnicodeRange(0x2b00, 0x2bff,"Miscellaneous Symbols and Arrows"),
+			new UnicodeRange(0xFB00, 0xFb4f,"Alphabetic Presentation Forms"),
+			new UnicodeRange(0x12400, 0x1240f,"Cuneiform Numbers and Punctuation"),
+			new UnicodeRange(0x1FA00, 0x1FA0f,"Chess Symbols"),
+
+			new UnicodeRange (0x0020 ,0x007F        ,"Basic Latin"),
+			new UnicodeRange (0x00A0 ,0x00FF        ,"Latin-1 Supplement"),
+			new UnicodeRange (0x0100 ,0x017F        ,"Latin Extended-A"),
+			new UnicodeRange (0x0180 ,0x024F        ,"Latin Extended-B"),
+			new UnicodeRange (0x0250 ,0x02AF        ,"IPA Extensions"),
+			new UnicodeRange (0x02B0 ,0x02FF        ,"Spacing Modifier Letters"),
+			new UnicodeRange (0x0300 ,0x036F        ,"Combining Diacritical Marks"),
+			new UnicodeRange (0x0370 ,0x03FF        ,"Greek and Coptic"),
+			new UnicodeRange (0x0400 ,0x04FF        ,"Cyrillic"),
+			new UnicodeRange (0x0500 ,0x052F        ,"Cyrillic Supplementary"),
+			new UnicodeRange (0x0530 ,0x058F        ,"Armenian"),
+			new UnicodeRange (0x0590 ,0x05FF        ,"Hebrew"),
+			new UnicodeRange (0x0600 ,0x06FF        ,"Arabic"),
+			new UnicodeRange (0x0700 ,0x074F        ,"Syriac"),
+			new UnicodeRange (0x0780 ,0x07BF        ,"Thaana"),
+			new UnicodeRange (0x0900 ,0x097F        ,"Devanagari"),
+			new UnicodeRange (0x0980 ,0x09FF        ,"Bengali"),
+			new UnicodeRange (0x0A00 ,0x0A7F        ,"Gurmukhi"),
+			new UnicodeRange (0x0A80 ,0x0AFF        ,"Gujarati"),
+			new UnicodeRange (0x0B00 ,0x0B7F        ,"Oriya"),
+			new UnicodeRange (0x0B80 ,0x0BFF        ,"Tamil"),
+			new UnicodeRange (0x0C00 ,0x0C7F        ,"Telugu"),
+			new UnicodeRange (0x0C80 ,0x0CFF        ,"Kannada"),
+			new UnicodeRange (0x0D00 ,0x0D7F        ,"Malayalam"),
+			new UnicodeRange (0x0D80 ,0x0DFF        ,"Sinhala"),
+			new UnicodeRange (0x0E00 ,0x0E7F        ,"Thai"),
+			new UnicodeRange (0x0E80 ,0x0EFF        ,"Lao"),
+			new UnicodeRange (0x0F00 ,0x0FFF        ,"Tibetan"),
+			new UnicodeRange (0x1000 ,0x109F        ,"Myanmar"),
+			new UnicodeRange (0x10A0 ,0x10FF        ,"Georgian"),
+			new UnicodeRange (0x1100 ,0x11FF        ,"Hangul Jamo"),
+			new UnicodeRange (0x1200 ,0x137F        ,"Ethiopic"),
+			new UnicodeRange (0x13A0 ,0x13FF        ,"Cherokee"),
+			new UnicodeRange (0x1400 ,0x167F        ,"Unified Canadian Aboriginal Syllabics"),
+			new UnicodeRange (0x1680 ,0x169F        ,"Ogham"),
+			new UnicodeRange (0x16A0 ,0x16FF        ,"Runic"),
+			new UnicodeRange (0x1700 ,0x171F        ,"Tagalog"),
+			new UnicodeRange (0x1720 ,0x173F        ,"Hanunoo"),
+			new UnicodeRange (0x1740 ,0x175F        ,"Buhid"),
+			new UnicodeRange (0x1760 ,0x177F        ,"Tagbanwa"),
+			new UnicodeRange (0x1780 ,0x17FF        ,"Khmer"),
+			new UnicodeRange (0x1800 ,0x18AF        ,"Mongolian"),
+			new UnicodeRange (0x1900 ,0x194F        ,"Limbu"),
+			new UnicodeRange (0x1950 ,0x197F        ,"Tai Le"),
+			new UnicodeRange (0x19E0 ,0x19FF        ,"Khmer Symbols"),
+			new UnicodeRange (0x1D00 ,0x1D7F        ,"Phonetic Extensions"),
+			new UnicodeRange (0x1E00 ,0x1EFF        ,"Latin Extended Additional"),
+			new UnicodeRange (0x1F00 ,0x1FFF        ,"Greek Extended"),
+			new UnicodeRange (0x2000 ,0x206F        ,"General Punctuation"),
+			new UnicodeRange (0x2070 ,0x209F        ,"Superscripts and Subscripts"),
+			new UnicodeRange (0x20A0 ,0x20CF        ,"Currency Symbols"),
+			new UnicodeRange (0x20D0 ,0x20FF        ,"Combining Diacritical Marks for Symbols"),
+			new UnicodeRange (0x2100 ,0x214F        ,"Letterlike Symbols"),
+			new UnicodeRange (0x2150 ,0x218F        ,"Number Forms"),
+			new UnicodeRange (0x2190 ,0x21FF        ,"Arrows"),
+			new UnicodeRange (0x2200 ,0x22FF        ,"Mathematical Operators"),
+			new UnicodeRange (0x2300 ,0x23FF        ,"Miscellaneous Technical"),
+			new UnicodeRange (0x2400 ,0x243F        ,"Control Pictures"),
+			new UnicodeRange (0x2440 ,0x245F        ,"Optical Character Recognition"),
+			new UnicodeRange (0x2460 ,0x24FF        ,"Enclosed Alphanumerics"),
+			new UnicodeRange (0x2500 ,0x257F        ,"Box Drawing"),
+			new UnicodeRange (0x2580 ,0x259F        ,"Block Elements"),
+			new UnicodeRange (0x25A0 ,0x25FF        ,"Geometric Shapes"),
+			new UnicodeRange (0x2600 ,0x26FF        ,"Miscellaneous Symbols"),
+			new UnicodeRange (0x2700 ,0x27BF        ,"Dingbats"),
+			new UnicodeRange (0x27C0 ,0x27EF        ,"Miscellaneous Mathematical Symbols-A"),
+			new UnicodeRange (0x27F0 ,0x27FF        ,"Supplemental Arrows-A"),
+			new UnicodeRange (0x2800 ,0x28FF        ,"Braille Patterns"),
+			new UnicodeRange (0x2900 ,0x297F        ,"Supplemental Arrows-B"),
+			new UnicodeRange (0x2980 ,0x29FF        ,"Miscellaneous Mathematical Symbols-B"),
+			new UnicodeRange (0x2A00 ,0x2AFF        ,"Supplemental Mathematical Operators"),
+			new UnicodeRange (0x2B00 ,0x2BFF        ,"Miscellaneous Symbols and Arrows"),
+			new UnicodeRange (0x2E80 ,0x2EFF        ,"CJK Radicals Supplement"),
+			new UnicodeRange (0x2F00 ,0x2FDF        ,"Kangxi Radicals"),
+			new UnicodeRange (0x2FF0 ,0x2FFF        ,"Ideographic Description Characters"),
+			new UnicodeRange (0x3000 ,0x303F        ,"CJK Symbols and Punctuation"),
+			new UnicodeRange (0x3040 ,0x309F        ,"Hiragana"),
+			new UnicodeRange (0x30A0 ,0x30FF        ,"Katakana"),
+			new UnicodeRange (0x3100 ,0x312F        ,"Bopomofo"),
+			new UnicodeRange (0x3130 ,0x318F        ,"Hangul Compatibility Jamo"),
+			new UnicodeRange (0x3190 ,0x319F        ,"Kanbun"),
+			new UnicodeRange (0x31A0 ,0x31BF        ,"Bopomofo Extended"),
+			new UnicodeRange (0x31F0 ,0x31FF        ,"Katakana Phonetic Extensions"),
+			new UnicodeRange (0x3200 ,0x32FF        ,"Enclosed CJK Letters and Months"),
+			new UnicodeRange (0x3300 ,0x33FF        ,"CJK Compatibility"),
+			new UnicodeRange (0x3400 ,0x4DBF        ,"CJK Unified Ideographs Extension A"),
+			new UnicodeRange (0x4DC0 ,0x4DFF        ,"Yijing Hexagram Symbols"),
+			new UnicodeRange (0x4E00 ,0x9FFF        ,"CJK Unified Ideographs"),
+			new UnicodeRange (0xA000 ,0xA48F        ,"Yi Syllables"),
+			new UnicodeRange (0xA490 ,0xA4CF        ,"Yi Radicals"),
+			new UnicodeRange (0xAC00 ,0xD7AF        ,"Hangul Syllables"),
+			new UnicodeRange (0xD800 ,0xDB7F        ,"High Surrogates"),
+			new UnicodeRange (0xDB80 ,0xDBFF        ,"High Private Use Surrogates"),
+			new UnicodeRange (0xDC00 ,0xDFFF        ,"Low Surrogates"),
+			new UnicodeRange (0xE000 ,0xF8FF        ,"Private Use Area"),
+			new UnicodeRange (0xF900 ,0xFAFF        ,"CJK Compatibility Ideographs"),
+			new UnicodeRange (0xFB00 ,0xFB4F        ,"Alphabetic Presentation Forms"),
+			new UnicodeRange (0xFB50 ,0xFDFF        ,"Arabic Presentation Forms-A"),
+			new UnicodeRange (0xFE00 ,0xFE0F        ,"Variation Selectors"),
+			new UnicodeRange (0xFE20 ,0xFE2F        ,"Combining Half Marks"),
+			new UnicodeRange (0xFE30 ,0xFE4F        ,"CJK Compatibility Forms"),
+			new UnicodeRange (0xFE50 ,0xFE6F        ,"Small Form Variants"),
+			new UnicodeRange (0xFE70 ,0xFEFF        ,"Arabic Presentation Forms-B"),
+			new UnicodeRange (0xFF00 ,0xFFEF        ,"Halfwidth and Fullwidth Forms"),
+			new UnicodeRange (0xFFF0 ,0xFFFF        ,"Specials"),
+			new UnicodeRange (0x10000, 0x1007F   ,"Linear B Syllabary"),
+			new UnicodeRange (0x10080, 0x100FF   ,"Linear B Ideograms"),
+			new UnicodeRange (0x10100, 0x1013F   ,"Aegean Numbers"),
+			new UnicodeRange (0x10300, 0x1032F   ,"Old Italic"),
+			new UnicodeRange (0x10330, 0x1034F   ,"Gothic"),
+			new UnicodeRange (0x10380, 0x1039F   ,"Ugaritic"),
+			new UnicodeRange (0x10400, 0x1044F   ,"Deseret"),
+			new UnicodeRange (0x10450, 0x1047F   ,"Shavian"),
+			new UnicodeRange (0x10480, 0x104AF   ,"Osmanya"),
+			new UnicodeRange (0x10800, 0x1083F   ,"Cypriot Syllabary"),
+			new UnicodeRange (0x1D000, 0x1D0FF   ,"Byzantine Musical Symbols"),
+			new UnicodeRange (0x1D100, 0x1D1FF   ,"Musical Symbols"),
+			new UnicodeRange (0x1D300, 0x1D35F   ,"Tai Xuan Jing Symbols"),
+			new UnicodeRange (0x1D400, 0x1D7FF   ,"Mathematical Alphanumeric Symbols"),
+			new UnicodeRange (0x1F600, 0x1F532   ,"Emojis Symbols"),
+			new UnicodeRange (0x20000, 0x2A6DF   ,"CJK Unified Ideographs Extension B"),
+			new UnicodeRange (0x2F800, 0x2FA1F   ,"CJK Compatibility Ideographs Supplement"),
+			new UnicodeRange (0xE0000, 0xE007F   ,"Tags"),
+			new UnicodeRange((uint)(CharMap.MaxCodePointVal - 16), (uint)CharMap.MaxCodePointVal,"End"),
+		};
+	}
+	
 }

+ 0 - 0
UICatalog/Scenarios/Generic - Copy.cs → UICatalog/Scenarios/RunTExample.cs


+ 84 - 33
UICatalog/Scenarios/Text.cs

@@ -1,5 +1,6 @@
 using NStack;
 using System;
+using System.IO;
 using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
@@ -16,12 +17,12 @@ namespace UICatalog.Scenarios {
 	public class Text : Scenario {
 		public override void Setup ()
 		{
-			var s = "TAB to jump between text fields.";
-			var textField = new TextField (s) {
+			// TextField is a simple, single-line text input control
+			var textField = new TextField ("TextField with test text. Unicode shouldn't 𝔹Aℝ𝔽!") {
 				X = 1,
-				Y = 1,
-				Width = Dim.Percent (50),
-				//ColorScheme = Colors.Dialog
+				Y = 0,
+				Width = Dim.Percent (50) - 1,
+				Height = 2
 			};
 			textField.TextChanging += TextField_TextChanging;
 
@@ -36,7 +37,7 @@ namespace UICatalog.Scenarios {
 			var labelMirroringTextField = new Label (textField.Text) {
 				X = Pos.Right (textField) + 1,
 				Y = Pos.Top (textField),
-				Width = Dim.Fill (1)
+				Width = Dim.Fill (1) - 1
 			};
 			Win.Add (labelMirroringTextField);
 
@@ -44,15 +45,17 @@ namespace UICatalog.Scenarios {
 				labelMirroringTextField.Text = textField.Text;
 			};
 
+			// TextView is a rich (as in functionality, not formatting) text editing control
 			var textView = new TextView () {
 				X = 1,
-				Y = 3,
-				Width = Dim.Percent (50),
+				Y = Pos.Bottom (textField),
+				Width = Dim.Percent (50) - 1,
 				Height = Dim.Percent (30),
 			};
-			textView.Text = s;
+			textView.Text = "TextView with some more test text. Unicode shouldn't 𝔹Aℝ𝔽!" ;
 			textView.DrawContent += TextView_DrawContent;
 
+			// This shows how to enable autocomplete in TextView.
 			void TextView_DrawContent (Rect e)
 			{
 				textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+")
@@ -61,40 +64,89 @@ namespace UICatalog.Scenarios {
 			}
 			Win.Add (textView);
 
-			var labelMirroringTextView = new Label (textView.Text) {
+			var labelMirroringTextView = new Label () {
 				X = Pos.Right (textView) + 1,
 				Y = Pos.Top (textView),
-				Width = Dim.Fill (1),
-				Height = Dim.Height (textView),
+				Width = Dim.Fill (1) - 1,
+				Height = Dim.Height (textView) - 1,
 			};
 			Win.Add (labelMirroringTextView);
 
-			textView.TextChanged += () => {
+			// Use ContentChanged to detect if the user has typed something in a TextView.
+			// The TextChanged property is only fired if the TextView.Text property is
+			// explicitly set
+			textView.ContentsChanged += (a) => {
+				labelMirroringTextView.Enabled = !labelMirroringTextView.Enabled;
 				labelMirroringTextView.Text = textView.Text;
 			};
 
-			var btnMultiline = new Button ("Toggle Multiline") {
-				X = Pos.Right (textView) + 1,
-				Y = Pos.Top (textView) + 1
+			// By default TextView is a multi-line control. It can be forced to 
+			// single-line mode.
+			var chxMultiline = new CheckBox ("Multiline") {
+				X = Pos.Left (textView),
+				Y = Pos.Bottom (textView), 
+				Checked = true
+			};
+			chxMultiline.Toggled += (b) => textView.Multiline = b;
+			Win.Add (chxMultiline);
+
+			var chxWordWrap = new CheckBox ("Word Wrap") {
+				X = Pos.Right (chxMultiline) + 2,
+				Y = Pos.Top (chxMultiline)
+			};
+			chxWordWrap.Toggled += (b) => textView.WordWrap = b;
+			Win.Add (chxWordWrap);
+
+			// TextView captures Tabs (so users can enter /t into text) by default;
+			// This means using Tab to navigate doesn't work by default. This shows
+			// how to turn tab capture off.
+			var chxCaptureTabs = new CheckBox ("Capture Tabs") {
+				X = Pos.Right (chxWordWrap) + 2,
+				Y = Pos.Top (chxWordWrap),
+				Checked = true
 			};
-			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))) {
+			Key keyTab = textView.GetKeyFromCommand (Command.Tab);
+			Key keyBackTab = textView.GetKeyFromCommand (Command.BackTab);
+			chxCaptureTabs.Toggled += (b) => { 
+				if (b) {
+					textView.AddKeyBinding (keyTab, Command.Tab);
+					textView.AddKeyBinding (keyBackTab, Command.BackTab);
+				} else {
+					textView.ClearKeybinding (keyTab);
+					textView.ClearKeybinding (keyBackTab);
+				}
+				textView.WordWrap = b; 
+			};
+			Win.Add (chxCaptureTabs);
+
+			var hexEditor = new HexView (new MemoryStream (Encoding.UTF8.GetBytes ("HexEditor Unicode that shouldn't 𝔹Aℝ𝔽!"))) {
 				X = 1,
-				Y = Pos.Bottom (textView) + 1,
-				Width = Dim.Fill (1),
+				Y = Pos.Bottom (chxMultiline) + 1,
+				Width = Dim.Percent (50) - 1,
 				Height = Dim.Percent (30),
-				//ColorScheme = Colors.Dialog
 			};
-			Win.Add (hexView);
+			Win.Add (hexEditor);
+
+			var labelMirroringHexEditor = new Label () {
+				X = Pos.Right (hexEditor) + 1,
+				Y = Pos.Top (hexEditor),
+				Width = Dim.Fill (1) - 1,
+				Height = Dim.Height (hexEditor) - 1,
+			};
+			var array = ((MemoryStream)hexEditor.Source).ToArray ();
+			labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length);
+			hexEditor.Edited += (kv) => {
+				hexEditor.ApplyEdits ();
+				var array = ((MemoryStream)hexEditor.Source).ToArray (); 
+				labelMirroringHexEditor.Text = Encoding.UTF8.GetString (array, 0, array.Length);
+			};
+			Win.Add (labelMirroringHexEditor);
 
 			var dateField = new DateField (System.DateTime.Now) {
 				X = 1,
-				Y = Pos.Bottom (hexView) + 1,
+				Y = Pos.Bottom (hexEditor) + 1,
 				Width = 20,
-				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
 			Win.Add (dateField);
@@ -113,9 +165,8 @@ namespace UICatalog.Scenarios {
 
 			_timeField = new TimeField (DateTime.Now.TimeOfDay) {
 				X = Pos.Right (labelMirroringDateField) + 5,
-				Y = Pos.Bottom (hexView) + 1,
+				Y = Pos.Bottom (hexEditor) + 1,
 				Width = 20,
-				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
 			Win.Add (_timeField);
@@ -130,8 +181,8 @@ namespace UICatalog.Scenarios {
 
 			_timeField.TimeChanged += TimeChanged;
 
-			// MaskedTextProvider
-			var netProviderLabel = new Label (".Net MaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") {
+			// MaskedTextProvider - uses .NET MaskedTextProvider
+			var netProviderLabel = new Label ("NetMaskedTextProvider [ 999 000 LLL >LLL| AAA aaa ]") {
 				X = Pos.Left (dateField),
 				Y = Pos.Bottom (dateField) + 1
 			};
@@ -141,13 +192,13 @@ namespace UICatalog.Scenarios {
 
 			var netProviderField = new TextValidateField (netProvider) {
 				X = Pos.Right (netProviderLabel) + 1,
-				Y = Pos.Y (netProviderLabel)
+				Y = Pos.Y (netProviderLabel),
 			};
 
 			Win.Add (netProviderField);
 
-			// TextRegexProvider
-			var regexProvider = new Label ("Gui.cs TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") {
+			// TextRegexProvider - Regex provider implemented by Terminal.Gui
+			var regexProvider = new Label ("TextRegexProvider [ ^([0-9]?[0-9]?[0-9]|1000)$ ]") {
 				X = Pos.Left (netProviderLabel),
 				Y = Pos.Bottom (netProviderLabel) + 1
 			};

+ 364 - 1
UnitTests/TextViewTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.Tracing;
 using System.Linq;
 using System.Reflection;
 using System.Text.RegularExpressions;
@@ -24,6 +25,7 @@ namespace Terminal.Gui.Views {
 		[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
 		public class InitShutdown : Xunit.Sdk.BeforeAfterTestAttribute {
 
+			public static string txt = "TAB to jump between text fields.";
 			public override void Before (MethodInfo methodUnderTest)
 			{
 				if (_textView != null) {
@@ -34,7 +36,6 @@ namespace Terminal.Gui.Views {
 
 				//                   1         2         3 
 				//         01234567890123456789012345678901=32 (Length)
-				var txt = "TAB to jump between text fields.";
 				var buff = new byte [txt.Length];
 				for (int i = 0; i < txt.Length; i++) {
 					buff [i] = (byte)txt [i];
@@ -1395,6 +1396,22 @@ namespace Terminal.Gui.Views {
 			Assert.Equal ("changed", _textView.Text);
 		}
 
+		[Fact]
+		[InitShutdown]
+		public void TextChanged_Event_NoFires_OnTyping ()
+		{
+			var eventcount = 0;
+			_textView.TextChanged += () => {
+				eventcount++;
+			};
+
+			_textView.Text = "ay";
+			Assert.Equal (1, eventcount);
+			_textView.ProcessKey (new KeyEvent (Key.Y, new KeyModifiers ()));
+			Assert.Equal (1, eventcount);
+			Assert.Equal ("Yay", _textView.Text.ToString ());
+		}
+
 		[Fact]
 		[InitShutdown]
 		public void Used_Is_True_By_Default ()
@@ -6409,5 +6426,351 @@ This is the second line.
 │             │
 └─────────────┘", output);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentsChanged_Event_NoFires_On_CursorPosition ()
+		{
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+			};
+
+			var eventcount = 0;
+			Assert.Null (tv.ContentsChanged);
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+			};
+
+			tv.CursorPosition = new Point (0, 0);
+
+			Assert.Equal (0, eventcount);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentsChanged_Event_Fires_On_InsertText ()
+		{
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+			};
+			tv.CursorPosition = new Point (0, 0);
+
+			var eventcount = 0;
+
+			Assert.Null (tv.ContentsChanged);
+			tv.ContentsChanged += (e) => {
+				eventcount++;
+			};
+
+
+			tv.InsertText ("a");
+			Assert.Equal (1, eventcount);
+
+			tv.CursorPosition = new Point (0, 0);
+			tv.InsertText ("bcd");
+			Assert.Equal (4, eventcount);
+			
+			tv.InsertText ("e");
+			Assert.Equal (5, eventcount);
+
+			tv.InsertText ("\n");
+			Assert.Equal (6, eventcount);
+
+			tv.InsertText ("1234");
+			Assert.Equal (10, eventcount);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentsChanged_Event_Fires_On_Init ()
+		{
+			Application.Iteration += () => {
+				Application.RequestStop ();
+			};
+
+			var expectedRow = 0;
+			var expectedCol = 0;
+			var eventcount = 0;
+
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				ContentsChanged = (e) => {
+					eventcount++;
+					Assert.Equal (expectedRow, e.Row);
+					Assert.Equal (expectedCol, e.Col);
+				}
+			};
+
+			Application.Top.Add (tv);
+			Application.Begin (Application.Top);
+			Assert.Equal (1, eventcount);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentsChanged_Event_Fires_On_Set_Text ()
+		{
+			Application.Iteration += () => {
+				Application.RequestStop ();
+			};
+			var eventcount = 0;
+
+			var expectedRow = 0;
+			var expectedCol = 0;
+
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				// you'd think col would be 3, but it's 0 because TextView sets
+				// row/col = 0 when you set Text
+				Text = "abc",
+				ContentsChanged = (e) => {
+					eventcount++;
+					Assert.Equal (expectedRow, e.Row);
+					Assert.Equal (expectedCol, e.Col);
+				}
+			};
+			Assert.Equal ("abc", tv.Text);
+
+			Application.Top.Add (tv);
+			var rs = Application.Begin (Application.Top);
+			Assert.Equal (1, eventcount); // for Initialize
+
+			expectedCol = 0;
+			tv.Text = "defg";
+			Assert.Equal (2, eventcount); // for set Text = "defg"
+		}
+
+		[Fact, AutoInitShutdown]
+		public void ContentsChanged_Event_Fires_On_Typing ()
+		{
+			Application.Iteration += () => {
+				Application.RequestStop ();
+			};
+			var eventcount = 0;
+
+			var expectedRow = 0;
+			var expectedCol = 0;
+
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				ContentsChanged = (e) => {
+					eventcount++;
+					Assert.Equal (expectedRow, e.Row);
+					Assert.Equal (expectedCol, e.Col);
+				}
+			};
+
+			Application.Top.Add (tv);
+			var rs = Application.Begin (Application.Top);
+			Assert.Equal (1, eventcount); // for Initialize
+
+			expectedCol = 0;
+			tv.Text = "ay";
+			Assert.Equal (2, eventcount);
+
+			expectedCol = 1;
+			tv.ProcessKey (new KeyEvent (Key.Y, new KeyModifiers ()));
+			Assert.Equal (3, eventcount);
+			Assert.Equal ("Yay", tv.Text.ToString ());
+		}
+
+		[Fact, InitShutdown]
+		public void ContentsChanged_Event_Fires_Using_Kill_Delete_Tests ()
+		{
+			var eventcount = 0;
+
+			_textView.ContentsChanged = (e) => {
+				eventcount++;
+			};
+
+			var expectedEventCount = 1;
+			Kill_Delete_WordForward ();
+			Assert.Equal (expectedEventCount, eventcount); // for Initialize
+
+			expectedEventCount += 1;
+			Kill_Delete_WordBackward ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 1;
+			Kill_To_End_Delete_Forwards_And_Copy_To_The_Clipboard ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 1;
+			Kill_To_Start_Delete_Backwards_And_Copy_To_The_Clipboard ();
+			Assert.Equal (expectedEventCount, eventcount);
+		}
+
+
+		[Fact, InitShutdown]
+		public void ContentsChanged_Event_Fires_Using_Copy_Or_Cut_Tests ()
+		{
+			var eventcount = 0;
+
+			_textView.ContentsChanged = (e) => {
+				eventcount++;
+			};
+
+			var expectedEventCount = 1;
+
+			// reset
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 3;
+			Copy_Or_Cut_And_Paste_With_No_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// reset
+			expectedEventCount += 1;
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 3;
+			Copy_Or_Cut_And_Paste_With_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// reset
+			expectedEventCount += 1;
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 1;
+			Copy_Or_Cut_Not_Null_If_Has_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// reset
+			expectedEventCount += 1;
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 1;
+			Copy_Or_Cut_Null_If_No_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// reset
+			expectedEventCount += 1;
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 4;
+			Copy_Without_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+			
+			// reset
+			expectedEventCount += 1;
+			_textView.Text = InitShutdown.txt;
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount += 4;
+			Copy_Without_Selection ();
+			Assert.Equal (expectedEventCount, eventcount);
+		}
+
+		[Fact, InitShutdown]
+		public void ContentsChanged_Event_Fires_On_Undo_Redo ()
+		{
+			var eventcount = 0;
+			var expectedEventCount = 0;
+
+			_textView.ContentsChanged = (e) => {
+				eventcount++;
+			};
+
+			expectedEventCount++;
+			_textView.Text = "This is the first line.\nThis is the second line.\nThis is the third line.";
+			Assert.Equal (expectedEventCount, eventcount);
+
+			expectedEventCount++;
+			Assert.True (_textView.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// Undo
+			expectedEventCount++;
+			Assert.True (_textView.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// Redo
+			expectedEventCount++;
+			Assert.True (_textView.ProcessKey (new KeyEvent (Key.R | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// Undo
+			expectedEventCount++;
+			Assert.True (_textView.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (expectedEventCount, eventcount);
+
+			// Redo
+			expectedEventCount++;
+			Assert.True (_textView.ProcessKey (new KeyEvent (Key.R | Key.CtrlMask, new KeyModifiers ())));
+			Assert.Equal (expectedEventCount, eventcount);
+		}
+
+		[Fact]
+		public void ContentsChanged_Event_Fires_ClearHistoryChanges ()
+		{
+			var eventcount = 0;
+
+			var text = "This is the first line.\nThis is the second line.\nThis is the third line.";
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				Text = text,
+				ContentsChanged = (e) => {
+					eventcount++;
+				}
+			};
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ())));
+			Assert.Equal ($"{Environment.NewLine}This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text);
+			Assert.Equal (4, tv.Lines);
+
+			var expectedEventCount = 1; // for ENTER key
+			Assert.Equal (expectedEventCount, eventcount);
+			
+			tv.ClearHistoryChanges ();
+			expectedEventCount = 2;
+			Assert.Equal (expectedEventCount, eventcount);
+		}
+
+		[Fact]
+		public void ContentsChanged_Event_Fires_LoadStream ()
+		{
+			var eventcount = 0;
+
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				ContentsChanged = (e) => {
+					eventcount++;
+				}
+			};
+
+			var text = "This is the first line.\r\nThis is the second line.\r\n";
+			tv.LoadStream (new System.IO.MemoryStream (System.Text.Encoding.ASCII.GetBytes (text)));
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
+			
+			Assert.Equal (1, eventcount);
+		}
+
+		[Fact]
+		public void ContentsChanged_Event_Fires_LoadFile ()
+		{
+			var eventcount = 0;
+
+			var tv = new TextView {
+				Width = 50,
+				Height = 10,
+				ContentsChanged = (e) => {
+					eventcount++;
+				}
+			};
+			var fileName = "textview.txt";
+			System.IO.File.WriteAllText (fileName, "This is the first line.\r\nThis is the second line.\r\n") ;
+
+			tv.LoadFile (fileName);
+			Assert.Equal (1, eventcount);
+			Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}", tv.Text);
+		}
 	}
 }