diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 47481724e..d22f9f941 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -190,6 +190,63 @@ namespace Terminal.Gui { // to make the same mistakes our users make when they poke at the Subviews. internal IList InternalSubviews => subviews ?? empty; + // This is null, and allocated on demand. + List tabIndexes; + + /// + /// This returns a tab index list of the subviews contained by this view. + /// + /// The tabIndexes. + public IList TabIndexes => tabIndexes == null ? empty : tabIndexes.AsReadOnly (); + + int tabIndex = -1; + + /// + /// Indicates the index of the current from the list. + /// + 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; + + /// + /// This only be true if the is also true and the focus can be avoided by setting this to false + /// + public bool TabStop { + get { return tabStop; } + set { + if (tabStop == value) { + return; + } + tabStop = CanFocus && value; + } + } + + /// + 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. @@ -548,12 +605,20 @@ namespace Terminal.Gui { { if (view == null) return; - if (subviews == null) + if (subviews == null) { subviews = new List (); + } + if (tabIndexes == null) { + tabIndexes = new List (); + } subviews.Add (view); + tabIndexes.Add (view); view.container = this; - if (view.CanFocus) + if (view.CanFocus) { CanFocus = true; + view.tabIndex = tabIndexes.IndexOf (view); + } + SetNeedsLayout (); SetNeedsDisplay (); } @@ -583,6 +648,7 @@ namespace Terminal.Gui { while (subviews.Count > 0) { Remove (subviews [0]); + Remove (tabIndexes [0]); } } @@ -600,8 +666,9 @@ namespace Terminal.Gui { SetNeedsDisplay (); var touched = view.Frame; subviews.Remove (view); + tabIndexes.Remove (view); view.container = null; - + view.tabIndex = -1; if (subviews.Count < 1) this.CanFocus = false; @@ -917,8 +984,8 @@ namespace Terminal.Gui { // Remove focus down the chain of subviews if focus is removed if (!value && focused != null) { - focused.OnLeave (focused); - focused.hasFocus = false; + focused.OnLeave (view); + focused.SetHasFocus (false, view); focused = null; } } @@ -1135,8 +1202,9 @@ namespace Terminal.Gui { if (focused != null) focused.SetHasFocus (false, view); + var f = focused; focused = view; - focused.SetHasFocus (true, view); + focused.SetHasFocus (true, f); focused.EnsureFocus (); // Send focus upwards @@ -1270,13 +1338,13 @@ namespace Terminal.Gui { /// 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; } @@ -1288,16 +1356,16 @@ namespace Terminal.Gui { /// 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; } @@ -1311,7 +1379,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) { @@ -1319,9 +1387,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 ()) @@ -1329,10 +1397,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); @@ -1340,7 +1408,7 @@ namespace Terminal.Gui { } } if (focused != null) { - focused.SetHasFocus (false, focused); + focused.SetHasFocus (false, this); focused = null; } return false; @@ -1353,17 +1421,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 ()) @@ -1371,10 +1439,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); diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index 4904ff684..4a2fa475f 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -135,5 +135,350 @@ namespace Terminal.Gui { sub2.Width = Dim.Width (sub2); Assert.Throws (() => root.LayoutSubviews ()); } + + [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); + } } }