diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index e68b3c7d3..094ed5a01 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -850,18 +850,18 @@ public partial class View // Focus and cross-view navigation management (TabStop SetNeedsDisplay (); } - private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) + private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - if (newHasFocus && focusedVew?.Focused is null) + if (newHasFocus && focusedView?.Focused is null) { - Application.Navigation?.SetFocused (focusedVew); + Application.Navigation?.SetFocused (focusedView); } // Call the virtual method - OnHasFocusChanged (newHasFocus, previousFocusedView, focusedVew); + OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); // Raise the event - var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedVew); + var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedView); HasFocusChanged?.Invoke (this, args); } @@ -876,8 +876,8 @@ public partial class View // Focus and cross-view navigation management (TabStop /// /// The new value of . /// - /// The view that is now focused. May be - protected virtual void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedVew) { } + /// The view that is now focused. May be + protected virtual void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { } /// Raised after has changed. /// diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index fbaaf4b23..9005a7ecd 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -962,7 +962,7 @@ public class ComboBox : View, IDesignable return true; } - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView) { if (newHasFocus) { diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/Tab.cs index eb42a59b9..be493ecf0 100644 --- a/Terminal.Gui/Views/Tab.cs +++ b/Terminal.Gui/Views/Tab.cs @@ -1,9 +1,10 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// A single tab in a . public class Tab : View { - private string _displayText; + private string? _displayText; /// Creates a new unnamed tab with no controls inside. public Tab () @@ -27,5 +28,5 @@ public class Tab : View /// The control to display when the tab is selected. /// - public View View { get; set; } + public View? View { get; set; } } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 5ef35e5c4..82231d918 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; - +#nullable enable namespace Terminal.Gui; /// Control that hosts multiple sub views, presenting a single one at once. @@ -19,8 +18,8 @@ public class TabView : View /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. private readonly TabRowView _tabsBar; - private Tab _selectedTab; - private TabToRender [] _tabLocations; + private Tab? _selectedTab; + private TabToRender []? _tabLocations; private int _tabScrollOffset; /// Initializes a class. @@ -48,7 +47,7 @@ public class TabView : View () => { TabScrollOffset = 0; - SelectedTab = Tabs.FirstOrDefault (); + SelectedTab = Tabs.FirstOrDefault ()!; return true; } @@ -59,7 +58,7 @@ public class TabView : View () => { TabScrollOffset = Tabs.Count - 1; - SelectedTab = Tabs.LastOrDefault (); + SelectedTab = Tabs.LastOrDefault()!; return true; } @@ -69,7 +68,7 @@ public class TabView : View Command.PageDown, () => { - TabScrollOffset += _tabLocations.Length; + TabScrollOffset += _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -80,7 +79,7 @@ public class TabView : View Command.PageUp, () => { - TabScrollOffset -= _tabLocations.Length; + TabScrollOffset -= _tabLocations!.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; @@ -104,19 +103,20 @@ public class TabView : View /// The currently selected member of chosen by the user. /// - public Tab SelectedTab + public Tab? SelectedTab { get => _selectedTab; set { UnSetCurrentTabs (); - Tab old = _selectedTab; + Tab? old = _selectedTab; if (_selectedTab is { }) { if (_selectedTab.View is { }) { + _selectedTab.View.CanFocusChanged -= ContentViewCanFocus!; // remove old content _contentView.Remove (_selectedTab.View); } @@ -124,33 +124,36 @@ public class TabView : View _selectedTab = value; - if (value is { }) + // add new content + if (_selectedTab?.View != null) { - // add new content - if (_selectedTab.View is { }) - { - _contentView.Add (_selectedTab.View); - // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; - } + _selectedTab.View.CanFocusChanged += ContentViewCanFocus!; + _contentView.Add (_selectedTab.View); + // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; } - _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + ContentViewCanFocus (null!, null!); EnsureSelectedTabIsVisible (); - if (old != value) + if (old != _selectedTab) { if (old?.HasFocus == true) { SelectedTab?.SetFocus (); } - OnSelectedTabChanged (old, value); + OnSelectedTabChanged (old!, _selectedTab!); } SetNeedsLayout (); } } + private void ContentViewCanFocus (object sender, EventArgs eventArgs) + { + _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + } + private TabStyle _style = new (); /// Render choices for how to display tabs. After making changes, call . @@ -289,6 +292,19 @@ public class TabView : View /// The valid for the given value. public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) + { + if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this) + { + SelectedTab?.SetFocus (); + + return; + } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); + } + /// protected override bool OnDrawingContent (Rectangle viewport) { @@ -313,7 +329,7 @@ public class TabView : View /// tab's hosted if appropriate. /// /// - public void RemoveTab (Tab tab) + public void RemoveTab (Tab? tab) { if (tab is null || !_tabs.Contains (tab)) { @@ -346,7 +362,7 @@ public class TabView : View } /// Event for when changes. - public event EventHandler SelectedTabChanged; + public event EventHandler? SelectedTabChanged; /// /// Changes the by the given . Positive for right, negative for @@ -395,7 +411,7 @@ public class TabView : View /// Event fired when a is clicked. Can be used to cancel navigation, show context menu (e.g. on /// right click) etc. /// - public event EventHandler TabClicked; + public event EventHandler? TabClicked; /// Disposes the control and all . /// @@ -428,7 +444,7 @@ public class TabView : View UnSetCurrentTabs (); var i = 1; - View prevTab = null; + View? prevTab = null; // Starting at the first or scrolled to tab foreach (Tab tab in Tabs.Skip (TabScrollOffset)) @@ -463,9 +479,9 @@ public class TabView : View if (maxWidth == 0) { tab.Visible = true; - tab.MouseClick += Tab_MouseClick; + tab.MouseClick += Tab_MouseClick!; - yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0); + yield return new TabToRender (tab, string.Empty, Equals (SelectedTab, tab)); break; } @@ -489,9 +505,9 @@ public class TabView : View // there is enough space! tab.Visible = true; - tab.MouseClick += Tab_MouseClick; + tab.MouseClick += Tab_MouseClick!; - yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth); + yield return new TabToRender (tab, text, Equals (SelectedTab, tab)); i += tabTextWidth + 1; } @@ -530,7 +546,7 @@ public class TabView : View { foreach (TabToRender tabToRender in _tabLocations) { - tabToRender.Tab.MouseClick -= Tab_MouseClick; + tabToRender.Tab.MouseClick -= Tab_MouseClick!; tabToRender.Tab.Visible = false; } @@ -565,7 +581,7 @@ public class TabView : View Visible = false, Text = Glyphs.RightArrow.ToString () }; - _rightScrollIndicator.MouseClick += _host.Tab_MouseClick; + _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; _leftScrollIndicator = new View { @@ -575,14 +591,14 @@ public class TabView : View Visible = false, Text = Glyphs.LeftArrow.ToString () }; - _leftScrollIndicator.MouseClick += _host.Tab_MouseClick; + _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!; Add (_rightScrollIndicator, _leftScrollIndicator); } protected override bool OnMouseEvent (MouseEventArgs me) { - Tab hit = me.View is Tab ? (Tab)me.View : null; + Tab? hit = me.View as Tab; if (me.IsSingleClicked) { @@ -652,14 +668,20 @@ public class TabView : View { _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - RenderTabLine (); - RenderUnderline (); SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); return true; } + /// + protected override bool OnDrawingSubviews (Rectangle viewport) + { + RenderTabLine (); + + return true; + } + protected override void OnDrawComplete () { if (_host._tabLocations is null) @@ -1190,10 +1212,10 @@ public class TabView : View } tab.LineCanvas.Merge (lc); - tab.DrawAdornments (); - } + tab.RenderLineCanvas (); - return; + RenderUnderline (); + } } private int GetUnderlineYPosition () @@ -1209,21 +1231,15 @@ public class TabView : View /// Renders the line with the tab names in it. private void RenderTabLine () { - TabToRender [] tabLocations = _host._tabLocations; - int y; + TabToRender []? tabLocations = _host._tabLocations; - if (_host.Style.TabsOnBottom) + if (tabLocations is null) { - y = 1; - } - else - { - y = _host.Style.ShowTopLine ? 1 : 0; + return; } - View selected = null; + View? selected = null; int topLine = _host.Style.ShowTopLine ? 1 : 0; - int width = Viewport.Width; foreach (TabToRender toRender in tabLocations) { @@ -1257,7 +1273,7 @@ public class TabView : View tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } - tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1); + tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } else { @@ -1272,7 +1288,7 @@ public class TabView : View tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } - tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1); + tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } tab.Text = toRender.TextToRender; @@ -1317,7 +1333,7 @@ public class TabView : View { int y = GetUnderlineYPosition (); - TabToRender selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected); + TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected); if (selected is null) { @@ -1363,17 +1379,15 @@ public class TabView : View } } - private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } + private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } } private class TabToRender { - public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width) + public TabToRender (Tab tab, string textToRender, bool isSelected) { - X = x; Tab = tab; IsSelected = isSelected; - Width = width; TextToRender = textToRender; } @@ -1383,7 +1397,5 @@ public class TabView : View public Tab Tab { get; } public string TextToRender { get; } - public int Width { get; } - public int X { get; set; } } } diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs index 45889f3e8..d7e181eae 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -111,7 +111,7 @@ public class ASCIICustomButtonTest : Scenario Add (_border, _fill, title); } - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView) { if (newHasFocus) { diff --git a/UnitTests/Views/ScrollViewTests.cs b/UnitTests/Views/ScrollViewTests.cs index 325a09d34..6122f0e64 100644 --- a/UnitTests/Views/ScrollViewTests.cs +++ b/UnitTests/Views/ScrollViewTests.cs @@ -1134,7 +1134,7 @@ public class ScrollViewTests (ITestOutputHelper output) CanFocus = true; } - protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedVew) + protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView) { if (newHasFocus) { diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index c470020e6..dc66cc298 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -103,7 +103,7 @@ public class TabViewTests (ITestOutputHelper output) Application.Shutdown (); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [AutoInitShutdown] public void MouseClick_ChangesTab () { @@ -188,7 +188,7 @@ public class TabViewTests (ITestOutputHelper output) top.Dispose (); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [AutoInitShutdown] public void MouseClick_Right_Left_Arrows_ChangesTab () { @@ -274,7 +274,7 @@ public class TabViewTests (ITestOutputHelper output) top.Dispose (); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [AutoInitShutdown] public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () { @@ -369,7 +369,7 @@ public class TabViewTests (ITestOutputHelper output) top.Dispose (); } - [Fact (Skip="#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [AutoInitShutdown] public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () { @@ -429,7 +429,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (btn, top.MostFocused); - // Add a focusable subview to Selected Tab + // Add a focusable subview to Selected Tab View which is a Label with CanFocus as false var btnSubView = new View () { Id = "btnSubView", @@ -438,9 +438,17 @@ public class TabViewTests (ITestOutputHelper output) }; tv.SelectedTab.View.Add (btnSubView); + Assert.False (tv.SelectedTab.View.CanFocus); + // Press cursor up. Should focus the subview in the selected tab. Application.RaiseKeyDownEvent (Key.CursorUp); Assert.Equal (tab2, tv.SelectedTab); + Assert.NotEqual (btnSubView, top.MostFocused); + Assert.Equal (tab2, top.MostFocused); + + tv.SelectedTab.View.CanFocus = true; + Application.RaiseKeyDownEvent (Key.CursorDown); + Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (btnSubView, top.MostFocused); Application.RaiseKeyDownEvent (Key.CursorUp); @@ -451,24 +459,18 @@ public class TabViewTests (ITestOutputHelper output) Application.RaiseKeyDownEvent (Key.CursorDown); Assert.Equal (btn, top.MostFocused); - // Press the cursor down key again will focus next view in the toplevel, whic is the TabView + // Press the cursor down key again will focus next view in the toplevel, which is the TabView Application.RaiseKeyDownEvent (Key.CursorDown); Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (tv, top.Focused); - Assert.Equal (tab1, tv.MostFocused); + // Due to the RestoreFocus method prioritize the _previouslyFocused, so btnSubView will be focused again + Assert.Equal (btnSubView, tv.MostFocused); - // Press the cursor down key to focus the selected tab view hosting again - Application.RaiseKeyDownEvent (Key.CursorDown); - Assert.Equal (tab2, tv.SelectedTab); - Assert.Equal (btnSubView, top.MostFocused); - - // Press the cursor up key to focus the selected tab + // Press the cursor up key to focus the selected tab which it's the only way to do that Application.RaiseKeyDownEvent (Key.CursorUp); - Application.Refresh (); - - // Is the selected tab focused Assert.Equal (tab2, tv.SelectedTab); Assert.Equal (tv, top.Focused); + Assert.Equal (tab2, top.Focused.MostFocused); Assert.Equal (tv.MostFocused, top.Focused.MostFocused); // Press the cursor left key to select the previous tab @@ -479,6 +481,7 @@ public class TabViewTests (ITestOutputHelper output) Assert.Equal (tab1, tv.SelectedTab); Assert.Equal (tv, top.Focused); Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + Assert.Equal (tab1, top.Focused.MostFocused); // Press the end key to select the last tab Application.RaiseKeyDownEvent (Key.End); @@ -595,7 +598,7 @@ public class TabViewTests (ITestOutputHelper output) Application.Shutdown (); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () { @@ -643,7 +646,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames () { @@ -735,7 +738,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () { @@ -783,7 +786,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () { @@ -875,7 +878,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () { @@ -919,7 +922,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () { @@ -1009,7 +1012,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () { @@ -1050,7 +1053,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () { @@ -1098,7 +1101,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () { @@ -1174,7 +1177,7 @@ public class TabViewTests (ITestOutputHelper output) ); } - [Fact (Skip = "#3789 Broke. The right way to fix is to refactor TabView to separate Layout and Draw")] + [Fact] [SetupFakeDriver] public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () {