Răsfoiți Sursa

Merge pull request #745 from BDisp/view-tab-index-feature

Fixes #742. Added a TabIndex to the View.
Charlie Kindel 5 ani în urmă
părinte
comite
f54b0c4849
2 a modificat fișierele cu 433 adăugiri și 21 ștergeri
  1. 87 20
      Terminal.Gui/Core/View.cs
  2. 346 1
      UnitTests/ViewTests.cs

+ 87 - 20
Terminal.Gui/Core/View.cs

@@ -200,6 +200,63 @@ namespace Terminal.Gui {
 		// to make the same mistakes our users make when they poke at the Subviews.
 		internal IList<View> InternalSubviews => subviews ?? empty;
 
+		// This is null, and allocated on demand.
+		List<View> tabIndexes;
+
+		/// <summary>
+		/// This returns a tab index list of the subviews contained by this view.
+		/// </summary>
+		/// <value>The tabIndexes.</value>
+		public IList<View> TabIndexes => tabIndexes == null ? empty : tabIndexes.AsReadOnly ();
+
+		int tabIndex = -1;
+
+		/// <summary>
+		/// Indicates the index of the current <see cref="View"/> from the <see cref="TabIndexes"/> list.
+		/// </summary>
+		public int TabIndex {
+			get { return tabIndex; }
+			set {
+				if (!CanFocus || SuperView?.tabIndexes == null || SuperView?.tabIndexes.Count == 1 || tabIndex == value) {
+					return;
+				}
+				tabIndex = value > SuperView.tabIndexes.Count - 1 ? SuperView.tabIndexes.Count - 1 : value < 0 ? 0 : value;
+				SuperView.tabIndexes.Remove (this);
+				SuperView.tabIndexes.Insert (tabIndex, this);
+			}
+		}
+
+		bool tabStop = true;
+
+		/// <summary>
+		/// This only be <c>true</c> if the <see cref="CanFocus"/> is also <c>true</c> and the focus can be avoided by setting this to <c>false</c>
+		/// </summary>
+		public bool TabStop {
+			get { return tabStop; }
+			set {
+				if (tabStop == value) {
+					return;
+				}
+				tabStop = CanFocus && value;
+			}
+		}
+
+		/// <inheritdoc/>
+		public override bool CanFocus {
+			get => base.CanFocus;
+			set {
+				if (base.CanFocus != value) {
+					base.CanFocus = value;
+					if (!value && tabIndex > -1) {
+						tabIndex = -1;
+					}
+					if (!value && tabStop) {
+						tabStop = false;
+					}
+				}
+			}
+		}
+
 		internal Rect NeedDisplay { get; private set; } = Rect.Empty;
 
 		// The frame for the object. Superview relative.
@@ -558,13 +615,21 @@ namespace Terminal.Gui {
 		{
 			if (view == null)
 				return;
-			if (subviews == null)
+			if (subviews == null) {
 				subviews = new List<View> ();
+			}
+			if (tabIndexes == null) {
+				tabIndexes = new List<View> ();
+			}
 			subviews.Add (view);
+			tabIndexes.Add (view);
 			view.container = this;
 			OnAdded (view);
-			if (view.CanFocus)
+			if (view.CanFocus) {
 				CanFocus = true;
+				view.tabIndex = tabIndexes.IndexOf (view);
+			}
+
 			SetNeedsLayout ();
 			SetNeedsDisplay ();
 		}
@@ -594,6 +659,7 @@ namespace Terminal.Gui {
 
 			while (subviews.Count > 0) {
 				Remove (subviews [0]);
+				Remove (tabIndexes [0]);
 			}
 		}
 
@@ -611,9 +677,10 @@ namespace Terminal.Gui {
 			SetNeedsDisplay ();
 			var touched = view.Frame;
 			subviews.Remove (view);
+			tabIndexes.Remove (view);
 			view.container = null;
 			OnRemoved (view);
-
+			view.tabIndex = -1;
 			if (subviews.Count < 1)
 				this.CanFocus = false;
 
@@ -1303,13 +1370,13 @@ namespace Terminal.Gui {
 		/// </summary>
 		public void FocusFirst ()
 		{
-			if (subviews == null) {
+			if (tabIndexes == null) {
 				SuperView?.SetFocus (this);
 				return;
 			}
 
-			foreach (var view in subviews) {
-				if (view.CanFocus) {
+			foreach (var view in tabIndexes) {
+				if (view.CanFocus && view.tabStop) {
 					SetFocus (view);
 					return;
 				}
@@ -1321,16 +1388,16 @@ namespace Terminal.Gui {
 		/// </summary>
 		public void FocusLast ()
 		{
-			if (subviews == null) {
+			if (tabIndexes == null) {
 				SuperView?.SetFocus (this);
 				return;
 			}
 
-			for (int i = subviews.Count; i > 0;) {
+			for (int i = tabIndexes.Count; i > 0;) {
 				i--;
 
-				View v = subviews [i];
-				if (v.CanFocus) {
+				View v = tabIndexes [i];
+				if (v.CanFocus && v.tabStop) {
 					SetFocus (v);
 					return;
 				}
@@ -1344,7 +1411,7 @@ namespace Terminal.Gui {
 		public bool FocusPrev ()
 		{
 			FocusDirection = Direction.Backward;
-			if (subviews == null || subviews.Count == 0)
+			if (tabIndexes == null || tabIndexes.Count == 0)
 				return false;
 
 			if (focused == null) {
@@ -1352,9 +1419,9 @@ namespace Terminal.Gui {
 				return focused != null;
 			}
 			int focused_idx = -1;
-			for (int i = subviews.Count; i > 0;) {
+			for (int i = tabIndexes.Count; i > 0;) {
 				i--;
-				View w = subviews [i];
+				View w = tabIndexes [i];
 
 				if (w.HasFocus) {
 					if (w.FocusPrev ())
@@ -1362,10 +1429,10 @@ namespace Terminal.Gui {
 					focused_idx = i;
 					continue;
 				}
-				if (w.CanFocus && focused_idx != -1) {
+				if (w.CanFocus && focused_idx != -1 && w.tabStop) {
 					focused.SetHasFocus (false, w);
 
-					if (w != null && w.CanFocus)
+					if (w != null && w.CanFocus && w.tabStop)
 						w.FocusLast ();
 
 					SetFocus (w);
@@ -1386,17 +1453,17 @@ namespace Terminal.Gui {
 		public bool FocusNext ()
 		{
 			FocusDirection = Direction.Forward;
-			if (subviews == null || subviews.Count == 0)
+			if (tabIndexes == null || tabIndexes.Count == 0)
 				return false;
 
 			if (focused == null) {
 				FocusFirst ();
 				return focused != null;
 			}
-			int n = subviews.Count;
+			int n = tabIndexes.Count;
 			int focused_idx = -1;
 			for (int i = 0; i < n; i++) {
-				View w = subviews [i];
+				View w = tabIndexes [i];
 
 				if (w.HasFocus) {
 					if (w.FocusNext ())
@@ -1404,10 +1471,10 @@ namespace Terminal.Gui {
 					focused_idx = i;
 					continue;
 				}
-				if (w.CanFocus && focused_idx != -1) {
+				if (w.CanFocus && focused_idx != -1 && w.tabStop) {
 					focused.SetHasFocus (false, w);
 
-					if (w != null && w.CanFocus)
+					if (w != null && w.CanFocus && w.tabStop)
 						w.FocusFirst ();
 
 					SetFocus (w);

+ 346 - 1
UnitTests/ViewTests.cs

@@ -137,7 +137,7 @@ namespace Terminal.Gui {
 		}
 
 		[Fact]
-		public void Added_Removing ()
+		public void Added_Removed ()
 		{
 			var v = new View (new Rect (0, 0, 10, 24));
 			var t = new View ();
@@ -156,5 +156,350 @@ namespace Terminal.Gui {
 			t.Remove (v);
 			Assert.True (t.Subviews.Count == 0);
 		}
+
+		[Fact]
+		public void Subviews_TabIndexes_AreEqual ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.Subviews.IndexOf (v2) == 1);
+			Assert.True (r.Subviews.IndexOf (v3) == 2);
+
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v2) == 1);
+			Assert.True (r.TabIndexes.IndexOf (v3) == 2);
+
+			Assert.Equal (r.Subviews.IndexOf (v1), r.TabIndexes.IndexOf (v1));
+			Assert.Equal (r.Subviews.IndexOf (v2), r.TabIndexes.IndexOf (v2));
+			Assert.Equal (r.Subviews.IndexOf (v3), r.TabIndexes.IndexOf (v3));
+		}
+
+		[Fact]
+		public void BringSubviewToFront_Subviews_vs_TabIndexes ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			r.BringSubviewToFront (v1);
+			Assert.True (r.Subviews.IndexOf (v1) == 2);
+			Assert.True (r.Subviews.IndexOf (v2) == 0);
+			Assert.True (r.Subviews.IndexOf (v3) == 1);
+
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v2) == 1);
+			Assert.True (r.TabIndexes.IndexOf (v3) == 2);
+		}
+
+		[Fact]
+		public void BringSubviewForward_Subviews_vs_TabIndexes ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			r.BringSubviewForward (v1);
+			Assert.True (r.Subviews.IndexOf (v1) == 1);
+			Assert.True (r.Subviews.IndexOf (v2) == 0);
+			Assert.True (r.Subviews.IndexOf (v3) == 2);
+
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v2) == 1);
+			Assert.True (r.TabIndexes.IndexOf (v3) == 2);
+		}
+
+		[Fact]
+		public void SendSubviewToBack_Subviews_vs_TabIndexes ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			r.SendSubviewToBack (v3);
+			Assert.True (r.Subviews.IndexOf (v1) == 1);
+			Assert.True (r.Subviews.IndexOf (v2) == 2);
+			Assert.True (r.Subviews.IndexOf (v3) == 0);
+
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v2) == 1);
+			Assert.True (r.TabIndexes.IndexOf (v3) == 2);
+		}
+
+		[Fact]
+		public void SendSubviewBackwards_Subviews_vs_TabIndexes ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			r.SendSubviewBackwards (v3);
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.Subviews.IndexOf (v2) == 2);
+			Assert.True (r.Subviews.IndexOf (v3) == 1);
+
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v2) == 1);
+			Assert.True (r.TabIndexes.IndexOf (v3) == 2);
+		}
+
+		[Fact]
+		public void TabIndex_Set_CanFocus_ValidValues ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			v1.TabIndex = 1;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 1);
+
+			v1.TabIndex = 2;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 2);
+		}
+
+		[Fact]
+		public void TabIndex_Set_CanFocus_HigherValues ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			v1.TabIndex = 3;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 2);
+		}
+
+		[Fact]
+		public void TabIndex_Set_CanFocus_LowerValues ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			v1.TabIndex = -1;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+		}
+
+		[Fact]
+		public void TabIndex_Set_CanFocus_False ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			v1.CanFocus = false;
+			v1.TabIndex = 0;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 0);
+			Assert.Equal (-1, v1.TabIndex);
+		}
+
+		[Fact]
+		public void TabIndex_Set_CanFocus_False_To_True ()
+		{
+			var r = new View ();
+			var v1 = new View ();
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			v1.CanFocus = true;
+			v1.TabIndex = 1;
+			Assert.True (r.Subviews.IndexOf (v1) == 0);
+			Assert.True (r.TabIndexes.IndexOf (v1) == 1);
+		}
+
+		[Fact]
+		public void TabStop_And_CanFocus_Are_All_True ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true };
+			var v2 = new View () { CanFocus = true };
+			var v3 = new View () { CanFocus = true };
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.True (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.True (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.True (v3.HasFocus);
+		}
+
+		[Fact]
+		public void TabStop_Are_All_True_And_CanFocus_Are_All_False ()
+		{
+			var r = new View ();
+			var v1 = new View ();
+			var v2 = new View ();
+			var v3 = new View ();
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+		}
+
+		[Fact]
+		public void TabStop_Are_All_False_And_CanFocus_Are_All_True ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true, TabStop = false };
+			var v2 = new View () { CanFocus = true, TabStop = false };
+			var v3 = new View () { CanFocus = true, TabStop = false };
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+		}
+
+		[Fact]
+		public void TabStop_And_CanFocus_Mixed_And_BothFalse ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true, TabStop = false };
+			var v2 = new View () { CanFocus = false, TabStop = true };
+			var v3 = new View () { CanFocus = false, TabStop = false };
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+		}
+
+		[Fact]
+		public void TabStop_All_True_And_Changing_CanFocus_Later ()
+		{
+			var r = new View ();
+			var v1 = new View ();
+			var v2 = new View ();
+			var v3 = new View ();
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+
+			v1.CanFocus = true;
+			r.FocusNext ();
+			Assert.True (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			v2.CanFocus = true;
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.True (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			v3.CanFocus = true;
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.True (v3.HasFocus);
+		}
+
+		[Fact]
+		public void TabStop_All_False_And_All_True_And_Changing_TabStop_Later ()
+		{
+			var r = new View ();
+			var v1 = new View () { CanFocus = true, TabStop = false };
+			var v2 = new View () { CanFocus = true, TabStop = false };
+			var v3 = new View () { CanFocus = true, TabStop = false };
+
+			r.Add (v1, v2, v3);
+
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+
+			v1.TabStop = true;
+			r.FocusNext ();
+			Assert.True (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			v2.TabStop = true;
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.True (v2.HasFocus);
+			Assert.False (v3.HasFocus);
+			v3.TabStop = true;
+			r.FocusNext ();
+			Assert.False (v1.HasFocus);
+			Assert.False (v2.HasFocus);
+			Assert.True (v3.HasFocus);
+		}
 	}
 }