Ver código fonte

Added gridlines and fixed partial column rendering

tznind 4 anos atrás
pai
commit
185f4ed4cd
2 arquivos alterados com 339 adições e 27 exclusões
  1. 251 27
      Terminal.Gui/Views/TableView.cs
  2. 88 0
      UICatalog/Scenarios/TableEditor.cs

+ 251 - 27
Terminal.Gui/Views/TableView.cs

@@ -6,6 +6,37 @@ using System.Linq;
 
 namespace Terminal.Gui.Views {
 
+	/// <summary>
+	/// Defines rendering options that affect how the table is displayed
+	/// </summary>
+	public class TableStyle {
+		
+		/// <summary>
+		/// When scrolling down always lock the column headers in place as the first row of the table
+		/// </summary>
+		public bool AlwaysShowHeaders {get;set;} = false;
+
+		/// <summary>
+		/// True to render a solid line above the headers
+		/// </summary>
+		public bool ShowHorizontalHeaderOverline {get;set;} = true;
+
+		/// <summary>
+		/// True to render a solid line under the headers
+		/// </summary>
+		public bool ShowHorizontalHeaderUnderline {get;set;} = true;
+
+		/// <summary>
+		/// True to render a solid line vertical line between cells
+		/// </summary>
+		public bool ShowVerticalCellLines {get;set;} = true;
+
+		/// <summary>
+		/// True to render a solid line vertical line between headers
+		/// </summary>
+		public bool ShowVerticalHeaderLines {get;set;} = true;
+	}
+	
 	/// <summary>
 	/// View for tabular data based on a <see cref="DataTable"/>
 	/// </summary>
@@ -16,12 +47,18 @@ namespace Terminal.Gui.Views {
 		private int selectedRow;
 		private int selectedColumn;
 		private DataTable table;
+		private TableStyle style = new TableStyle();
 
 		/// <summary>
 		/// The data table to render in the view.  Setting this property automatically updates and redraws the control.
 		/// </summary>
 		public DataTable Table { get => table; set {table = value; Update(); } }
-
+		
+		/// <summary>
+		/// Contains options for changing how the table is rendered
+		/// </summary>
+		public TableStyle Style { get => style; set {style = value; Update(); } }
+						
 		/// <summary>
 		/// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
 		/// </summary>
@@ -71,7 +108,7 @@ namespace Terminal.Gui.Views {
 		public string NullSymbol { get; set; } = "-";
 
 		/// <summary>
-		/// The symbol to add after each cell value and header value to visually seperate values
+		/// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
 		/// </summary>
 		public char SeparatorSymbol { get; set; } = ' ';
 
@@ -102,45 +139,204 @@ namespace Terminal.Gui.Views {
 			Dictionary<DataColumn, int> columnsToRender = CalculateViewport (bounds);
 
 			Driver.SetAttribute (ColorScheme.Normal);
-
+			
 			//invalidate current row (prevents scrolling around leaving old characters in the frame
 			Driver.AddStr (new string (' ', bounds.Width));
 
-			// Render the headers
-			foreach (var kvp in columnsToRender) {
+			int line = 0;
+
+			if(ShouldRenderHeaders()){
+				// Render something like:
+				/*
+					┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
+					│ArithmeticComparator│chi       │Healthboard│Interpretation│Labnumber│
+					└────────────────────┴──────────┴───────────┴──────────────┴─────────┘
+				*/
+				if(Style.ShowHorizontalHeaderOverline){
+					RenderHeaderOverline(line,bounds.Width,columnsToRender);
+					line++;
+				}
 
-				Move (kvp.Value, 0);
-				Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value));
-			}
+				RenderHeaderMidline(line,bounds.Width,columnsToRender);
+				line++;
 
+				if(Style.ShowHorizontalHeaderUnderline){
+					RenderHeaderUnderline(line,bounds.Width,columnsToRender);
+					line++;
+				}
+			}
+					
 			//render the cells
-			for (int line = 1; line < frame.Height; line++) {
+			for (; line < frame.Height; line++) {
 
-				//invalidate current row (prevents scrolling around leaving old characters in the frame
-				Move (0, line);
-				Driver.SetAttribute (ColorScheme.Normal);
-				Driver.AddStr (new string (' ', bounds.Width));
+				ClearLine(line,bounds.Width);
 
 				//work out what Row to render
-				var rowToRender = RowOffset + (line - 1);
+				var rowToRender = RowOffset + (line - GetHeaderHeight());
 
 				//if we have run off the end of the table
-				if ( Table == null || rowToRender >= Table.Rows.Count)
+				if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0)
 					continue;
 
-				foreach (var kvp in columnsToRender) {
-					Move (kvp.Value, line);
+				RenderRow(line,bounds.Width,rowToRender,columnsToRender);
+			}
+		}
 
-					bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn;
+		/// <summary>
+		/// Clears a line of the console by filling it with spaces
+		/// </summary>
+		/// <param name="row"></param>
+		/// <param name="width"></param>
+		private void ClearLine(int row, int width)
+		{            
+			Move (0, row);
+			Driver.SetAttribute (ColorScheme.Normal);
+			Driver.AddStr (new string (' ', width));
+		}
 
-					Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
+		/// <summary>
+		/// Returns the amount of vertical space required to display the header
+		/// </summary>
+		/// <returns></returns>
+		private int GetHeaderHeight()
+		{
+			int heightRequired = 1;
+			
+			if(Style.ShowHorizontalHeaderOverline)
+				heightRequired++;
+
+			if(Style.ShowHorizontalHeaderUnderline)
+				heightRequired++;
+			
+			return heightRequired;
+		}
 
+		private void RenderHeaderOverline(int row,int availableWidth, Dictionary<DataColumn, int> columnsToRender)
+		{
+			// Renders a line above table headers (when visible) like:
+			// ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
+
+			for(int c = 0;c< availableWidth;c++) {
+
+				var rune = Driver.HLine;
+
+				if (Style.ShowVerticalHeaderLines){
+							
+					if(c == 0){
+						rune = Driver.ULCorner;
+					}	
+					// if the next column is the start of a header
+					else if(columnsToRender.Values.Contains(c+1)){
+						rune = Driver.TopTee;
+					}
+					else if(c == availableWidth -1){
+						rune = Driver.URCorner;
+					}
+				}
 
-					var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol;
-					Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value));
+				AddRuneAt(Driver,c,row,rune);
+			}
+		}
+
+		private void RenderHeaderMidline(int row,int availableWidth, Dictionary<DataColumn, int> columnsToRender)
+		{
+			// Renders something like:
+			// │ArithmeticComparator│chi       │Healthboard│Interpretation│Labnumber│
+						
+			ClearLine(row,availableWidth);
+
+			//render start of line
+			if(style.ShowVerticalHeaderLines)
+				AddRune(0,row,Driver.VLine);
+
+			foreach (var kvp in columnsToRender) {
+				
+				//where the header should start
+				var col = kvp.Value;
+
+				RenderSeparator(col-1,row);
+									
+				Move (col, row);
+				Driver.AddStr(Truncate (kvp.Key.ColumnName, availableWidth - kvp.Value));
+
+			}
+
+			//render end of line
+			if(style.ShowVerticalHeaderLines)
+				AddRune(availableWidth-1,row,Driver.VLine);
+		}
+
+		private void RenderHeaderUnderline(int row,int availableWidth, Dictionary<DataColumn, int> columnsToRender)
+		{
+			// Renders a line below the table headers (when visible) like:
+			// ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
+								
+			for(int c = 0;c< availableWidth;c++) {
+
+				var rune = Driver.HLine;
+
+				if (Style.ShowVerticalHeaderLines){
+					if(c == 0){
+						rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner;
+					}	
+					// if the next column is the start of a header
+					else if(columnsToRender.Values.Contains(c+1)){
+					
+						/*TODO: is ┼ symbol in Driver?*/ 
+						rune = Style.ShowVerticalCellLines ? '┼' :Driver.BottomTee;
+					}
+					else if(c == availableWidth -1){
+						rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
+					}
 				}
+
+				AddRuneAt(Driver,c,row,rune);
+			}
+			
+		}
+		private void RenderRow(int row, int availableWidth, int rowToRender, Dictionary<DataColumn, int> columnsToRender)
+		{
+			//render start of line
+			if(style.ShowVerticalHeaderLines)
+				AddRune(0,row,Driver.VLine);
+
+			// Render cells for each visible header for the current row
+			foreach (var kvp in columnsToRender) {
+
+				// move to start of cell (in line with header positions)
+				Move (kvp.Value, row);
+
+				// Set color scheme based on whether the current cell is the selected one
+				bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn;
+				Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
+
+				// Render the (possibly truncated) cell value
+				var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]);
+				Driver.AddStr (Truncate (valueToRender, availableWidth - kvp.Value));
+				
+				// Reset color scheme to normal and render the vertical line (or space) at the end of the cell
+				Driver.SetAttribute (ColorScheme.Normal);
+				RenderSeparator(kvp.Value-1,row);
 			}
 
+			//render end of line
+			if(style.ShowVerticalHeaderLines)
+				AddRune(availableWidth-1,row,Driver.VLine);
+		}
+		
+		private void RenderSeparator(int col, int row)
+		{
+			if(col<0)
+				return;
+
+			Rune symbol = style.ShowVerticalHeaderLines ? Driver.VLine : SeparatorSymbol;
+			AddRune(col,row,symbol);
+		}
+
+		void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch)
+		{
+			Move (col, row);
+			d.AddRune (ch);
 		}
 
 		/// <summary>
@@ -231,6 +427,7 @@ namespace Terminal.Gui.Views {
 			SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
 
 			Dictionary<DataColumn, int> columnsToRender = CalculateViewport (Bounds);
+			var headerHeight = GetHeaderHeight();
 
 			//if we have scrolled too far to the left 
 			if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) {
@@ -243,7 +440,7 @@ namespace Terminal.Gui.Views {
 			}
 
 			//if we have scrolled too far down
-			if (SelectedRow > RowOffset + Bounds.Height - 1) {
+			if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) {
 				RowOffset = SelectedRow;
 			}
 			//if we have scrolled too far up
@@ -266,24 +463,46 @@ namespace Terminal.Gui.Views {
 
 			if(Table == null)
 				return toReturn;
-
+			
 			int usedSpace = 0;
+
+			//if horizontal space is required at the start of the line (before the first header)
+			if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines)
+				usedSpace+=2;
+			
 			int availableHorizontalSpace = bounds.Width;
-			int rowsToRender = bounds.Height - 1; //1 reserved for the headers row
+			int rowsToRender = bounds.Height;
+
+			// reserved for the headers row
+			if(ShouldRenderHeaders())
+				rowsToRender -= GetHeaderHeight(); 
+
+			bool first = true;
+
+			foreach (var col in Table.Columns.Cast<DataColumn>().Skip (ColumnOffset)) {
 
-			foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
+				int startingIdxForCurrentHeader = usedSpace;
 
-				toReturn.Add (col, usedSpace);
+				// is there enough space for this column (and it's data)?
 				usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
 
-				if (usedSpace > availableHorizontalSpace)
+				// no (don't render it) unless its the only column we are render (that must be one massively wide column!)
+				if (!first && usedSpace > availableHorizontalSpace)
 					return toReturn;
 
+				// there is space
+				toReturn.Add (col, startingIdxForCurrentHeader);
+				first=false;
 			}
 
 			return toReturn;
 		}
 
+		private bool ShouldRenderHeaders()
+		{
+		    return Style.AlwaysShowHeaders || rowOffset == 0;
+		}
+
 		/// <summary>
 		/// Returns the maximum of the <paramref name="col"/> name and the maximum length of data that will be rendered starting at <see cref="RowOffset"/> and rendering <paramref name="rowsToRender"/>
 		/// </summary>
@@ -294,6 +513,11 @@ namespace Terminal.Gui.Views {
 		{
 			int spaceRequired = col.ColumnName.Length;
 
+			// if table has no rows
+			if(RowOffset < 0)
+				return spaceRequired;
+
+
 			for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
 
 				//expand required space if cell is bigger than the last biggest cell or header

+ 88 - 0
UICatalog/Scenarios/TableEditor.cs

@@ -30,6 +30,15 @@ namespace UICatalog.Scenarios {
 					new MenuItem ("_CloseExample", "", () => CloseExample()),
 					new MenuItem ("_Quit", "", () => Quit()),
 				}),
+				new MenuBarItem ("_View", new MenuItem [] {
+					new MenuItem ("_AlwaysShowHeaders", "", () => ToggleAlwaysShowHeader()),
+					new MenuItem ("_HeaderOverLine", "", () => ToggleOverline()),
+					new MenuItem ("_HeaderMidLine", "", () => ToggleHeaderMidline()),
+					new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()),
+					new MenuItem ("_CellLines", "", () => ToggleCellLines()),
+					new MenuItem ("_AllLines", "", () => ToggleAllCellLines()),
+					new MenuItem ("_NoLines", "", () => ToggleNoCellLines()),
+				}),
 			});
 			Top.Add (menu);
 
@@ -38,6 +47,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)),
 				new StatusItem(Key.F3, "~F3~ EditCell", () => EditCurrentCell()),
 				new StatusItem(Key.F4, "~F4~ CloseExample", () => CloseExample()),
+				new StatusItem(Key.F5, "~F5~ OpenSimple", () => OpenSimple(true)),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
 			Top.Add (statusBar);
@@ -52,6 +62,52 @@ namespace UICatalog.Scenarios {
 			Win.Add (tableView);
 		}
 
+
+
+		private void ToggleAlwaysShowHeader ()
+		{
+			tableView.Style.AlwaysShowHeaders = !tableView.Style.AlwaysShowHeaders;
+			tableView.Update();
+		}
+
+		private void ToggleOverline ()
+		{
+			tableView.Style.ShowHorizontalHeaderOverline = !tableView.Style.ShowHorizontalHeaderOverline;
+			tableView.Update();
+		}
+		private void ToggleHeaderMidline ()
+		{
+			tableView.Style.ShowVerticalHeaderLines = !tableView.Style.ShowVerticalHeaderLines;
+			tableView.Update();
+		}
+		private void ToggleUnderline ()
+		{
+			tableView.Style.ShowHorizontalHeaderUnderline = !tableView.Style.ShowHorizontalHeaderUnderline;
+			tableView.Update();
+		}
+		private void ToggleCellLines()
+		{
+			tableView.Style.ShowVerticalCellLines = !tableView.Style.ShowVerticalCellLines;
+			tableView.Update();
+		}
+		private void ToggleAllCellLines()
+		{
+			tableView.Style.ShowHorizontalHeaderOverline = true;
+			tableView.Style.ShowVerticalHeaderLines = true;
+			tableView.Style.ShowHorizontalHeaderUnderline = true;
+			tableView.Style.ShowVerticalCellLines = true;
+			tableView.Update();
+		}
+		private void ToggleNoCellLines()
+		{
+			tableView.Style.ShowHorizontalHeaderOverline = false;
+			tableView.Style.ShowVerticalHeaderLines = false;
+			tableView.Style.ShowHorizontalHeaderUnderline = false;
+			tableView.Style.ShowVerticalCellLines = false;
+			tableView.Update();
+		}
+		
+
 		private void CloseExample ()
 		{
 			tableView.Table = null;
@@ -66,6 +122,11 @@ namespace UICatalog.Scenarios {
 		{
 			tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5);
 		}
+		private void OpenSimple (bool big)
+		{
+			
+			tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5);
+		}
 
 		private void EditCurrentCell ()
 		{
@@ -154,5 +215,32 @@ namespace UICatalog.Scenarios {
 
 			return dt;
 		}
+
+		/// <summary>
+		/// Builds a simple table in which cell values contents are the index of the cell.  This helps testing that scrolling etc is working correctly and not skipping out any rows/columns when paging
+		/// </summary>
+		/// <param name="cols"></param>
+		/// <param name="rows"></param>
+		/// <returns></returns>
+		public static DataTable BuildSimpleDataTable(int cols, int rows)
+		{
+			var dt = new DataTable();
+
+			for(int c = 0; c < cols; c++) {
+				dt.Columns.Add("Col"+c);
+			}
+				
+			for(int r = 0; r < rows; r++) {
+				var newRow = dt.NewRow();
+
+				for(int c = 0; c < cols; c++) {
+					newRow[c] = $"R{r}C{c}";
+				}
+
+				dt.Rows.Add(newRow);
+			}
+			
+			return dt;
+		}
 	}
 }