diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/Tab.cs index 0c0be4a2c..975d535d5 100644 --- a/Terminal.Gui/Views/Tab.cs +++ b/Terminal.Gui/Views/Tab.cs @@ -1,25 +1,25 @@ namespace Terminal.Gui; /// -/// A single tab in a +/// A single tab in a . /// -public class Tab { - private string text; +public class Tab : View { + private string _displayText; /// - /// The text to display in a + /// The text to display in a . /// /// - public string Text { get => text ?? "Unamed"; set => text = value; } + public string DisplayText { get => _displayText ?? "Unamed"; set => _displayText = value; } /// - /// The control to display when the tab is selected + /// The control to display when the tab is selected. /// /// public View View { get; set; } /// - /// Creates a new unamed tab with no controls inside + /// Creates a new unamed tab with no controls inside. /// public Tab () { @@ -27,13 +27,16 @@ public class Tab { } /// - /// Creates a new tab with the given text hosting a view + /// Creates a new tab with the given text hosting a view. /// - /// - /// - public Tab (string text, View view) + /// The real text. + /// The hosted view. + public Tab (string displayText, View view) { - this.Text = text; + this.DisplayText = displayText; this.View = view; + BorderStyle = LineStyle.Rounded; + CanFocus = true; + Visible = false; } } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 86a558b82..4cb2beef4 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -7,40 +7,45 @@ using System.Linq; namespace Terminal.Gui { /// - /// Control that hosts multiple sub views, presenting a single one at once + /// Control that hosts multiple sub views, presenting a single one at once. /// public class TabView : View { - private Tab selectedTab; + private Tab _selectedTab; /// - /// The default to set on new controls + /// The default to set on new controls. /// public const uint DefaultMaxTabTextWidth = 30; /// - /// This sub view is the 2 or 3 line control that represents the actual tabs themselves + /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. /// - TabRowView tabsBar; + TabRowView _tabsBar; /// /// This sub view is the main client area of the current tab. It hosts the - /// of the tab, the + /// of the tab, the . /// - View contentView; - private List tabs = new List (); + View _contentView; + private List _tabs = new List (); /// - /// All tabs currently hosted by the control + /// All tabs currently hosted by the control. /// /// - public IReadOnlyCollection Tabs { get => tabs.AsReadOnly (); } + public IReadOnlyCollection Tabs { get => _tabs.AsReadOnly (); } /// /// When there are too many tabs to render, this indicates the first /// tab to render on the screen. /// /// - public int TabScrollOffset { get; set; } + public int TabScrollOffset { + get => _tabScrollOffset; + set { + _tabScrollOffset = EnsureValidScrollOffsets (value); + } + } /// /// The maximum number of characters to render in a Tab header. This prevents one long tab @@ -49,7 +54,7 @@ namespace Terminal.Gui { public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth; /// - /// Event for when changes + /// Event for when changes. /// public event EventHandler SelectedTabChanged; @@ -60,44 +65,45 @@ namespace Terminal.Gui { public event EventHandler TabClicked; /// - /// The currently selected member of chosen by the user + /// The currently selected member of chosen by the user. /// /// public Tab SelectedTab { - get => selectedTab; + get => _selectedTab; set { + UnSetCurrentTabs (); - var old = selectedTab; + var old = _selectedTab; - if (selectedTab != null) { - - if (selectedTab.View != null) { + if (_selectedTab != null) { + if (_selectedTab.View != null) { // remove old content - contentView.Remove (selectedTab.View); + _contentView.Remove (_selectedTab.View); } } - selectedTab = value; + _selectedTab = value; if (value != null) { - // add new content - if (selectedTab.View != null) { - contentView.Add (selectedTab.View); + if (_selectedTab.View != null) { + _contentView.Add (_selectedTab.View); } } EnsureSelectedTabIsVisible (); if (old != value) { + if (old?.HasFocus == true) { + SelectedTab.SetFocus (); + } OnSelectedTabChanged (old, value); } - } } /// - /// Render choices for how to display tabs. After making changes, call + /// Render choices for how to display tabs. After making changes, call . /// /// public TabStyle Style { get; set; } = new TabStyle (); @@ -108,101 +114,112 @@ namespace Terminal.Gui { public TabView () : base () { CanFocus = true; - contentView = new View (); - tabsBar = new TabRowView (this); + _tabsBar = new TabRowView (this); + _contentView = new View (); ApplyStyleChanges (); - base.Add (tabsBar); - base.Add (contentView); + base.Add (_tabsBar); + base.Add (_contentView); // Things this view knows how to do AddCommand (Command.Left, () => { SwitchTabBy (-1); return true; }); AddCommand (Command.Right, () => { SwitchTabBy (1); return true; }); - AddCommand (Command.LeftHome, () => { SelectedTab = Tabs.FirstOrDefault (); return true; }); - AddCommand (Command.RightEnd, () => { SelectedTab = Tabs.LastOrDefault (); return true; }); + AddCommand (Command.LeftHome, () => { TabScrollOffset = 0; SelectedTab = Tabs.FirstOrDefault (); return true; }); + AddCommand (Command.RightEnd, () => { TabScrollOffset = Tabs.Count - 1; SelectedTab = Tabs.LastOrDefault (); return true; }); + AddCommand (Command.NextView, () => { _contentView.SetFocus (); return true; }); + AddCommand (Command.PreviousView, () => { SuperView?.FocusPrev (); return true; }); + AddCommand (Command.PageDown, () => { TabScrollOffset += _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; }); + AddCommand (Command.PageUp, () => { TabScrollOffset -= _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; }); // Default keybindings for this view AddKeyBinding (Key.CursorLeft, Command.Left); AddKeyBinding (Key.CursorRight, Command.Right); AddKeyBinding (Key.Home, Command.LeftHome); AddKeyBinding (Key.End, Command.RightEnd); + AddKeyBinding (Key.CursorDown, Command.NextView); + AddKeyBinding (Key.CursorUp, Command.PreviousView); + AddKeyBinding (Key.PageDown, Command.PageDown); + AddKeyBinding (Key.PageUp, Command.PageUp); } /// /// Updates the control to use the latest state settings in . /// This can change the size of the client area of the tab (for rendering the /// selected tab's content). This method includes a call - /// to + /// to . /// public void ApplyStyleChanges () { - contentView.X = Style.ShowBorder ? 1 : 0; - contentView.Width = Dim.Fill (Style.ShowBorder ? 1 : 0); + _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; + _contentView.Width = Dim.Fill (); if (Style.TabsOnBottom) { // Tabs are along the bottom so just dodge the border - contentView.Y = Style.ShowBorder ? 1 : 0; + if (Style.ShowBorder) { + _contentView.Border.Thickness = new Thickness (1, 1, 1, 0); + } - // Fill client area leaving space at bottom for tabs - contentView.Height = Dim.Fill (GetTabHeight (false)); + _contentView.Y = 0; var tabHeight = GetTabHeight (false); - tabsBar.Height = tabHeight; - tabsBar.Y = Pos.Percent (100) - tabHeight; + // Fill client area leaving space at bottom for tabs + _contentView.Height = Dim.Fill (tabHeight); + + _tabsBar.Height = tabHeight; + + _tabsBar.Y = Pos.Bottom (_contentView); } else { // Tabs are along the top + if (Style.ShowBorder) { + _contentView.Border.Thickness = new Thickness (1, 0, 1, 1); + } + + _tabsBar.Y = 0; var tabHeight = GetTabHeight (true); //move content down to make space for tabs - contentView.Y = tabHeight; + _contentView.Y = Pos.Bottom (_tabsBar); // Fill client area leaving space at bottom for border - contentView.Height = Dim.Fill (Style.ShowBorder ? 1 : 0); + _contentView.Height = Dim.Fill (); // The top tab should be 2 or 3 rows high and on the top - tabsBar.Height = tabHeight; + _tabsBar.Height = tabHeight; // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 - tabsBar.Y = Pos.Percent (0); } LayoutSubviews (); SetNeedsDisplay (); } - /// public override void OnDrawContent (Rect contentArea) { - Move (0, 0); Driver.SetAttribute (GetNormalColor ()); - if (Style.ShowBorder) { - - // How much space do we need to leave at the bottom to show the tabs - int spaceAtBottom = Math.Max (0, GetTabHeight (false) - 1); - int startAtY = Math.Max (0, GetTabHeight (true) - 1); - - Border.DrawFrame (new Rect (0, startAtY, Bounds.Width, - Math.Max (Bounds.Height - spaceAtBottom - startAtY, 0)), false); - } - if (Tabs.Any ()) { - tabsBar.OnDrawContent (contentArea); - contentView.SetNeedsDisplay (); - var savedClip = contentView.ClipToBounds (); - contentView.Draw (); + var savedClip = ClipToBounds (); + _tabsBar.OnDrawContent (contentArea); + _contentView.SetNeedsDisplay (); + _contentView.Draw (); Driver.Clip = savedClip; } } + /// + public override void OnDrawContentComplete (Rect contentArea) + { + _tabsBar.OnDrawContentComplete (contentArea); + } + /// - /// Disposes the control and all + /// Disposes the control and all . /// /// protected override void Dispose (bool disposing) @@ -216,12 +233,11 @@ namespace Terminal.Gui { if (!Equals (SelectedTab, tab)) { tab.View?.Dispose (); } - } } /// - /// Raises the event + /// Raises the event. /// protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) { @@ -232,7 +248,7 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - if (HasFocus && CanFocus && Focused == tabsBar) { + if (HasFocus && CanFocus && Focused == _tabsBar) { var result = InvokeKeybindings (keyEvent); if (result != null) return (bool)result; @@ -242,9 +258,9 @@ namespace Terminal.Gui { } /// - /// Changes the by the given . + /// Changes the by the given . /// Positive for right, negative for left. If no tab is currently selected then - /// the first tab will become selected + /// the first tab will become selected. /// /// public void SwitchTabBy (int amount) @@ -271,23 +287,25 @@ namespace Terminal.Gui { var newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1)); - SelectedTab = tabs [newIdx]; + SelectedTab = _tabs [newIdx]; SetNeedsDisplay (); EnsureSelectedTabIsVisible (); } /// - /// Updates to be a valid index of + /// Updates to be a valid index of . /// - /// Changes will not be immediately visible in the display until you call - public void EnsureValidScrollOffsets () + /// The value to validate. + /// Changes will not be immediately visible in the display until you call . + /// The valid for the given value. + public int EnsureValidScrollOffsets (int value) { - TabScrollOffset = Math.Max (Math.Min (TabScrollOffset, Tabs.Count - 1), 0); + return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } /// - /// Updates to ensure that is visible + /// Updates to ensure that is visible. /// public void EnsureSelectedTabIsVisible () { @@ -309,7 +327,7 @@ namespace Terminal.Gui { /// and you ask for ). /// /// True to measure the space required at the top of the control, - /// false to measure space at the bottom + /// false to measure space at the bottom.. /// private int GetTabHeight (bool top) { @@ -324,61 +342,104 @@ namespace Terminal.Gui { return Style.ShowTopLine ? 3 : 2; } + private TabToRender [] _tabLocations; + private int _tabScrollOffset; + /// - /// Returns which tabs to render at each x location + /// Returns which tabs to render at each x location. /// /// private IEnumerable CalculateViewport (Rect bounds) { + UnSetCurrentTabs (); + int i = 1; + View prevTab = null; // Starting at the first or scrolled to tab foreach (var tab in Tabs.Skip (TabScrollOffset)) { - // while there is space for the tab - var tabTextWidth = tab.Text.EnumerateRunes ().Sum (c => c.GetColumns ()); + if (prevTab != null) { + tab.X = Pos.Right (prevTab); + } else { + tab.X = 0; + } + tab.Y = 0; - string text = tab.Text; + // while there is space for the tab + var tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); + + string text = tab.DisplayText; // The maximum number of characters to use for the tab name as specified // by the user (MaxTabTextWidth). But not more than the width of the view // or we won't even be able to render a single tab! var maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); + prevTab = tab; + + tab.Width = 2; + tab.Height = Style.ShowTopLine ? 3 : 2; + // if tab view is width <= 3 don't render any tabs if (maxWidth == 0) { + tab.Visible = true; + tab.MouseClick += Tab_MouseClick; yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0); break; } if (tabTextWidth > maxWidth) { - text = tab.Text.Substring (0, (int)maxWidth); + text = tab.DisplayText.Substring (0, (int)maxWidth); tabTextWidth = (int)maxWidth; } + tab.Width = Math.Max (tabTextWidth + 2, 1); + tab.Height = Style.ShowTopLine ? 3 : 2; + // if there is not enough space for this tab if (i + tabTextWidth >= bounds.Width) { + tab.Visible = false; break; } // there is enough space! + tab.Visible = true; + tab.MouseClick += Tab_MouseClick; yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth); i += tabTextWidth + 1; } } + private void UnSetCurrentTabs () + { + if (_tabLocations != null) { + foreach (var tabToRender in _tabLocations) { + tabToRender.Tab.MouseClick -= Tab_MouseClick; + tabToRender.Tab.Visible = false; + } + _tabLocations = null; + } + } + + private void Tab_MouseClick (object sender, MouseEventEventArgs e) + { + e.Handled = _tabsBar.MouseEvent (e.MouseEvent); + } + /// - /// Adds the given to + /// Adds the given to . /// /// - /// True to make the newly added Tab the + /// True to make the newly added Tab the . public void AddTab (Tab tab, bool andSelect) { - if (tabs.Contains (tab)) { + if (_tabs.Contains (tab)) { return; } - tabs.Add (tab); + _tabs.Add (tab); + _tabsBar.Add (tab); if (SelectedTab == null || andSelect) { SelectedTab = tab; @@ -399,14 +460,14 @@ namespace Terminal.Gui { /// public void RemoveTab (Tab tab) { - if (tab == null || !tabs.Contains (tab)) { + if (tab == null || !_tabs.Contains (tab)) { return; } // what tab was selected before closing - var idx = tabs.IndexOf (tab); + var idx = _tabs.IndexOf (tab); - tabs.Remove (tab); + _tabs.Remove (tab); // if the currently selected tab is no longer a member of Tabs if (SelectedTab == null || !Tabs.Contains (SelectedTab)) { @@ -430,7 +491,7 @@ namespace Terminal.Gui { public Tab Tab { get; set; } /// - /// True if the tab that is being rendered is the selected one + /// True if the tab that is being rendered is the selected one. /// /// public bool IsSelected { get; set; } @@ -448,16 +509,24 @@ namespace Terminal.Gui { } private class TabRowView : View { - - readonly TabView host; + readonly TabView _host; + View _rightScrollIndicator; + View _leftScrollIndicator; public TabRowView (TabView host) { - this.host = host; + this._host = host; CanFocus = true; Height = 1; Width = Dim.Fill (); + + _rightScrollIndicator = new View () { Id = "rightScrollIndicator", Width = 1, Height = 1, Visible = false, Text = CM.Glyphs.RightArrow.ToString () }; + _rightScrollIndicator.MouseClick += _host.Tab_MouseClick; + _leftScrollIndicator = new View () { Id = "leftScrollIndicator", Width = 1, Height = 1, Visible = false , Text = CM.Glyphs.LeftArrow.ToString ()}; + _leftScrollIndicator.MouseClick += _host.Tab_MouseClick; + + Add (_rightScrollIndicator, _leftScrollIndicator); } public override bool OnEnter (View view) @@ -468,195 +537,353 @@ namespace Terminal.Gui { public override void OnDrawContent (Rect contentArea) { - var tabLocations = host.CalculateViewport (Bounds).ToArray (); - var width = Bounds.Width; - Driver.SetAttribute (GetNormalColor ()); + _host._tabLocations = _host.CalculateViewport (Bounds).ToArray (); - if (host.Style.ShowTopLine) { - RenderOverline (tabLocations, width); - } + // clear any old text + Clear (); - RenderTabLine (tabLocations, width); + RenderTabLine (); - RenderUnderline (tabLocations, width); + RenderUnderline (); Driver.SetAttribute (GetNormalColor ()); } - /// - /// Renders the line of the tabs that does not adjoin the content - /// - /// - /// - private void RenderOverline (TabToRender [] tabLocations, int width) + public override void OnDrawContentComplete (Rect contentArea) { - // if tabs are on the bottom draw the side of the tab that doesn't border the content area at the bottom otherwise the top - int y = host.Style.TabsOnBottom ? 2 : 0; - - Move (0, y); - - var selected = tabLocations.FirstOrDefault (t => t.IsSelected); - - // Clear out everything - Driver.AddStr (new string (' ', width)); - - // Nothing is selected... odd but we are done - if (selected == null) { + if (_host._tabLocations == null) { return; } - Move (selected.X - 1, y); - Driver.AddRune (host.Style.TabsOnBottom ? CM.Glyphs.LLCorner : CM.Glyphs.ULCorner); + var tabLocations = _host._tabLocations; + var selectedTab = -1; - for (int i = 0; i < selected.Width; i++) { + for (int i = 0; i < tabLocations.Length; i++) { + View tab = tabLocations [i].Tab; + var vts = tab.BoundsToScreen (tab.Bounds); + LineCanvas lc = new LineCanvas (); + var selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; - if (selected.X + i > width) { - // we ran out of space horizontally - return; - } + if (tabLocations [i].IsSelected) { + selectedTab = i; - Driver.AddRune (CM.Glyphs.HLine); - } + if (i == 0 && _host.TabScrollOffset == 0) { + if (_host.Style.TabsOnBottom) { + // Upper left vertical line + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Vertical, tab.BorderStyle); + } else { + // Lower left vertical line + lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle); + } + } else if (i > 0 && i <= tabLocations.Length - 1) { + if (_host.Style.TabsOnBottom) { + // URCorner + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Horizontal, tab.BorderStyle); + } else { + // LRCorner + lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Horizontal, tab.BorderStyle); + } - // Add the end of the selected tab - Driver.AddRune (host.Style.TabsOnBottom ? CM.Glyphs.LRCorner : CM.Glyphs.URCorner); + if (_host.Style.ShowTopLine) { + if (_host.Style.TabsOnBottom) { + // Lower left tee + lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle); + } else { + // Upper left tee + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); + } + } + } - } - - /// - /// Renders the line with the tab names in it - /// - /// - /// - private void RenderTabLine (TabToRender [] tabLocations, int width) - { - int y; - - if (host.Style.TabsOnBottom) { - - y = 1; - } else { - y = host.Style.ShowTopLine ? 1 : 0; - } - - - // clear any old text - Move (0, y); - Driver.AddStr (new string (' ', width)); - - foreach (var toRender in tabLocations) { - - if (toRender.IsSelected) { - Move (toRender.X - 1, y); - Driver.AddRune (CM.Glyphs.VLine); - } - - Move (toRender.X, y); - - // if tab is the selected one and focus is inside this control - if (toRender.IsSelected && host.HasFocus) { - - if (host.Focused == this) { - - // if focus is the tab bar ourself then show that they can switch tabs - Driver.SetAttribute (ColorScheme.HotFocus); + if (i < tabLocations.Length - 1) { + if (_host.Style.ShowTopLine) { + if (_host.Style.TabsOnBottom) { + // Lower right tee + lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle); + } else { + // Upper right tee + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); + } + } + } + if (_host.Style.TabsOnBottom) { + //URCorner + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle); } else { + //LLCorner + lc.AddLine (new Point (vts.Right, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Bottom - selectedOffset), 1, Orientation.Horizontal, tab.BorderStyle); + } - // Focus is inside the tab - Driver.SetAttribute (ColorScheme.HotNormal); + } else if (selectedTab == -1) { + if (i == 0 && string.IsNullOrEmpty (tab.Text)) { + if (_host.Style.TabsOnBottom) { + if (_host.Style.ShowTopLine) { + // LLCorner + lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle); + } + // ULCorner + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle); + } else { + if (_host.Style.ShowTopLine) { + // ULCorner + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle); + } + // LLCorner + lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle); + } + } else if (i > 0) { + if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) { + // Upper left tee + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); + } + + // Lower left tee + lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle); + + } + } else if (i < tabLocations.Length - 1) { + if (_host.Style.ShowTopLine) { + // Upper right tee + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); + } + + if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) { + // Lower right tee + lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle); + } else { + // Upper right tee + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); } } - Driver.AddStr (toRender.TextToRender); - Driver.SetAttribute (GetNormalColor ()); - - if (toRender.IsSelected) { - Driver.AddRune (CM.Glyphs.VLine); + if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder) { + if (_host.Style.TabsOnBottom) { + // Upper left vertical line + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle); + } else { + // Lower left vertical line + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle); + } } + + if (i == tabLocations.Length - 1 && i != selectedTab) { + if (_host.Style.TabsOnBottom) { + // Upper right tee + lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle); + } else { + // Lower right tee + lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle); + lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle); + } + } + + if (i == tabLocations.Length - 1) { + var arrowOffset = 1; + var lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : _host.Style.TabsOnBottom ? 1 : 0; + var tabsBarVts = BoundsToScreen (Bounds); + var lineLength = tabsBarVts.Right - vts.Right; + // Right horizontal line + if (ShouldDrawRightScrollIndicator ()) { + if (lineLength - arrowOffset > 0) { + if (_host.Style.TabsOnBottom) { + lc.AddLine (new Point (vts.Right, vts.Y - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle); + } else { + lc.AddLine (new Point (vts.Right, vts.Bottom - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle); + } + } + } else { + if (_host.Style.TabsOnBottom) { + lc.AddLine (new Point (vts.Right, vts.Y - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle); + } else { + lc.AddLine (new Point (vts.Right, vts.Bottom - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle); + } + if (_host.Style.ShowBorder) { + if (_host.Style.TabsOnBottom) { + // More LRCorner + lc.AddLine (new Point (tabsBarVts.Right - 1, vts.Y - lastSelectedTab), -1, Orientation.Vertical, tab.BorderStyle); + } else { + // More URCorner + lc.AddLine (new Point (tabsBarVts.Right - 1, vts.Bottom - lastSelectedTab), 1, Orientation.Vertical, tab.BorderStyle); + } + } + } + } + + tab.LineCanvas.Merge (lc); + tab.OnRenderLineCanvas (); } } /// - /// Renders the line of the tab that adjoins the content of the tab + /// Renders the line with the tab names in it. /// - /// - /// - private void RenderUnderline (TabToRender [] tabLocations, int width) + private void RenderTabLine () + { + var tabLocations = _host._tabLocations; + int y; + + if (_host.Style.TabsOnBottom) { + + y = 1; + } else { + y = _host.Style.ShowTopLine ? 1 : 0; + } + + View selected = null; + var topLine = _host.Style.ShowTopLine ? 1 : 0; + var width = Bounds.Width; + + foreach (var toRender in tabLocations) { + var tab = toRender.Tab; + + if (toRender.IsSelected) { + selected = tab; + if (_host.Style.TabsOnBottom) { + tab.Border.Thickness = new Thickness (1, 0, 1, topLine); + tab.Margin.Thickness = new Thickness (0, 1, 0, 0); + } else { + tab.Border.Thickness = new Thickness (1, topLine, 1, 0); + tab.Margin.Thickness = new Thickness (0, 0, 0, topLine); + } + } else if (selected == null) { + if (_host.Style.TabsOnBottom) { + tab.Border.Thickness = new Thickness (1, 1, 0, topLine); + tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + } else { + tab.Border.Thickness = new Thickness (1, topLine, 0, 1); + tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + } + tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1); + } else { + if (_host.Style.TabsOnBottom) { + tab.Border.Thickness = new Thickness (0, 1, 1, topLine); + tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + } else { + tab.Border.Thickness = new Thickness (0, topLine, 1, 1); + tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + } + tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1); + } + + tab.Text = toRender.TextToRender; + + LayoutSubviews (); + + tab.OnDrawFrames (); + + var prevAttr = Driver.GetAttribute (); + + // if tab is the selected one and focus is inside this control + if (toRender.IsSelected && _host.HasFocus) { + + if (_host.Focused == this) { + + // if focus is the tab bar ourself then show that they can switch tabs + prevAttr = ColorScheme.HotFocus; + } else { + + // Focus is inside the tab + prevAttr = ColorScheme.HotNormal; + } + } + tab.TextFormatter.Draw (tab.BoundsToScreen (tab.Bounds), prevAttr, ColorScheme.HotNormal); + + tab.OnRenderLineCanvas (); + + Driver.SetAttribute (GetNormalColor ()); + } + } + + /// + /// Renders the line of the tab that adjoins the content of the tab. + /// + private void RenderUnderline () { int y = GetUnderlineYPosition (); - Move (0, y); - - // If host has no border then we need to draw the solid line first (then we draw gaps over the top) - if (!host.Style.ShowBorder) { - - for (int x = 0; x < width; x++) { - Driver.AddRune (CM.Glyphs.HLine); - } - - } - var selected = tabLocations.FirstOrDefault (t => t.IsSelected); + var selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected); if (selected == null) { return; } - Move (selected.X - 1, y); - - Driver.AddRune (selected.X == 1 ? CM.Glyphs.VLine : - (host.Style.TabsOnBottom ? CM.Glyphs.URCorner : CM.Glyphs.LRCorner)); - - Driver.AddStr (new string (' ', selected.Width)); - - Driver.AddRune (selected.X + selected.Width == width - 1 ? - CM.Glyphs.VLine : - (host.Style.TabsOnBottom ? CM.Glyphs.ULCorner : CM.Glyphs.LLCorner)); - // draw scroll indicators // if there are more tabs to the left not visible - if (host.TabScrollOffset > 0) { - Move (0, y); + if (_host.TabScrollOffset > 0) { + _leftScrollIndicator.X = 0; + _leftScrollIndicator .Y = y; // indicate that - Driver.AddRune (CM.Glyphs.LeftArrow); + _leftScrollIndicator.Visible = true; + // Ensures this is clicked instead of the first tab + BringSubviewToFront (_leftScrollIndicator); + _leftScrollIndicator.Draw (); + } else { + _leftScrollIndicator.Visible = false; } // if there are more tabs to the right not visible - if (ShouldDrawRightScrollIndicator (tabLocations)) { - Move (width - 1, y); + if (ShouldDrawRightScrollIndicator ()) { + _rightScrollIndicator.X = Bounds.Width - 1; + _rightScrollIndicator .Y = y; // indicate that - Driver.AddRune (CM.Glyphs.RightArrow); + _rightScrollIndicator.Visible = true; + // Ensures this is clicked instead of the last tab if under this + BringSubviewToFront (_rightScrollIndicator); + _rightScrollIndicator.Draw (); + } else { + _rightScrollIndicator.Visible = false; } } - private bool ShouldDrawRightScrollIndicator (TabToRender [] tabLocations) + private bool ShouldDrawRightScrollIndicator () { - return tabLocations.LastOrDefault ()?.Tab != host.Tabs.LastOrDefault (); + return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } private int GetUnderlineYPosition () { - if (host.Style.TabsOnBottom) { + if (_host.Style.TabsOnBottom) { return 0; } else { - return host.Style.ShowTopLine ? 2 : 1; + return _host.Style.ShowTopLine ? 2 : 1; } } public override bool MouseEvent (MouseEvent me) { - var hit = ScreenToTab (me.X, me.Y); + var hit = me.View is Tab ? (Tab)me.View : null; bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) || me.Flags.HasFlag (MouseFlags.Button2Clicked) || me.Flags.HasFlag (MouseFlags.Button3Clicked); if (isClick) { - host.OnTabClicked (new TabMouseEventArgs (hit, me)); + _host.OnTabClicked (new TabMouseEventArgs (hit, me)); // user canceled click if (me.Handled) { @@ -677,18 +904,23 @@ namespace Terminal.Gui { me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) || me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) { - var scrollIndicatorHit = ScreenToScrollIndicator (me.X, me.Y); + int scrollIndicatorHit = 0; + if (me.View != null && me.View.Id == "rightScrollIndicator") { + scrollIndicatorHit = 1; + } else if (me.View != null && me.View.Id == "leftScrollIndicator") { + scrollIndicatorHit = -1; + } if (scrollIndicatorHit != 0) { - host.SwitchTabBy (scrollIndicatorHit); + _host.SwitchTabBy (scrollIndicatorHit); SetNeedsDisplay (); return true; } if (hit != null) { - host.SelectedTab = hit; + _host.SelectedTab = hit; SetNeedsDisplay (); return true; } @@ -696,43 +928,6 @@ namespace Terminal.Gui { return false; } - - /// - /// Calculates whether scroll indicators are visible and if so whether the click - /// was on one of them. - /// - /// - /// - /// -1 for click in scroll left, 1 for scroll right or 0 for no hit - private int ScreenToScrollIndicator (int x, int y) - { - // scroll indicator is showing - if (host.TabScrollOffset > 0 && x == 0) { - - return y == GetUnderlineYPosition () ? -1 : 0; - } - - // scroll indicator is showing - if (x == Bounds.Width - 1 && ShouldDrawRightScrollIndicator (host.CalculateViewport (Bounds).ToArray ())) { - - return y == GetUnderlineYPosition () ? 1 : 0; - } - - return 0; - } - - /// - /// Translates the client coordinates of a click into a tab when the click is on top of a tab - /// - /// - /// - /// - public Tab ScreenToTab (int x, int y) - { - var tabs = host.CalculateViewport (Bounds); - - return tabs.LastOrDefault (t => x >= t.X && x < t.X + t.Width)?.Tab; - } } /// diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index c521d896a..9ddb1a109 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -198,7 +198,7 @@ namespace UICatalog.Scenarios { if (tab.UnsavedChanges) { - int result = MessageBox.Query ("Save Changes", $"Save changes to {tab.Text.TrimEnd ('*')}", "Yes", "No", "Cancel"); + int result = MessageBox.Query ("Save Changes", $"Save changes to {tab.DisplayText.TrimEnd ('*')}", "Yes", "No", "Cancel"); if (result == -1 || result == 2) { @@ -321,7 +321,7 @@ namespace UICatalog.Scenarios { } tab.File = new FileInfo (fd.Path); - tab.Text = fd.FileName; + tab.DisplayText = fd.FileName; tab.Save (); return true; @@ -357,16 +357,16 @@ namespace UICatalog.Scenarios { var areDiff = this.UnsavedChanges; if (areDiff) { - if (!this.Text.EndsWith ('*')) { + if (!this.DisplayText.EndsWith ('*')) { - this.Text = this.Text + '*'; + this.DisplayText = this.DisplayText + '*'; parent.SetNeedsDisplay (); } } else { - if (Text.EndsWith ('*')) { + if (DisplayText.EndsWith ('*')) { - Text = Text.TrimEnd ('*'); + DisplayText = DisplayText.TrimEnd ('*'); parent.SetNeedsDisplay (); } } @@ -392,7 +392,7 @@ namespace UICatalog.Scenarios { } public OpenedFile CloneTo(TabView other) { - var newTab = new OpenedFile (other, base.Text.ToString(), File); + var newTab = new OpenedFile (other, base.DisplayText.ToString(), File); other.AddTab (newTab, true); return newTab; } @@ -403,7 +403,7 @@ namespace UICatalog.Scenarios { System.IO.File.WriteAllText (File.FullName, newText); SavedText = newText; - Text = Text.TrimEnd ('*'); + DisplayText = DisplayText.TrimEnd ('*'); } } diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index 190487751..b45729afa 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using Terminal.Gui; -using static UICatalog.Scenario; namespace UICatalog.Scenarios { @@ -18,6 +12,7 @@ namespace UICatalog.Scenarios { MenuItem miShowTopLine; MenuItem miShowBorder; MenuItem miTabsOnBottom; + MenuItem miShowTabViewBorder; public override void Setup () { @@ -46,6 +41,10 @@ namespace UICatalog.Scenarios { miTabsOnBottom = new MenuItem ("_Tabs On Bottom", "", () => SetTabsOnBottom()){ Checked = false, CheckType = MenuItemCheckStyle.Checked + }, + miShowTabViewBorder = new MenuItem ("_Show TabView Border", "", () => ShowTabViewBorder()){ + Checked = true, + CheckType = MenuItemCheckStyle.Checked } }) @@ -57,6 +56,7 @@ namespace UICatalog.Scenarios { Y = 0, Width = 60, Height = 20, + BorderStyle = LineStyle.Single }; tabView.AddTab (new Tab ("Tab1", new Label ("hodor!")), false); @@ -192,6 +192,15 @@ namespace UICatalog.Scenarios { tabView.ApplyStyleChanges (); } + private void ShowTabViewBorder () + { + miShowTabViewBorder.Checked = !miShowTabViewBorder.Checked; + + tabView.BorderStyle = miShowTabViewBorder.Checked == true ? tabView.BorderStyle = LineStyle.Single + : LineStyle.None; + tabView.ApplyStyleChanges (); + } + private void Quit () { Application.RequestStop (); diff --git a/UnitTests/Views/ButtonTests.cs b/UnitTests/Views/ButtonTests.cs index 5097ade02..be9b90133 100644 --- a/UnitTests/Views/ButtonTests.cs +++ b/UnitTests/Views/ButtonTests.cs @@ -509,9 +509,9 @@ namespace Terminal.Gui.ViewsTests { var btn3 = $"{CM.Glyphs.LeftBracket} Cancel {CM.Glyphs.RightBracket}"; var expected = @$" ┌────────────────────────────────────────────────────┐ -│┌────┐ │ +│╭────╮ │ ││Find│ │ -││ └─────────────────────────────────────────────┐│ +││ ╰─────────────────────────────────────────────╮│ ││ ││ ││ Find: Testing buttons. {btn1} ││ ││ {btn2} ││ diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index ea049a299..f4da0896f 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -255,15 +255,15 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); // Test two tab names that fit - tab1.Text = "12"; - tab2.Text = "13"; + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌──┐ -│12│13 -│ └─────┐ +╭──┬──╮ +│12│13│ +│ ╰──┴──╮ │hi │ └────────┘", output); @@ -272,23 +272,23 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" - ┌──┐ - 12│13│ -┌──┘ └──┐ +╭──┬──╮ +│12│13│ +├──╯ ╰──╮ │hi2 │ └────────┘", output); tv.SelectedTab = tab1; // Test first tab name too long - tab1.Text = "12345678910"; - tab2.Text = "13"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌───────┐ +╭───────╮ │1234567│ -│ └► +│ ╰► │hi │ └────────┘", output); @@ -297,22 +297,22 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌──┐ +╭──╮ │13│ -◄ └─────┐ +◄ ╰─────╮ │hi2 │ └────────┘", output); // now make both tabs too long - tab1.Text = "12345678910"; - tab2.Text = "abcdefghijklmnopq"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌───────┐ +╭───────╮ │abcdefg│ -◄ └┐ +◄ ╰╮ │hi2 │ └────────┘", output); } @@ -330,14 +330,14 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); // Test two tab names that fit - tab1.Text = "12"; - tab2.Text = "13"; + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -│12│13 -│ └─────┐ +│12│13│ +│ ╰──┴──╮ │hi │ │ │ └────────┘", output); @@ -347,8 +347,8 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" - 12│13│ -┌──┘ └──┐ +│12│13│ +├──╯ ╰──╮ │hi2 │ │ │ └────────┘", output); @@ -356,14 +356,14 @@ namespace Terminal.Gui.ViewsTests { tv.SelectedTab = tab1; // Test first tab name too long - tab1.Text = "12345678910"; - tab2.Text = "13"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" │1234567│ -│ └► +│ ╰► │hi │ │ │ └────────┘", output); @@ -374,20 +374,20 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" │13│ -◄ └─────┐ +◄ ╰─────╮ │hi2 │ │ │ └────────┘", output); // now make both tabs too long - tab1.Text = "12345678910"; - tab2.Text = "abcdefghijklmnopq"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" │abcdefg│ -◄ └┐ +◄ ╰╮ │hi2 │ │ │ └────────┘", output); @@ -404,9 +404,9 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌─┐ +╭─╮ │T│ -│ └► +│ ╰► │hi│ └──┘", output); } @@ -425,7 +425,7 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" │T│ -│ └► +│ ╰► │hi│ │ │ └──┘", output); @@ -442,9 +442,9 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌┐ +╭╮ ││ -│└► +│╰► │h│ └─┘", output); } @@ -463,7 +463,7 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" ││ -│└► +│╰► │h│ │ │ └─┘", output); @@ -482,30 +482,30 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); // Test two tab names that fit - tab1.Text = "12"; - tab2.Text = "13"; + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" ┌────────┐ │hi │ -│ ┌─────┘ -│12│13 -└──┘ ", output); +│ ╭──┬──╯ +│12│13│ +╰──┴──╯ ", output); // Test first tab name too long - tab1.Text = "12345678910"; - tab2.Text = "13"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" ┌────────┐ │hi │ -│ ┌► +│ ╭► │1234567│ -└───────┘ ", output); +╰───────╯ ", output); //switch to tab2 tv.SelectedTab = tab2; @@ -514,22 +514,22 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" ┌────────┐ │hi2 │ -◄ ┌─────┘ +◄ ╭─────╯ │13│ -└──┘ ", output); +╰──╯ ", output); // now make both tabs too long - tab1.Text = "12345678910"; - tab2.Text = "abcdefghijklmnopq"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" ┌────────┐ │hi2 │ -◄ ┌┘ +◄ ╭╯ │abcdefg│ -└───────┘ ", output); +╰───────╯ ", output); } [Fact, AutoInitShutdown] @@ -545,8 +545,8 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); // Test two tab names that fit - tab1.Text = "12"; - tab2.Text = "13"; + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; tv.Draw (); @@ -554,8 +554,8 @@ namespace Terminal.Gui.ViewsTests { ┌────────┐ │hi │ │ │ -│ ┌─────┘ -│12│13 ", output); +│ ╭──┬──╯ +│12│13│ ", output); tv.SelectedTab = tab2; @@ -565,14 +565,14 @@ namespace Terminal.Gui.ViewsTests { ┌────────┐ │hi2 │ │ │ -└──┐ ┌──┘ - 12│13│ ", output); +├──╮ ╭──╯ +│12│13│ ", output); tv.SelectedTab = tab1; // Test first tab name too long - tab1.Text = "12345678910"; - tab2.Text = "13"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; tv.Draw (); @@ -580,7 +580,7 @@ namespace Terminal.Gui.ViewsTests { ┌────────┐ │hi │ │ │ -│ ┌► +│ ╭► │1234567│ ", output); //switch to tab2 @@ -591,12 +591,12 @@ namespace Terminal.Gui.ViewsTests { ┌────────┐ │hi2 │ │ │ -◄ ┌─────┘ +◄ ╭─────╯ │13│ ", output); // now make both tabs too long - tab1.Text = "12345678910"; - tab2.Text = "abcdefghijklmnopq"; + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; tv.Draw (); @@ -604,7 +604,7 @@ namespace Terminal.Gui.ViewsTests { ┌────────┐ │hi2 │ │ │ -◄ ┌┘ +◄ ╭╯ │abcdefg│ ", output); } @@ -623,9 +623,9 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" ┌──┐ │hi│ -│ ┌► +│ ╭► │T│ -└─┘ ", output); +╰─╯ ", output); } [Fact, AutoInitShutdown] @@ -644,7 +644,7 @@ namespace Terminal.Gui.ViewsTests { ┌──┐ │hi│ │ │ -│ ┌► +│ ╭► │T│ ", output); } @@ -663,9 +663,9 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" ┌─┐ │h│ -│┌► +│╭► ││ -└┘ ", output); +╰╯ ", output); } [Fact, AutoInitShutdown] @@ -684,7 +684,7 @@ namespace Terminal.Gui.ViewsTests { ┌─┐ │h│ │ │ -│┌► +│╭► ││ ", output); } @@ -697,15 +697,15 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); - tab1.Text = "Tab0"; - tab2.Text = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables"; + tab1.DisplayText = "Tab0"; + tab2.DisplayText = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌────┐ +╭────╮ │Tab0│ -│ └─────────────► +│ ╰─────────────► │hi │ └──────────────────┘", output); @@ -714,9 +714,9 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" -┌──────────────┐ +╭──────────────╮ │Les Misérables│ -◄ └───┐ +◄ ╰───╮ │hi2 │ └──────────────────┘", output); } @@ -732,17 +732,17 @@ namespace Terminal.Gui.ViewsTests { tv.LayoutSubviews (); - tab1.Text = "Tab0"; - tab2.Text = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables"; + tab1.DisplayText = "Tab0"; + tab2.DisplayText = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables"; tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre (@" ┌──────────────────┐ │hi │ -│ ┌─────────────► +│ ╭─────────────► │Tab0│ -└────┘ ", output); +╰────╯ ", output); tv.SelectedTab = tab2; @@ -751,9 +751,9 @@ namespace Terminal.Gui.ViewsTests { TestHelpers.AssertDriverContentsWithFrameAre (@" ┌──────────────────┐ │hi2 │ -◄ ┌───┘ +◄ ╭───╯ │Les Misérables│ -└──────────────┘ ", output); +╰──────────────╯ ", output); } [Fact, AutoInitShutdown] @@ -768,82 +768,401 @@ namespace Terminal.Gui.ViewsTests { tv.Draw (); - var tabRow = tv.Subviews[0]; - Assert.Equal("TabRowView",tabRow.GetType().Name); + var tabRow = tv.Subviews [0]; + Assert.Equal ("TabRowView", tabRow.GetType ().Name); TestHelpers.AssertDriverContentsAre (@" -┌────┐ -│Tab1│Tab2 -│ └─────────────┐ +╭────┬────╮ +│Tab1│Tab2│ +│ ╰────┴────────╮ │hi │ └──────────────────┘ ", output); Tab clicked = null; - - tv.TabClicked += (s,e)=>{ + tv.TabClicked += (s, e) => { clicked = e.Tab; }; - // Waving mouse around does not trigger click - for(int i=0;i<100;i++) - { - tabRow.MouseEvent(new MouseEvent{ - X = i, - Y = 1, - Flags = MouseFlags.ReportMousePosition - }); + Application.Top.Add (tv); + Application.Begin (Application.Top); - Assert.Null(clicked); - Assert.Equal(tab1, tv.SelectedTab); + MouseEventEventArgs args; + + // Waving mouse around does not trigger click + for (int i = 0; i < 100; i++) { + args = new MouseEventEventArgs (new MouseEvent { + X = i, + Y = 1, + Flags = MouseFlags.ReportMousePosition + }); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Null (clicked); + Assert.Equal (tab1, tv.SelectedTab); } - tabRow.MouseEvent(new MouseEvent{ - X = 3, - Y = 1, - Flags = MouseFlags.Button1Clicked + args = new MouseEventEventArgs (new MouseEvent { + X = 3, + Y = 1, + Flags = MouseFlags.Button1Clicked }); - - Assert.Equal(tab1, clicked); - Assert.Equal(tab1, tv.SelectedTab); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Equal (tab1, clicked); + Assert.Equal (tab1, tv.SelectedTab); // Click to tab2 - tabRow.MouseEvent(new MouseEvent{ - X = 7, - Y = 1, - Flags = MouseFlags.Button1Clicked + args = new MouseEventEventArgs (new MouseEvent { + X = 6, + Y = 1, + Flags = MouseFlags.Button1Clicked }); - - Assert.Equal(tab2, clicked); - Assert.Equal(tab2, tv.SelectedTab); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Equal (tab2, clicked); + Assert.Equal (tab2, tv.SelectedTab); // cancel navigation - tv.TabClicked += (s,e)=>{ + tv.TabClicked += (s, e) => { clicked = e.Tab; e.MouseEvent.Handled = true; }; - tabRow.MouseEvent(new MouseEvent{ - X = 3, - Y = 1, - Flags = MouseFlags.Button1Clicked - }); - - // Tab 1 was clicked but event handler blocked navigation - Assert.Equal(tab1, clicked); - Assert.Equal(tab2, tv.SelectedTab); - - tabRow.MouseEvent (new MouseEvent { - X = 10, + args = new MouseEventEventArgs (new MouseEvent { + X = 3, Y = 1, Flags = MouseFlags.Button1Clicked }); + Application.OnMouseEvent (args); + Application.Refresh (); + // Tab 1 was clicked but event handler blocked navigation + Assert.Equal (tab1, clicked); + Assert.Equal (tab2, tv.SelectedTab); + args = new MouseEventEventArgs (new MouseEvent { + X = 12, + Y = 1, + Flags = MouseFlags.Button1Clicked + }); + Application.OnMouseEvent (args); + Application.Refresh (); // Clicking beyond last tab should raise event with null Tab Assert.Null (clicked); Assert.Equal (tab2, tv.SelectedTab); + } + [Fact, AutoInitShutdown] + public void MouseClick_Right_Left_Arrows_ChangesTab () + { + var tv = GetTabView (out var tab1, out var tab2, false); + + tv.Width = 7; + tv.Height = 5; + + tv.LayoutSubviews (); + + tv.Draw (); + + var tabRow = tv.Subviews [0]; + Assert.Equal ("TabRowView", tabRow.GetType ().Name); + + TestHelpers.AssertDriverContentsAre (@" +╭────╮ +│Tab1│ +│ ╰► +│hi │ +└─────┘ +", output); + + Tab clicked = null; + + tv.TabClicked += (s, e) => { + clicked = e.Tab; + }; + + Tab oldChanged = null; + Tab newChanged = null; + + tv.SelectedTabChanged += (s, e) => { + oldChanged = e.OldTab; + newChanged = e.NewTab; + }; + + Application.Top.Add (tv); + Application.Begin (Application.Top); + + // Click the right arrow + var args = new MouseEventEventArgs (new MouseEvent { + X = 6, + Y = 2, + Flags = MouseFlags.Button1Clicked + }); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Null (clicked); + Assert.Equal (tab1, oldChanged); + Assert.Equal (tab2, newChanged); + Assert.Equal (tab2, tv.SelectedTab); + TestHelpers.AssertDriverContentsAre (@" +╭────╮ +│Tab2│ +◄ ╰╮ +│hi2 │ +└─────┘ +", output); + + // Click the left arrow + args = new MouseEventEventArgs (new MouseEvent { + X = 0, + Y = 2, + Flags = MouseFlags.Button1Clicked + }); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Null (clicked); + Assert.Equal (tab2, oldChanged); + Assert.Equal (tab1, newChanged); + Assert.Equal (tab1, tv.SelectedTab); + TestHelpers.AssertDriverContentsAre (@" +╭────╮ +│Tab1│ +│ ╰► +│hi │ +└─────┘ +", output); + } + + [Fact, AutoInitShutdown] + public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () + { + var tv = GetTabView (out var tab1, out var tab2, false); + + tv.Width = 9; + tv.Height = 7; + + Assert.Equal (LineStyle.None, tv.BorderStyle); + tv.BorderStyle = LineStyle.Single; + + tv.LayoutSubviews (); + + tv.Draw (); + + var tabRow = tv.Subviews [0]; + Assert.Equal ("TabRowView", tabRow.GetType ().Name); + + TestHelpers.AssertDriverContentsAre (@" +┌───────┐ +│╭────╮ │ +││Tab1│ │ +││ ╰►│ +││hi ││ +│└─────┘│ +└───────┘ +", output); + + Tab clicked = null; + + tv.TabClicked += (s, e) => { + clicked = e.Tab; + }; + + Tab oldChanged = null; + Tab newChanged = null; + + tv.SelectedTabChanged += (s, e) => { + oldChanged = e.OldTab; + newChanged = e.NewTab; + }; + + Application.Top.Add (tv); + Application.Begin (Application.Top); + + // Click the right arrow + var args = new MouseEventEventArgs (new MouseEvent { + X = 7, + Y = 3, + Flags = MouseFlags.Button1Clicked + }); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Null (clicked); + Assert.Equal (tab1, oldChanged); + Assert.Equal (tab2, newChanged); + Assert.Equal (tab2, tv.SelectedTab); + TestHelpers.AssertDriverContentsAre (@" +┌───────┐ +│╭────╮ │ +││Tab2│ │ +│◄ ╰╮│ +││hi2 ││ +│└─────┘│ +└───────┘ +", output); + + // Click the left arrow + args = new MouseEventEventArgs (new MouseEvent { + X = 1, + Y = 3, + Flags = MouseFlags.Button1Clicked + }); + Application.OnMouseEvent (args); + Application.Refresh (); + Assert.Null (clicked); + Assert.Equal (tab2, oldChanged); + Assert.Equal (tab1, newChanged); + Assert.Equal (tab1, tv.SelectedTab); + TestHelpers.AssertDriverContentsAre (@" +┌───────┐ +│╭────╮ │ +││Tab1│ │ +││ ╰►│ +││hi ││ +│└─────┘│ +└───────┘ +", output); + } + + [Fact] + public void EnsureValidScrollOffsets_TabScrollOffset () + { + var tv = GetTabView (out var tab1, out var tab2); + + // Make tab width small to force only one tab visible at once + tv.Width = 4; + + tv.SelectedTab = tab1; + Assert.Equal (0, tv.TabScrollOffset); + + tv.TabScrollOffset = 10; + tv.SelectedTab = tab2; + Assert.Equal (1, tv.TabScrollOffset); + + tv.TabScrollOffset = -1; + tv.SelectedTab = tab1; + Assert.Equal (0, tv.TabScrollOffset); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } + + [Fact, AutoInitShutdown] + public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () + { + var tv = GetTabView (out var tab1, out var tab2, false); + + tv.Width = 7; + tv.Height = 5; + + var btn = new Button ("Ok") { Y = Pos.Bottom (tv) + 1, Width = 7 }; + + var top = Application.Top; + top.Add (tv, btn); + Application.Begin (top); + + // Is the selected tab view hosting focused + Assert.Equal (tab1, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + Assert.Equal (tv.SelectedTab.View, top.Focused.MostFocused); + + // Press the cursor up key to focus the selected tab + var args = new KeyEventEventArgs (new KeyEvent (Key.CursorUp, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + // Is the selected tab focused + Assert.Equal (tab1, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + Tab oldChanged = null; + Tab newChanged = null; + + tv.SelectedTabChanged += (s, e) => { + oldChanged = e.OldTab; + newChanged = e.NewTab; + }; + + // Press the cursor right key to select the next tab + args = new KeyEventEventArgs (new KeyEvent (Key.CursorRight, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab1, oldChanged); + Assert.Equal (tab2, newChanged); + Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the cursor down key to focused the selected tab view hosting + args = new KeyEventEventArgs (new KeyEvent (Key.CursorDown, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + // The tab view hosting is a label which can't be focused + // and the View container is the focused one + Assert.Equal (tv.Subviews [1], top.Focused.MostFocused); + + // Press the cursor up key to focus the selected tab + args = new KeyEventEventArgs (new KeyEvent (Key.CursorUp, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + // Is the selected tab focused + Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the cursor left key to select the previous tab + args = new KeyEventEventArgs (new KeyEvent (Key.CursorLeft, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab2, oldChanged); + Assert.Equal (tab1, newChanged); + Assert.Equal (tab1, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the end key to select the last tab + args = new KeyEventEventArgs (new KeyEvent (Key.End, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab1, oldChanged); + Assert.Equal (tab2, newChanged); + Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the home key to select the first tab + args = new KeyEventEventArgs (new KeyEvent (Key.Home, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab2, oldChanged); + Assert.Equal (tab1, newChanged); + Assert.Equal (tab1, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the page down key to select the next set of tabs + args = new KeyEventEventArgs (new KeyEvent (Key.PageDown, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab1, oldChanged); + Assert.Equal (tab2, newChanged); + Assert.Equal (tab2, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); + + // Press the page up key to select the previous set of tabs + args = new KeyEventEventArgs (new KeyEvent (Key.PageUp, new KeyModifiers ())); + Application.OnKeyPressed (args); + Application.Refresh (); + Assert.Equal (tab2, oldChanged); + Assert.Equal (tab1, newChanged); + Assert.Equal (tab1, tv.SelectedTab); + Assert.Equal (tv, top.Focused); + Assert.Equal (tv.MostFocused, top.Focused.MostFocused); } private void InitFakeDriver ()