瀏覽代碼

Added ContentsChanged event & unit tests. Updated Text.cs Scenario

Charlie Kindel 2 年之前
父節點
當前提交
787349e177
共有 4 個文件被更改,包括 554 次插入71 次删除
  1. 106 37
      Terminal.Gui/Views/TextView.cs
  2. 0 0
      UICatalog/Scenarios/RunTExample.cs
  3. 84 33
      UICatalog/Scenarios/Text.cs
  4. 364 1
      UnitTests/TextViewTests.cs

+ 106 - 37
Terminal.Gui/Views/TextView.cs

@@ -33,6 +33,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 +743,7 @@ namespace Terminal.Gui {
 			historyTextItems.Clear ();
 			idxHistoryText = -1;
 			originalText = text;
+			OnChangeText (null);
 		}
 
 		public bool IsDirty (ustring text)
@@ -1172,10 +1174,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 explictly
+		/// 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>
@@ -1187,16 +1203,6 @@ namespace Terminal.Gui {
 		/// </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.
 		/// </summary>
@@ -1404,48 +1410,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 +1468,7 @@ namespace Terminal.Gui {
 
 			Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged;
 			Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged;
+			OnContentsChanged ();
 		}
 
 		void Top_AlternateBackwardKeyChanged (Key obj)
@@ -1483,6 +1498,8 @@ namespace Terminal.Gui {
 		///   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 {
@@ -1845,6 +1862,7 @@ namespace Terminal.Gui {
 				UpdateWrapModel ();
 				SetNeedsDisplay ();
 				Adjust ();
+				OnContentsChanged ();
 			}
 			return res;
 		}
@@ -1859,6 +1877,7 @@ namespace Terminal.Gui {
 			model.LoadStream (stream);
 			ResetPosition ();
 			SetNeedsDisplay ();
+			OnContentsChanged ();
 		}
 
 		/// <summary>
@@ -2504,6 +2523,12 @@ namespace Terminal.Gui {
 
 				InsertText (new KeyEvent () { Key = key });
 			}
+
+			if (NeedDisplay.IsEmpty) {
+				PositionCursor ();
+			} else {
+				Adjust ();
+			}
 		}
 
 		void Insert (Rune rune)
@@ -2521,6 +2546,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 +2610,8 @@ namespace Terminal.Gui {
 
 				UpdateWrapModel ();
 
+				OnContentsChanged ();
+
 				return;
 			}
 
@@ -2690,6 +2718,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;
@@ -3178,6 +3242,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 
 			DoNeededAction ();
+			OnContentsChanged ();
 			return true;
 		}
 
@@ -3674,6 +3739,7 @@ namespace Terminal.Gui {
 				HistoryText.LineStatus.Replaced);
 
 			UpdateWrapModel ();
+			OnContentsChanged ();
 
 			return true;
 		}
@@ -3883,6 +3949,7 @@ namespace Terminal.Gui {
 			UpdateWrapModel ();
 			selecting = false;
 			DoNeededAction ();
+			OnContentsChanged ();
 		}
 
 		/// <summary>
@@ -3913,6 +3980,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 +4491,7 @@ namespace Terminal.Gui {
 		}
 	}
 
+
 	/// <summary>
 	/// Renders an overlay on another view at a given point that allows selecting
 	/// from a range of 'autocomplete' options.

+ 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);
+		}
 	}
 }