From 0dd0f2e5c4965e1ec430439605a2dab6ff6d2114 Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Tue, 11 May 2021 04:58:15 +0100 Subject: [PATCH] TableView - adds last column dividing line (#1289) * Adds a final vertical column line at the end of the last header in TableView * Added clear line as first step in RenderRow * Added TableStyle.EnforceMaxWidthOnLastColumn * Added Scenario toggle setting and tests * Fixed EnforceMaxWidthOnLastColumn when Bounds match exactly the last column width * Fixed whitespace and comment on ColumntoRender.Width * Renamed EnforceMaxWidthOnLastColumn to ExpandLastColumn --- Terminal.Gui/Views/TableView.cs | 93 ++++++++++++++++++++---------- UICatalog/Scenarios/TableEditor.cs | 11 ++++ UnitTests/TableViewTests.cs | 87 +++++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 33 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index c91a2942a..e8e15fe07 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -300,10 +300,15 @@ namespace Terminal.Gui { // if the next column is the start of a header else if(columnsToRender.Any(r=>r.X == c+1)){ rune = Driver.TopTee; - } + } else if(c == availableWidth -1){ rune = Driver.URCorner; } + // if the next console column is the lastcolumns end + else if ( Style.ExpandLastColumn == false && + columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width-1 == c)) { + rune = Driver.TopTee; + } } AddRuneAt(Driver,c,row,rune); @@ -324,45 +329,26 @@ namespace Terminal.Gui { for(int i =0 ; i - /// Calculates how much space is available to render index of the given the remaining horizontal space - /// - /// - /// - private int GetCellWidth (ColumnToRender [] columnsToRender, int i) - { - var current = columnsToRender[i]; - var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null; - - if(next == null) { - // cell can fill to end of the line - return Bounds.Width - current.X; - } - else { - // cell can fill up to next cell start - return next.X - current.X; - } - - } - + private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender) { // Renders a line below the table headers (when visible) like: @@ -385,6 +371,11 @@ namespace Terminal.Gui { else if(c == availableWidth -1){ rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner; } + // if the next console column is the lastcolumns end + else if (Style.ExpandLastColumn == false && + columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width-1 == c)) { + rune = Style.ShowVerticalCellLines ? '┼' : Driver.BottomTee; + } } AddRuneAt(Driver,c,row,rune); @@ -397,11 +388,15 @@ namespace Terminal.Gui { if(style.ShowVerticalCellLines) AddRune(0,row,Driver.VLine); + //start by clearing the entire line + Move (0,row); + Driver.SetAttribute (FullRowSelect && IsSelected(0,rowToRender) ? ColorScheme.HotFocus : ColorScheme.Normal); + Driver.AddStr (new string(' ',Bounds.Width)); + // Render cells for each visible header for the current row for(int i=0;i< columnsToRender.Length ;i++) { var current = columnsToRender[i]; - var availableWidthForCell = GetCellWidth(columnsToRender,i); var colStyle = Style.GetColumnStyleIfAny(current.Column); @@ -418,13 +413,17 @@ namespace Terminal.Gui { // Render the (possibly truncated) cell value var representation = GetRepresentation(val,colStyle); - Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle)); + Driver.AddStr (TruncateOrPad(val,representation, current.Width, colStyle)); // If not in full row select mode always, reset color scheme to normal and render the vertical line (or space) at the end of the cell if(!FullRowSelect) Driver.SetAttribute (ColorScheme.Normal); RenderSeparator(current.X-1,row,false); + + if (Style.ExpandLastColumn == false && current.IsVeryLast) { + RenderSeparator (current.X + current.Width-1, row, false); + } } //render end of line @@ -1019,21 +1018,26 @@ namespace Terminal.Gui { rowsToRender -= GetHeaderHeight(); bool first = true; + var lastColumn = Table.Columns.Cast ().Last (); foreach (var col in Table.Columns.Cast().Skip (ColumnOffset)) { int startingIdxForCurrentHeader = usedSpace; var colStyle = Style.GetColumnStyleIfAny(col); + int colWidth; // is there enough space for this column (and it's data)? - usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding; + usedSpace += colWidth = CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding; // no (don't render it) unless its the only column we are render (that must be one massively wide column!) if (!first && usedSpace > availableHorizontalSpace) yield break; // there is space - yield return new ColumnToRender(col, startingIdxForCurrentHeader); + yield return new ColumnToRender(col, startingIdxForCurrentHeader, + // required for if we end up here because first == true i.e. we have a single massive width (overspilling bounds) column to present + Math.Min(availableHorizontalSpace,colWidth), + lastColumn == col); first=false; } } @@ -1215,6 +1219,17 @@ namespace Terminal.Gui { /// public Dictionary ColumnStyles { get; set; } = new Dictionary (); + + /// + /// Determines rendering when the last column in the table is visible but it's + /// content or is less than the remaining + /// space in the control. True (the default) will expand the column to fill + /// the remaining bounds of the control. False will draw a column ending line + /// and leave a blank column that cannot be selected in the remaining space. + /// + /// + public bool ExpandLastColumn {get;set;} = true; + /// /// Returns the entry from for the given or null if no custom styling is defined for it /// @@ -1254,11 +1269,25 @@ namespace Terminal.Gui { /// public int X { get; set; } - public ColumnToRender (DataColumn col, int x) + /// + /// The width that the column should occupy as calculated by . Note that this includes + /// space for padding i.e. the separator between columns. + /// + public int Width { get; } + + /// + /// True if this column is the very last column in the (not just the last visible column) + /// + public bool IsVeryLast { get; } + + public ColumnToRender (DataColumn col, int x, int width, bool isVeryLast) { Column = col; X = x; + Width = width; + IsVeryLast = isVeryLast; } + } /// diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 483010a2b..229cc7c71 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -22,6 +22,7 @@ namespace UICatalog.Scenarios { private MenuItem miHeaderUnderline; private MenuItem miCellLines; private MenuItem miFullRowSelect; + private MenuItem miExpandLastColumn; public override void Setup () { @@ -51,6 +52,7 @@ namespace UICatalog.Scenarios { miHeaderUnderline =new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()){Checked = tableView.Style.ShowHorizontalHeaderUnderline, CheckType = MenuItemCheckStyle.Checked }, miFullRowSelect =new MenuItem ("_FullRowSelect", "", () => ToggleFullRowSelect()){Checked = tableView.FullRowSelect, CheckType = MenuItemCheckStyle.Checked }, miCellLines =new MenuItem ("_CellLines", "", () => ToggleCellLines()){Checked = tableView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, + miExpandLastColumn = new MenuItem ("_ExpandLastColumn", "", () => ToggleExpandLastColumn()){Checked = tableView.Style.ExpandLastColumn, CheckType = MenuItemCheckStyle.Checked }, new MenuItem ("_AllLines", "", () => ToggleAllCellLines()), new MenuItem ("_NoLines", "", () => ToggleNoCellLines()), new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()), @@ -181,6 +183,15 @@ namespace UICatalog.Scenarios { tableView.FullRowSelect= miFullRowSelect.Checked; tableView.Update(); } + + private void ToggleExpandLastColumn() + { + miExpandLastColumn.Checked = !miExpandLastColumn.Checked; + tableView.Style.ExpandLastColumn = miExpandLastColumn.Checked; + + tableView.Update(); + + } private void ToggleCellLines() { miCellLines.Checked = !miCellLines.Checked; diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index c964d6d8b..815a8c617 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -418,7 +418,92 @@ namespace Terminal.Gui.Views { Assert.Equal(new Point(8,3),selected[5]); } - /// + [Fact] + public void TableView_ExpandLastColumn_True() + { + var tv = SetUpMiniTable(); + + // the thing we are testing + tv.Style.ExpandLastColumn = true; + + tv.Redraw(tv.Bounds); + + string expected = @" +┌─┬──────┐ +│A│B │ +├─┼──────┤ +│1│2 │ +"; + GraphViewTests.AssertDriverContentsAre(expected); + } + + + [Fact] + public void TableView_ExpandLastColumn_False() + { + var tv = SetUpMiniTable(); + + // the thing we are testing + tv.Style.ExpandLastColumn = false; + + tv.Redraw(tv.Bounds); + + string expected = @" +┌─┬─┬────┐ +│A│B│ │ +├─┼─┼────┤ +│1│2│ │ +"; + GraphViewTests.AssertDriverContentsAre(expected); + } + + [Fact] + public void TableView_ExpandLastColumn_False_ExactBounds() + { + var tv = SetUpMiniTable(); + + // the thing we are testing + tv.Style.ExpandLastColumn = false; + // width exactly matches the max col widths + tv.Bounds = new Rect(0,0,5,4); + + tv.Redraw(tv.Bounds); + + string expected = @" +┌─┬─┐ +│A│B│ +├─┼─┤ +│1│2│ +"; + GraphViewTests.AssertDriverContentsAre(expected); + } + + private TableView SetUpMiniTable () + { + + var tv = new TableView(); + tv.Bounds = new Rect(0,0,10,4); + + var dt = new DataTable(); + var colA = dt.Columns.Add("A"); + var colB = dt.Columns.Add("B"); + dt.Rows.Add(1,2); + + tv.Table = dt; + tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1; + tv.Style.GetOrCreateColumnStyle(colA).MinWidth=1; + tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1; + tv.Style.GetOrCreateColumnStyle(colB).MaxWidth=1; + + GraphViewTests.InitFakeDriver(); + tv.ColorScheme = new ColorScheme(){ + Normal = Application.Driver.MakeAttribute(Color.White,Color.Black), + HotFocus = Application.Driver.MakeAttribute(Color.White,Color.Black) + }; + return tv; + } + + /// /// Builds a simple table of string columns with the requested number of columns and rows /// ///