Browse Source

Merge pull request #1357 from tznind/table-colors

Added support for coloring cells in TableView
Charlie Kindel 4 years ago
parent
commit
1026f2145b

File diff suppressed because it is too large
+ 329 - 277
Terminal.Gui/Views/TableView.cs


+ 166 - 0
UICatalog/Scenarios/MultiColouredTable.cs

@@ -0,0 +1,166 @@
+using System;
+using System.Data;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "MultiColouredTable", Description: "Demonstrates how to multi color cell contents")]
+	[ScenarioCategory ("Controls")]
+	public class MultiColouredTable : Scenario {
+		TableViewColors tableView;
+
+		public override void Setup ()
+		{
+			Win.Title = this.GetName ();
+			Win.Y = 1; // menu
+			Win.Height = Dim.Fill (1); // status bar
+			Top.LayoutSubviews ();
+
+			this.tableView = new TableViewColors () {
+				X = 0,
+				Y = 0,
+				Width = Dim.Fill (),
+				Height = Dim.Fill (1),
+			};
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_File", new MenuItem [] {
+					new MenuItem ("_Quit", "", () => Quit()),
+				}),
+			});
+			Top.Add (menu);
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
+			});
+			Top.Add (statusBar);
+
+			Win.Add (tableView);
+
+			tableView.CellActivated += EditCurrentCell;
+
+			var dt = new DataTable ();
+			dt.Columns.Add ("Col1");
+			dt.Columns.Add ("Col2");
+
+			dt.Rows.Add ("some text", "Rainbows and Unicorns are so fun!");
+			dt.Rows.Add ("some text", "When it rains you get rainbows");
+			dt.Rows.Add (DBNull.Value, DBNull.Value);
+			dt.Rows.Add (DBNull.Value, DBNull.Value);
+			dt.Rows.Add (DBNull.Value, DBNull.Value);
+			dt.Rows.Add (DBNull.Value, DBNull.Value);
+
+			tableView.ColorScheme = new ColorScheme () {
+
+				Disabled = Win.ColorScheme.Disabled,
+				HotFocus = Win.ColorScheme.HotFocus,
+				Focus = Win.ColorScheme.Focus,
+				Normal = Application.Driver.MakeAttribute (Color.DarkGray, Color.Black)
+			};
+
+			tableView.Table = dt;
+		}
+				
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
+		private bool GetText (string title, string label, string initialText, out string enteredText)
+		{
+			bool okPressed = false;
+
+			var ok = new Button ("Ok", is_default: true);
+			ok.Clicked += () => { okPressed = true; Application.RequestStop (); };
+			var cancel = new Button ("Cancel");
+			cancel.Clicked += () => { Application.RequestStop (); };
+			var d = new Dialog (title, 60, 20, ok, cancel);
+
+			var lbl = new Label () {
+				X = 0,
+				Y = 1,
+				Text = label
+			};
+
+			var tf = new TextField () {
+				Text = initialText,
+				X = 0,
+				Y = 2,
+				Width = Dim.Fill ()
+			};
+
+			d.Add (lbl, tf);
+			tf.SetFocus ();
+
+			Application.Run (d);
+
+			enteredText = okPressed ? tf.Text.ToString () : null;
+			return okPressed;
+		}
+		private void EditCurrentCell (TableView.CellActivatedEventArgs e)
+		{
+			if (e.Table == null)
+				return;
+
+			var oldValue = e.Table.Rows [e.Row] [e.Col].ToString ();
+
+			if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) {
+				try {
+					e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
+				} catch (Exception ex) {
+					MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
+				}
+
+				tableView.Update ();
+			}
+		}
+
+		class TableViewColors : TableView {
+			protected override void RenderCell (Terminal.Gui.Attribute cellColor, string render, bool isPrimaryCell)
+			{
+				int unicorns = render.IndexOf ("unicorns",StringComparison.CurrentCultureIgnoreCase);
+				int rainbows = render.IndexOf ("rainbows", StringComparison.CurrentCultureIgnoreCase);
+
+				for (int i=0;i<render.Length;i++) {
+
+					if(unicorns != -1 && i >= unicorns && i <= unicorns + 8) {
+						Driver.SetAttribute (Driver.MakeAttribute (Color.White, cellColor.Background));
+					}
+					
+					if (rainbows != -1 && i >= rainbows && i <= rainbows + 8) {
+
+						var letterOfWord = i - rainbows;
+						switch(letterOfWord) {
+						case 0 :
+							Driver.SetAttribute (Driver.MakeAttribute (Color.Red, cellColor.Background));
+								break;
+						case 1:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.BrightRed, cellColor.Background));
+								break;
+						case 2:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.BrightYellow, cellColor.Background));
+								break;
+						case 3:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.Green, cellColor.Background));
+								break;
+						case 4:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.BrightGreen, cellColor.Background));
+								break;
+						case 5:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.BrightBlue, cellColor.Background));
+								break;
+						case 6:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.BrightCyan, cellColor.Background));
+								break;
+						case 7:
+							Driver.SetAttribute (Driver.MakeAttribute (Color.Cyan, cellColor.Background));
+								break;
+						}
+					} 
+					
+					Driver.AddRune (render [i]);
+					Driver.SetAttribute (cellColor);
+				}				
+			}
+		}
+	}
+}

+ 61 - 4
UICatalog/Scenarios/TableEditor.cs

@@ -23,6 +23,12 @@ namespace UICatalog.Scenarios {
 		private MenuItem miCellLines;
 		private MenuItem miFullRowSelect;
 		private MenuItem miExpandLastColumn;
+		private MenuItem miAlternatingColors;
+		private MenuItem miCursor;
+
+		ColorScheme redColorScheme;
+		ColorScheme redColorSchemeAlt;
+		ColorScheme alternatingColorScheme;
 
 		public override void Setup ()
 		{
@@ -55,13 +61,13 @@ namespace UICatalog.Scenarios {
 					miExpandLastColumn = new MenuItem ("_ExpandLastColumn", "", () => ToggleExpandLastColumn()){Checked = tableView.Style.ExpandLastColumn, 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()),
 				}),
 			});
 			Top.Add (menu);
 
-
-
 			var statusBar = new StatusBar (new StatusItem [] {
 				new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)),
 				new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample()),
@@ -88,6 +94,28 @@ namespace UICatalog.Scenarios {
 			tableView.KeyPress += TableViewKeyPress;
 
 			SetupScrollBar();
+
+			redColorScheme = new ColorScheme(){
+				Disabled = Win.ColorScheme.Disabled,
+				HotFocus = Win.ColorScheme.HotFocus,
+				Focus = Win.ColorScheme.Focus,
+				Normal = Application.Driver.MakeAttribute(Color.Red,Win.ColorScheme.Normal.Background)
+			};
+
+			alternatingColorScheme = new ColorScheme(){
+
+				Disabled = Win.ColorScheme.Disabled,
+				HotFocus = Win.ColorScheme.HotFocus,
+				Focus = Win.ColorScheme.Focus,
+				Normal = Application.Driver.MakeAttribute(Color.White,Color.BrightBlue)
+			};
+			redColorSchemeAlt = new ColorScheme(){
+
+				Disabled = Win.ColorScheme.Disabled,
+				HotFocus = Win.ColorScheme.HotFocus,
+				Focus = Win.ColorScheme.Focus,
+				Normal = Application.Driver.MakeAttribute(Color.Red,Color.BrightBlue)
+			};
 		}
 
 		private void SetupScrollBar ()
@@ -226,8 +254,29 @@ namespace UICatalog.Scenarios {
 
 			tableView.Update();
 		}
-		
 
+		private void ToggleAlternatingColors()
+		{
+			//toggle menu item
+			miAlternatingColors.Checked = !miAlternatingColors.Checked;
+
+			if(miAlternatingColors.Checked){
+				tableView.Style.RowColorGetter = (a)=> {return a.RowIndex%2==0 ? alternatingColorScheme : null;};
+			}
+			else
+			{
+				tableView.Style.RowColorGetter = null;
+			}
+			tableView.SetNeedsDisplay();
+		}
+
+		private void ToggleInvertSelectedCellFirstCharacter ()
+		{
+			//toggle menu item
+			miCursor.Checked = !miCursor.Checked;
+			tableView.Style.InvertSelectedCellFirstCharacter = miCursor.Checked;
+			tableView.SetNeedsDisplay ();
+		}
 		private void CloseExample ()
 		{
 			tableView.Table = null;
@@ -268,7 +317,15 @@ namespace UICatalog.Scenarios {
 								// align positive values left
 								TextAlignment.Left:
 								// not a double
-								TextAlignment.Left
+								TextAlignment.Left,
+				
+				ColorGetter = (a)=> a.CellValue is double d ? 
+								// color 0 and negative values red
+								d <= 0.0000001 ? a.RowIndex%2==0 && miAlternatingColors.Checked ? redColorSchemeAlt: redColorScheme : 
+								// use normal scheme for positive values
+								null:
+								// not a double
+								null
 			};
 			
 			tableView.Style.ColumnStyles.Add(tableView.Table.Columns["DateCol"],dateFormatStyle);

+ 47 - 0
UnitTests/GraphViewTests.cs

@@ -116,6 +116,53 @@ namespace Terminal.Gui.Views {
 			}
 		}
 
+#pragma warning disable xUnit1013 // Public method should be marked as test
+		/// <summary>
+		/// Verifies the console was rendered using the given <paramref name="expectedColors"/> at the given locations.
+		/// Pass a bitmap of indexes into <paramref name="expectedColors"/> as <paramref name="expectedLook"/> and the
+		/// test method will verify those colors were used in the row/col of the console during rendering
+		/// </summary>
+		/// <param name="expectedLook">Numbers between 0 and 9 for each row/col of the console.  Must be valid indexes of <paramref name="expectedColors"/></param>
+		/// <param name="expectedColors"></param>
+		public static void AssertDriverColorsAre (string expectedLook, Attribute[] expectedColors)
+		{
+#pragma warning restore xUnit1013 // Public method should be marked as test
+
+			if(expectedColors.Length > 10) {
+				throw new ArgumentException ("This method only works for UIs that use at most 10 colors");
+			}
+
+			expectedLook = expectedLook.Trim ();
+			var driver = ((FakeDriver)Application.Driver);
+
+			var contents = driver.Contents;
+
+			int r = 0;
+			foreach(var line in expectedLook.Split ('\n').Select(l=>l.Trim())) {
+
+				for (int c = 0; c < line.Length; c++) {
+
+					int val = contents [r, c, 1];
+
+					var match = expectedColors.Where (e => e.Value == val).ToList ();
+					if (match.Count == 0) {
+						throw new Exception ($"Unexpected color {val} was used at row {r} and col {c}.  Color value was {val} (expected colors were {string.Join (",", expectedColors.Select (c => c.Value))})");
+					} else if (match.Count > 1) {
+						throw new ArgumentException ($"Bad value for expectedColors, {match.Count} Attributes had the same Value");
+					}
+
+					var colorUsed = Array.IndexOf(expectedColors,match[0]).ToString()[0];
+					var userExpected = line [c];
+
+					if( colorUsed != userExpected) {
+						throw new Exception ($"Colors used did not match expected at row {r} and col {c}.  Color index used was {colorUsed} but test expected {userExpected} (these are indexes into the expectedColors array)");
+					}
+				}
+
+				r++;
+			}
+		}
+
 		#region Screen to Graph Tests
 
 		[Fact]

+ 48 - 0
UnitTests/TableViewTests.cs

@@ -495,6 +495,54 @@ namespace Terminal.Gui.Views {
 			Application.Shutdown ();
 		}
 
+		[Fact]
+		public void TableView_ColorsTest_ColorGetter ()
+		{
+			var tv = SetUpMiniTable ();
+
+			tv.Style.ExpandLastColumn = false;
+			tv.Style.InvertSelectedCellFirstCharacter = true;
+
+			// width exactly matches the max col widths
+			tv.Bounds = new Rect (0, 0, 5, 4);
+			
+			// Create a style for column B
+			var bStyle = tv.Style.GetOrCreateColumnStyle (tv.Table.Columns ["B"]);
+
+			// when B is 2 use the custom highlight colour
+			ColorScheme cellHighlight = new ColorScheme () { Normal = Attribute.Make (Color.BrightCyan, Color.DarkGray) };
+			bStyle.ColorGetter = (a) => Convert.ToInt32(a.CellValue) == 2 ? cellHighlight : null;
+
+			tv.Redraw (tv.Bounds);
+
+			string expected = @"
+┌─┬─┐
+│A│B│
+├─┼─┤
+│1│2│
+";
+			GraphViewTests.AssertDriverContentsAre (expected, output);
+
+			string expectedColors = @"
+00000
+00000
+00000
+01020
+";
+			var invertedNormalColor = Application.Driver.MakeAttribute (tv.ColorScheme.Normal.Background, tv.ColorScheme.Normal.Foreground);
+
+			GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
+				// 0
+				tv.ColorScheme.Normal,				
+				// 1
+				invertedNormalColor,				
+				// 2
+				cellHighlight.Normal});
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
 		private TableView SetUpMiniTable ()
 		{
 

Some files were not shown because too many files changed in this diff