浏览代码

Support for flexible column widths in TableView (#1760)

* Support for flexible column widths in TableView

* Fixed not respecting min width of MinAcceptableWidth an added UICatalog support

* Added menu options for SmoothHorizontalScrolling and setting all MinAcceptableWidth to 1

* spelling fix
Thomas Nind 3 年之前
父节点
当前提交
13af2b1662
共有 4 个文件被更改,包括 301 次插入7 次删除
  1. 54 5
      Terminal.Gui/Views/TableView.cs
  2. 100 2
      UICatalog/Scenarios/TableEditor.cs
  3. 135 0
      UnitTests/TableViewTests.cs
  4. 12 0
      docfx/articles/tableview.md

+ 54 - 5
Terminal.Gui/Views/TableView.cs

@@ -68,6 +68,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		public const int DefaultMaxCellWidth = 100;
 
+
+		/// <summary>
+		/// The default minimum cell width for <see cref="ColumnStyle.MinAcceptableWidth"/>
+		/// </summary>
+		public const int DefaultMinAcceptableWidth = 100;
+
 		/// <summary>
 		/// The data table to render in the view.  Setting this property automatically updates and redraws the control.
 		/// </summary>
@@ -1214,11 +1220,40 @@ namespace Terminal.Gui {
 				int colWidth;
 
 				// is there enough space for this column (and it's data)?
-				usedSpace += colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
+				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 not enough space for this columns 
+				// visible content
+				if (usedSpace + colWidth > availableHorizontalSpace)
+				{
+					bool showColumn = false;
+
+					// if this column accepts flexible width rendering and
+					// is therefore happy rendering into less space
+					if ( colStyle != null && colStyle.MinAcceptableWidth > 0 &&
+						// is there enough space to meet the MinAcceptableWidth
+						(availableHorizontalSpace - usedSpace) >= colStyle.MinAcceptableWidth)
+					{
+						// show column and use use whatever space is 
+						// left for rendering it
+						showColumn = true;
+						colWidth = availableHorizontalSpace - usedSpace;
+					}
+
+					// If its the only column we are able to render then
+					// accept it anyway (that must be one massively wide column!)
+					if (first)
+					{
+						showColumn = true;
+					}
+
+					// no special exceptions and we are out of space
+					// so stop accepting new columns for the render area
+					if(!showColumn)
+						break;
+				}
+
+				usedSpace += colWidth;
 
 				// there is space
 				yield return new ColumnToRender (col, startingIdxForCurrentHeader,
@@ -1351,10 +1386,24 @@ namespace Terminal.Gui {
 			public int MaxWidth { get; set; } = TableView.DefaultMaxCellWidth;
 
 			/// <summary>
-			/// Set the minimum width of the column in characters.  This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/> or the <see cref="MaxWidth"/>
+			/// Set the minimum width of the column in characters.  Setting this will ensure that
+			/// even when a column has short content/header it still fills a given width of the control.
+			/// 
+			/// <para>This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/> 
+			/// or the <see cref="MaxWidth"/>
+			/// </para>
+			/// <remarks>
+			/// For setting a flexible column width (down to a lower limit) use <see cref="MinAcceptableWidth"/>
+			/// instead
+			/// </remarks>
 			/// </summary>
 			public int MinWidth { get; set; }
 
+			/// <summary>
+			/// Enables flexible sizing of this column based on available screen space to render into.
+			/// </summary>
+			public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth;
+
 			/// <summary>
 			/// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
 			/// </summary>

+ 100 - 2
UICatalog/Scenarios/TableEditor.cs

@@ -4,6 +4,7 @@ using System.Data;
 using Terminal.Gui;
 using System.Linq;
 using System.Globalization;
+using static Terminal.Gui.TableView;
 
 namespace UICatalog.Scenarios {
 
@@ -24,6 +25,7 @@ namespace UICatalog.Scenarios {
 		private MenuItem miCellLines;
 		private MenuItem miFullRowSelect;
 		private MenuItem miExpandLastColumn;
+		private MenuItem miSmoothScrolling;
 		private MenuItem miAlternatingColors;
 		private MenuItem miCursor;
 
@@ -62,14 +64,23 @@ namespace UICatalog.Scenarios {
 					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 },
+					miSmoothScrolling = new MenuItem ("_SmoothHorizontalScrolling", "", () => ToggleSmoothScrolling()){Checked = tableView.Style.SmoothHorizontalScrolling, CheckType = MenuItemCheckStyle.Checked },
 					new MenuItem ("_AllLines", "", () => ToggleAllCellLines()),
 					new MenuItem ("_NoLines", "", () => ToggleNoCellLines()),
 					miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors()){CheckType = MenuItemCheckStyle.Checked},
 					miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter()){Checked = tableView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked},
 					new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
 				}),
+				new MenuBarItem ("_Column", new MenuItem [] {
+					new MenuItem ("_Set Max Width", "", SetMaxWidth),
+					new MenuItem ("_Set Min Width", "", SetMinWidth),
+					new MenuItem ("_Set MinAcceptableWidth", "",SetMinAcceptableWidth),
+					new MenuItem ("_Set All MinAcceptableWidth=1", "",SetMinAcceptableWidthToOne),
+				}),
 			});
-			Top.Add (menu);
+		
+
+		Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)),
@@ -92,7 +103,7 @@ namespace UICatalog.Scenarios {
 
 			Win.Add(selectedCellLabel);
 
-			tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";};
+			tableView.SelectedCellChanged += (e) => { selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; };
 			tableView.CellActivated += EditCurrentCell;
 			tableView.KeyPress += TableViewKeyPress;
 
@@ -121,6 +132,85 @@ namespace UICatalog.Scenarios {
 			};
 		}
 
+
+		private DataColumn GetColumn ()
+		{
+			if (tableView.Table == null)
+				return null;
+
+			if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns.Count)
+				return null;
+
+			return tableView.Table.Columns [tableView.SelectedColumn];
+		}
+
+		private void SetMinAcceptableWidthToOne ()
+		{
+			foreach (DataColumn c in tableView.Table.Columns) 
+			{
+				var style = tableView.Style.GetOrCreateColumnStyle (c);
+				style.MinAcceptableWidth = 1;
+			}
+		}
+		private void SetMinAcceptableWidth ()
+		{
+			var col = GetColumn ();
+			RunColumnWidthDialog (col, "MinAcceptableWidth", (s,v)=>s.MinAcceptableWidth = v,(s)=>s.MinAcceptableWidth);
+		}
+
+		private void SetMinWidth ()
+		{
+			var col = GetColumn ();
+			RunColumnWidthDialog (col, "MinWidth", (s, v) => s.MinWidth = v, (s) => s.MinWidth);
+		}
+
+		private void SetMaxWidth ()
+		{
+			var col = GetColumn ();
+			RunColumnWidthDialog (col, "MaxWidth", (s, v) => s.MaxWidth = v, (s) => s.MaxWidth);
+		}
+
+		private void RunColumnWidthDialog (DataColumn col, string prompt, Action<ColumnStyle,int> setter,Func<ColumnStyle,int> getter)
+		{
+			var accepted = false;
+			var ok = new Button ("Ok", is_default: true);
+			ok.Clicked += () => { accepted = true; Application.RequestStop (); };
+			var cancel = new Button ("Cancel");
+			cancel.Clicked += () => { Application.RequestStop (); };
+			var d = new Dialog (prompt, 60, 20, ok, cancel);
+
+			var style = tableView.Style.GetOrCreateColumnStyle (col);
+
+			var lbl = new Label () {
+				X = 0,
+				Y = 1,
+				Text = col.ColumnName
+			};
+
+			var tf = new TextField () {
+				Text = getter(style).ToString (),
+				X = 0,
+				Y = 2,
+				Width = Dim.Fill ()
+			};
+
+			d.Add (lbl, tf);
+			tf.SetFocus ();
+
+			Application.Run (d);
+
+			if (accepted) {
+
+				try {
+					setter (style, int.Parse (tf.Text.ToString()));
+				} catch (Exception ex) {
+					MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok");
+				}
+
+				tableView.Update ();
+			}
+		}
+
 		private void SetupScrollBar ()
 		{
 			var _scrollBar = new ScrollBarView (tableView, true);
@@ -228,6 +318,14 @@ namespace UICatalog.Scenarios {
 
 			tableView.Update();
 
+		}
+		private void ToggleSmoothScrolling()
+		{
+			miSmoothScrolling.Checked = !miSmoothScrolling.Checked;
+			tableView.Style.SmoothHorizontalScrolling = miSmoothScrolling.Checked;
+
+			tableView.Update ();
+
 		}
 		private void ToggleCellLines()
 		{

+ 135 - 0
UnitTests/TableViewTests.cs

@@ -778,6 +778,141 @@ namespace Terminal.Gui.Views {
 			Application.Shutdown ();
 		}
 
+		[Fact]
+		public void LongColumnTest ()
+		{
+			GraphViewTests.InitFakeDriver ();
+
+			var tableView = new TableView ();
+			tableView.ColorScheme = Colors.TopLevel;
+
+			// 25 characters can be printed into table
+			tableView.Bounds = new Rect (0, 0, 25, 5);
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+			tableView.Style.ShowHorizontalHeaderOverline = false;
+			tableView.Style.AlwaysShowHeaders = true;
+			tableView.Style.SmoothHorizontalScrolling = true;
+
+			var dt = new DataTable ();
+			dt.Columns.Add ("A");
+			dt.Columns.Add ("B");
+			dt.Columns.Add ("Very Long Column");
+
+			dt.Rows.Add (1, 2, new string('a',500));
+			dt.Rows.Add (1, 2, "aaa");
+
+			tableView.Table = dt;
+
+			tableView.Redraw (tableView.Bounds);
+
+			// default behaviour of TableView is not to render
+			// columns unless there is sufficient space
+			string expected = 
+				@"
+│A│B                    │
+├─┼─────────────────────►
+│1│2                    │
+│1│2                    │
+";
+
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// get a style for the long column
+			var style = tableView.Style.GetOrCreateColumnStyle(dt.Columns[2]);
+			
+			// one way the API user can fix this for long columns
+			// is to specify a max width for the column
+			style.MaxWidth = 10;
+
+			tableView.Redraw (tableView.Bounds);
+			expected = 
+				@"
+│A│B│Very Long          │
+├─┼─┼───────────────────┤
+│1│2│aaaaaaaaaa         │
+│1│2│aaa                │
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// revert the style change
+			style.MaxWidth = TableView.DefaultMaxCellWidth;
+
+			// another way API user can fix problem is to implement
+			// RepresentationGetter and apply max length there
+
+			style.RepresentationGetter = (s)=>{
+				return s.ToString().Length < 15 ? s.ToString() : s.ToString().Substring(0,13)+"...";
+			};
+
+			tableView.Redraw (tableView.Bounds);
+			expected = 
+				@"
+│A│B│Very Long Column   │
+├─┼─┼───────────────────┤
+│1│2│aaaaaaaaaaaaa...   │
+│1│2│aaa                │
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// revert style change
+			style.RepresentationGetter = null;
+
+			// Both of the above methods rely on having a fixed
+			// size limit for the column.  These are awkward if a
+			// table is resizeable e.g. Dim.Fill().  Ideally we want
+			// to render in any space available and truncate the content
+			// of the column dynamically so it fills the free space at
+			// the end of the table.
+
+			// We can now specify that the column can be any length
+			// (Up to MaxWidth) but the renderer can accept using
+			// less space down to this limit
+			style.MinAcceptableWidth = 5;
+
+			tableView.Redraw (tableView.Bounds);
+			expected = 
+				@"
+│A│B│Very Long Column   │
+├─┼─┼───────────────────┤
+│1│2│aaaaaaaaaaaaaaaaaaa│
+│1│2│aaa                │
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// Now test making the width too small for the MinAcceptableWidth
+			// the Column won't fit so should not be rendered
+			Application.Shutdown ();
+			GraphViewTests.InitFakeDriver ();
+
+			tableView.Bounds = new Rect(0,0,9,5);
+			tableView.Redraw (tableView.Bounds);
+			expected =
+@"
+│A│B    │
+├─┼─────►
+│1│2    │
+│1│2    │
+
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			// setting width to 10 leaves just enough space for the column to
+			// meet MinAcceptableWidth of 5.  Column width includes terminator line
+			// symbol (e.g. ┤ or │)
+			tableView.Bounds = new Rect (0, 0, 10, 5);
+			tableView.Redraw (tableView.Bounds);
+			expected =
+@"
+│A│B│Very│
+├─┼─┼────┤
+│1│2│aaaa│
+│1│2│aaa │
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			Application.Shutdown ();
+		}
+
 
 		[Fact]
 		public void ScrollIndicators ()

+ 12 - 0
docfx/articles/tableview.md

@@ -54,3 +54,15 @@ tableView = new TableView () {
 
 tableView.Table = yourDataTable;
 ```
+
+## Table Rendering
+TableView supports any size of table (limited only by the RAM requirements of `System.DataTable`).  You can have
+thousands of columns and/or millions of rows if you want.  Horizontal and vertical scrolling can be done using
+the mouse or keyboard.
+
+TableView uses `ColumnOffset` and `RowOffset` to determine the first visible cell of the `System.DataTable`.
+Rendering then continues until the avaialble console space is exhausted.  Updating the `ColumnOffset` and 
+`RowOffset` changes which part of the table is rendered (scrolls the viewport).
+
+This approach ensures that no matter how big the table, only a small number of columns/rows need to be
+evaluated for rendering.