diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index e8e15fe07..4f8deab56 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Terminal.Gui { - + /// /// View for tabular data based on a . @@ -57,7 +57,7 @@ namespace Terminal.Gui { private int selectedRow; private int selectedColumn; private DataTable table; - private TableStyle style = new TableStyle(); + private TableStyle style = new TableStyle (); /// /// The default maximum cell width for and @@ -67,29 +67,29 @@ namespace Terminal.Gui { /// /// The data table to render in the view. Setting this property automatically updates and redraws the control. /// - public DataTable Table { get => table; set {table = value; Update(); } } - + public DataTable Table { get => table; set { table = value; Update (); } } + /// /// Contains options for changing how the table is rendered /// - public TableStyle Style { get => style; set {style = value; Update(); } } - + public TableStyle Style { get => style; set { style = value; Update (); } } + /// /// True to select the entire row at once. False to select individual cells. Defaults to false /// - public bool FullRowSelect {get;set;} + public bool FullRowSelect { get; set; } /// /// True to allow regions to be selected /// /// - public bool MultiSelect {get;set;} = true; + public bool MultiSelect { get; set; } = true; /// /// When is enabled this property contain all rectangles of selected cells. Rectangles describe column/rows selected in (not screen coordinates) /// /// - public Stack MultiSelectedRegions {get;} = new Stack(); + public Stack MultiSelectedRegions { get; } = new Stack (); /// /// Horizontal scroll offset. The index of the first column in to display when when rendering the view. @@ -99,7 +99,7 @@ namespace Terminal.Gui { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = Table == null ? 0 :Math.Max (0,Math.Min (Table.Columns.Count - 1, value)); + set => columnOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); } /// @@ -107,7 +107,7 @@ namespace Terminal.Gui { /// public int RowOffset { get => rowOffset; - set => rowOffset = Table == null ? 0 : Math.Max (0,Math.Min (Table.Rows.Count - 1, value)); + set => rowOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); } /// @@ -120,11 +120,11 @@ namespace Terminal.Gui { var oldValue = selectedColumn; //try to prevent this being set to an out of bounds column - selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); - if(oldValue != selectedColumn) - OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,oldValue,SelectedColumn,SelectedRow,SelectedRow)); - } + if (oldValue != selectedColumn) + OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow)); + } } /// @@ -136,10 +136,10 @@ namespace Terminal.Gui { var oldValue = selectedRow; - selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); - if(oldValue != selectedRow) - OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,SelectedColumn,SelectedColumn,oldValue,selectedRow)); + if (oldValue != selectedRow) + OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow)); } } @@ -171,7 +171,7 @@ namespace Terminal.Gui { /// /// The key which when pressed should trigger event. Defaults to Enter. /// - public Key CellActivationKey {get;set;} = Key.Enter; + public Key CellActivationKey { get; set; } = Key.Enter; /// /// Initialzies a class using layout. @@ -197,51 +197,51 @@ namespace Terminal.Gui { var frame = Frame; // What columns to render at what X offset in viewport - var columnsToRender = CalculateViewport(bounds).ToArray(); + var columnsToRender = CalculateViewport (bounds).ToArray (); Driver.SetAttribute (ColorScheme.Normal); - + //invalidate current row (prevents scrolling around leaving old characters in the frame Driver.AddStr (new string (' ', bounds.Width)); int line = 0; - if(ShouldRenderHeaders()){ + if (ShouldRenderHeaders ()) { // Render something like: /* ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ └────────────────────┴──────────┴───────────┴──────────────┴─────────┘ */ - if(Style.ShowHorizontalHeaderOverline){ - RenderHeaderOverline(line,bounds.Width,columnsToRender); + if (Style.ShowHorizontalHeaderOverline) { + RenderHeaderOverline (line, bounds.Width, columnsToRender); line++; } - RenderHeaderMidline(line,columnsToRender); + RenderHeaderMidline (line, columnsToRender); line++; - if(Style.ShowHorizontalHeaderUnderline){ - RenderHeaderUnderline(line,bounds.Width,columnsToRender); + if (Style.ShowHorizontalHeaderUnderline) { + RenderHeaderUnderline (line, bounds.Width, columnsToRender); line++; } } - + int headerLinesConsumed = line; //render the cells for (; line < frame.Height; line++) { - ClearLine(line,bounds.Width); + ClearLine (line, bounds.Width); //work out what Row to render var rowToRender = RowOffset + (line - headerLinesConsumed); //if we have run off the end of the table - if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) + if (Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) continue; - RenderRow(line,rowToRender,columnsToRender); + RenderRow (line, rowToRender, columnsToRender); } } @@ -250,8 +250,8 @@ namespace Terminal.Gui { /// /// /// - private void ClearLine(int row, int width) - { + private void ClearLine (int row, int width) + { Move (0, row); Driver.SetAttribute (ColorScheme.Normal); Driver.AddStr (new string (' ', width)); @@ -261,188 +261,251 @@ namespace Terminal.Gui { /// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible. /// /// - private int GetHeaderHeightIfAny() + private int GetHeaderHeightIfAny () { - return ShouldRenderHeaders()? GetHeaderHeight():0; + return ShouldRenderHeaders () ? GetHeaderHeight () : 0; } /// /// Returns the amount of vertical space required to display the header /// /// - private int GetHeaderHeight() + private int GetHeaderHeight () { int heightRequired = 1; - - if(Style.ShowHorizontalHeaderOverline) + + if (Style.ShowHorizontalHeaderOverline) heightRequired++; - if(Style.ShowHorizontalHeaderUnderline) + if (Style.ShowHorizontalHeaderUnderline) heightRequired++; - + return heightRequired; } - private void RenderHeaderOverline(int row,int availableWidth, ColumnToRender[] columnsToRender) + private void RenderHeaderOverline (int row, int availableWidth, ColumnToRender [] columnsToRender) { // Renders a line above table headers (when visible) like: // ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ - for(int c = 0;c< availableWidth;c++) { + for (int c = 0; c < availableWidth; c++) { var rune = Driver.HLine; - if (Style.ShowVerticalHeaderLines){ - - if(c == 0){ + if (Style.ShowVerticalHeaderLines) { + + if (c == 0) { rune = Driver.ULCorner; - } + } // if the next column is the start of a header - else if(columnsToRender.Any(r=>r.X == c+1)){ + else if (columnsToRender.Any (r => r.X == c + 1)) { rune = Driver.TopTee; - } - else if(c == availableWidth -1){ + } 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)) { + // 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); + AddRuneAt (Driver, c, row, rune); } } - private void RenderHeaderMidline(int row, ColumnToRender[] columnsToRender) + private void RenderHeaderMidline (int row, ColumnToRender [] columnsToRender) { // Renders something like: // │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ - - ClearLine(row,Bounds.Width); + + ClearLine (row, Bounds.Width); //render start of line - if(style.ShowVerticalHeaderLines) - AddRune(0,row,Driver.VLine); + if (style.ShowVerticalHeaderLines) + AddRune (0, row, Driver.VLine); - for(int i =0 ; ir.X == c+1)){ - - /*TODO: is ┼ symbol in Driver?*/ - rune = Style.ShowVerticalCellLines ? '┼' :Driver.BottomTee; } - else if(c == availableWidth -1){ + // if the next column is the start of a header + else if (columnsToRender.Any (r => r.X == c + 1)) { + + /*TODO: is ┼ symbol in Driver?*/ + rune = Style.ShowVerticalCellLines ? '┼' : Driver.BottomTee; + } 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)) { + // 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); + AddRuneAt (Driver, c, row, rune); } - + } - private void RenderRow(int row, int rowToRender, ColumnToRender[] columnsToRender) + private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRender) { + var rowScheme = (Style.RowColorGetter?.Invoke ( + new RowColorGetterArgs(Table,rowToRender))) ?? ColorScheme; + //render start of line - if(style.ShowVerticalCellLines) - AddRune(0,row,Driver.VLine); + 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)); + Move (0, row); + Driver.SetAttribute (FullRowSelect && IsSelected (0, rowToRender) ? rowScheme.HotFocus : rowScheme.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++) { + for (int i = 0; i < columnsToRender.Length; i++) { - var current = columnsToRender[i]; + var current = columnsToRender [i]; - var colStyle = Style.GetColumnStyleIfAny(current.Column); + var colStyle = Style.GetColumnStyleIfAny (current.Column); // move to start of cell (in line with header positions) Move (current.X, row); // Set color scheme based on whether the current cell is the selected one - bool isSelectedCell = IsSelected(current.Column.Ordinal,rowToRender); + bool isSelectedCell = IsSelected (current.Column.Ordinal, rowToRender); - Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); - - var val = Table.Rows [rowToRender][current.Column]; + var val = Table.Rows [rowToRender] [current.Column]; // Render the (possibly truncated) cell value - var representation = GetRepresentation(val,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); + var representation = GetRepresentation (val, colStyle); - RenderSeparator(current.X-1,row,false); + // to get the colour scheme + var colorSchemeGetter = colStyle?.ColorGetter; + + ColorScheme scheme; + if(colorSchemeGetter != null) { + // user has a delegate for defining row color per cell, call it + scheme = colorSchemeGetter( + new CellColorGetterArgs (Table, rowToRender, current.Column.Ordinal, val, representation,rowScheme)); + + // if users custom color getter returned null, use the row scheme + if(scheme == null) { + scheme = rowScheme; + } + } + else { + // There is no custom cell coloring delegate so use the scheme for the row + scheme = rowScheme; + } + + var cellColor = isSelectedCell ? scheme.HotFocus : scheme.Normal; + + var render = TruncateOrPad (val, representation, current.Width, colStyle); + + // While many cells can be selected (see MultiSelectedRegions) only one cell is the primary (drives navigation etc) + bool isPrimaryCell = current.Column.Ordinal == selectedColumn && rowToRender == selectedRow; + + RenderCell (cellColor,render,isPrimaryCell); + + // Reset color scheme to normal for drawing separators if we drew text with custom scheme + if (scheme != rowScheme) { + Driver.SetAttribute (isSelectedCell ? rowScheme.HotFocus : rowScheme.Normal); + } + + // 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 (rowScheme.Normal); + + RenderSeparator (current.X - 1, row, false); if (Style.ExpandLastColumn == false && current.IsVeryLast) { - RenderSeparator (current.X + current.Width-1, row, false); + RenderSeparator (current.X + current.Width - 1, row, false); } } //render end of line - if(style.ShowVerticalCellLines) - AddRune(Bounds.Width-1,row,Driver.VLine); + if (style.ShowVerticalCellLines) + AddRune (Bounds.Width - 1, row, Driver.VLine); } - - private void RenderSeparator(int col, int row,bool isHeader) + + /// + /// Override to provide custom multi colouring to cells. Use to + /// with . The driver will already be + /// in the correct place when rendering and you must render the full + /// or the view will not look right. For simpler provision of color use + /// For changing the content that is rendered use + /// + /// + /// + /// + protected virtual void RenderCell (Attribute cellColor, string render,bool isPrimaryCell) { - if(col<0) + // If the cell is the selected col/row then draw the first rune in inverted colors + // this allows the user to track which cell is the active one during a multi cell + // selection or in full row select mode + if (Style.InvertSelectedCellFirstCharacter && isPrimaryCell) { + + if (render.Length > 0) { + // invert the color of the current cell for the first character + Driver.SetAttribute (Driver.MakeAttribute (cellColor.Background, cellColor.Foreground)); + Driver.AddRune (render [0]); + + if (render.Length > 1) { + Driver.SetAttribute (cellColor); + Driver.AddStr (render.Substring (1)); + } + } + } else { + Driver.SetAttribute (cellColor); + Driver.AddStr (render); + } + } + + private void RenderSeparator (int col, int row, bool isHeader) + { + if (col < 0) return; - + var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines; - Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol; - AddRune(col,row,symbol); + Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol; + AddRune (col, row, symbol); } - void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch) + void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) { Move (col, row); d.AddRune (ch); @@ -456,108 +519,108 @@ namespace Terminal.Gui { /// /// Optional style indicating custom alignment for the cell /// - private string TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle) + private string TruncateOrPad (object originalCellValue, string representation, int availableHorizontalSpace, ColumnStyle colStyle) { if (string.IsNullOrEmpty (representation)) return representation; // if value is not wide enough - if(representation.Sum(c=>Rune.ColumnWidth(c)) < availableHorizontalSpace) { - + if (representation.Sum (c => Rune.ColumnWidth (c)) < availableHorizontalSpace) { + // pad it out with spaces to the given alignment - int toPad = availableHorizontalSpace - (representation.Sum(c=>Rune.ColumnWidth(c)) +1 /*leave 1 space for cell boundary*/); + int toPad = availableHorizontalSpace - (representation.Sum (c => Rune.ColumnWidth (c)) + 1 /*leave 1 space for cell boundary*/); - switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) { + switch (colStyle?.GetAlignment (originalCellValue) ?? TextAlignment.Left) { - case TextAlignment.Left : - return representation + new string(' ',toPad); - case TextAlignment.Right : - return new string(' ',toPad) + representation; - - // TODO: With single line cells, centered and justified are the same right? - case TextAlignment.Centered : - case TextAlignment.Justified : - return - new string(' ',(int)Math.Floor(toPad/2.0)) + // round down - representation + - new string(' ',(int)Math.Ceiling(toPad/2.0)) ; // round up + case TextAlignment.Left: + return representation + new string (' ', toPad); + case TextAlignment.Right: + return new string (' ', toPad) + representation; + + // TODO: With single line cells, centered and justified are the same right? + case TextAlignment.Centered: + case TextAlignment.Justified: + return + new string (' ', (int)Math.Floor (toPad / 2.0)) + // round down + representation + + new string (' ', (int)Math.Ceiling (toPad / 2.0)); // round up } } // value is too wide - return new string(representation.TakeWhile(c=>(availableHorizontalSpace-= Rune.ColumnWidth(c))>0).ToArray()); + return new string (representation.TakeWhile (c => (availableHorizontalSpace -= Rune.ColumnWidth (c)) > 0).ToArray ()); } /// public override bool ProcessKey (KeyEvent keyEvent) { - if(Table == null){ + if (Table == null) { PositionCursor (); return false; } - if(keyEvent.Key == CellActivationKey && Table != null) { - OnCellActivated(new CellActivatedEventArgs(Table,SelectedColumn,SelectedRow)); + if (keyEvent.Key == CellActivationKey && Table != null) { + OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); return true; } switch (keyEvent.Key) { case Key.CursorLeft: case Key.CursorLeft | Key.ShiftMask: - ChangeSelectionByOffset(-1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (-1, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.CursorRight: case Key.CursorRight | Key.ShiftMask: - ChangeSelectionByOffset(1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (1, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.CursorDown: case Key.CursorDown | Key.ShiftMask: - ChangeSelectionByOffset(0,1,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (0, 1, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.CursorUp: case Key.CursorUp | Key.ShiftMask: - ChangeSelectionByOffset(0,-1,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (0, -1, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.PageUp: case Key.PageUp | Key.ShiftMask: - ChangeSelectionByOffset(0,-(Bounds.Height - GetHeaderHeightIfAny()),keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.PageDown: case Key.PageDown | Key.ShiftMask: - ChangeSelectionByOffset(0,Bounds.Height - GetHeaderHeightIfAny(),keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.Home | Key.CtrlMask: case Key.Home | Key.CtrlMask | Key.ShiftMask: // jump to table origin - SetSelection(0,0,keyEvent.Key.HasFlag(Key.ShiftMask)); + SetSelection (0, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.Home: case Key.Home | Key.ShiftMask: // jump to start of line - SetSelection(0,SelectedRow,keyEvent.Key.HasFlag(Key.ShiftMask)); + SetSelection (0, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.End | Key.CtrlMask: case Key.End | Key.CtrlMask | Key.ShiftMask: // jump to end of table - SetSelection(Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag(Key.ShiftMask)); + SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; case Key.A | Key.CtrlMask: - SelectAll(); + SelectAll (); Update (); break; case Key.End: case Key.End | Key.ShiftMask: //jump to end of row - SetSelection(Table.Columns.Count - 1,SelectedRow, keyEvent.Key.HasFlag(Key.ShiftMask)); + SetSelection (Table.Columns.Count - 1, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask)); Update (); break; default: @@ -576,24 +639,20 @@ namespace Terminal.Gui { /// True to create a multi cell selection or adjust an existing one public void SetSelection (int col, int row, bool extendExistingSelection) { - if(!MultiSelect || !extendExistingSelection) - MultiSelectedRegions.Clear(); + if (!MultiSelect || !extendExistingSelection) + MultiSelectedRegions.Clear (); - if(extendExistingSelection) - { + if (extendExistingSelection) { // If we are extending current selection but there isn't one - if(MultiSelectedRegions.Count == 0) - { + if (MultiSelectedRegions.Count == 0) { // Create a new region between the old active cell and the new cell - var rect = CreateTableSelection(SelectedColumn,SelectedRow,col,row); - MultiSelectedRegions.Push(rect); - } - else - { + var rect = CreateTableSelection (SelectedColumn, SelectedRow, col, row); + MultiSelectedRegions.Push (rect); + } else { // Extend the current head selection to include the new cell - var head = MultiSelectedRegions.Pop(); - var newRect = CreateTableSelection(head.Origin.X,head.Origin.Y,col,row); - MultiSelectedRegions.Push(newRect); + var head = MultiSelectedRegions.Pop (); + var newRect = CreateTableSelection (head.Origin.X, head.Origin.Y, col, row); + MultiSelectedRegions.Push (newRect); } } @@ -609,72 +668,66 @@ namespace Terminal.Gui { /// True to create a multi cell selection or adjust an existing one public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection) { - SetSelection(SelectedColumn + offsetX, SelectedRow + offsetY,extendExistingSelection); + SetSelection (SelectedColumn + offsetX, SelectedRow + offsetY, extendExistingSelection); } /// /// When is on, creates selection over all cells in the table (replacing any old selection regions) /// - public void SelectAll() + public void SelectAll () { - if(Table == null || !MultiSelect || Table.Rows.Count == 0) + if (Table == null || !MultiSelect || Table.Rows.Count == 0) return; - MultiSelectedRegions.Clear(); + MultiSelectedRegions.Clear (); // Create a single region over entire table, set the origin of the selection to the active cell so that a followup spread selection e.g. shift-right behaves properly - MultiSelectedRegions.Push(new TableSelection(new Point(SelectedColumn,SelectedRow), new Rect(0,0,Table.Columns.Count,table.Rows.Count))); - Update(); + MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns.Count, table.Rows.Count))); + Update (); } /// /// Returns all cells in any (if is enabled) and the selected cell /// /// - public IEnumerable GetAllSelectedCells() + public IEnumerable GetAllSelectedCells () { - if(Table == null || Table.Rows.Count == 0) + if (Table == null || Table.Rows.Count == 0) yield break; - EnsureValidSelection(); + EnsureValidSelection (); // If there are one or more rectangular selections - if(MultiSelect && MultiSelectedRegions.Any()){ - + if (MultiSelect && MultiSelectedRegions.Any ()) { + // Quiz any cells for whether they are selected. For performance we only need to check those between the top left and lower right vertex of selection regions - var yMin = MultiSelectedRegions.Min(r=>r.Rect.Top); - var yMax = MultiSelectedRegions.Max(r=>r.Rect.Bottom); + var yMin = MultiSelectedRegions.Min (r => r.Rect.Top); + var yMax = MultiSelectedRegions.Max (r => r.Rect.Bottom); - var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min(r=>r.Rect.Left); - var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max(r=>r.Rect.Right); + var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min (r => r.Rect.Left); + var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max (r => r.Rect.Right); - for(int y = yMin ; y < yMax ; y++) - { - for(int x = xMin ; x < xMax ; x++) - { - if(IsSelected(x,y)){ - yield return new Point(x,y); + for (int y = yMin; y < yMax; y++) { + for (int x = xMin; x < xMax; x++) { + if (IsSelected (x, y)) { + yield return new Point (x, y); } } } - } - else{ + } else { // if there are no region selections then it is just the active cell // if we are selecting the full row - if(FullRowSelect) - { + if (FullRowSelect) { // all cells in active row are selected - for(int x =0;x private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y) { - var top = Math.Min(pt1Y,pt2Y); - var bot = Math.Max(pt1Y,pt2Y); + var top = Math.Min (pt1Y, pt2Y); + var bot = Math.Max (pt1Y, pt2Y); - var left = Math.Min(pt1X,pt2X); - var right = Math.Max(pt1X,pt2X); + var left = Math.Min (pt1X, pt2X); + var right = Math.Max (pt1X, pt2X); // Rect class is inclusive of Top Left but exclusive of Bottom Right so extend by 1 - return new TableSelection(new Point(pt1X,pt1Y),new Rect(left,top,right-left + 1,bot-top + 1)); + return new TableSelection (new Point (pt1X, pt1Y), new Rect (left, top, right - left + 1, bot - top + 1)); } /// @@ -704,34 +757,34 @@ namespace Terminal.Gui { /// /// /// - public bool IsSelected(int col, int row) + public bool IsSelected (int col, int row) { // Cell is also selected if in any multi selection region - if(MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Contains(col,row))) + if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row))) return true; // Cell is also selected if Y axis appears in any region (when FullRowSelect is enabled) - if(FullRowSelect && MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Bottom> row && r.Rect.Top <= row)) + if (FullRowSelect && MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Bottom > row && r.Rect.Top <= row)) return true; - return row == SelectedRow && + return row == SelectedRow && (col == SelectedColumn || FullRowSelect); } /// /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc /// - public override void PositionCursor() + public override void PositionCursor () { - if(Table == null) { - base.PositionCursor(); + if (Table == null) { + base.PositionCursor (); return; } - - var screenPoint = CellToScreen(SelectedColumn,SelectedRow); - - if(screenPoint != null) - Move(screenPoint.Value.X,screenPoint.Value.Y); + + var screenPoint = CellToScreen (SelectedColumn, SelectedRow); + + if (screenPoint != null) + Move (screenPoint.Value.X, screenPoint.Value.Y); } /// @@ -751,48 +804,47 @@ namespace Terminal.Gui { } // Scroll wheel flags - switch(me.Flags) - { - case MouseFlags.WheeledDown: - RowOffset++; - EnsureValidScrollOffsets(); - SetNeedsDisplay(); - return true; + switch (me.Flags) { + case MouseFlags.WheeledDown: + RowOffset++; + EnsureValidScrollOffsets (); + SetNeedsDisplay (); + return true; - case MouseFlags.WheeledUp: - RowOffset--; - EnsureValidScrollOffsets(); - SetNeedsDisplay(); - return true; + case MouseFlags.WheeledUp: + RowOffset--; + EnsureValidScrollOffsets (); + SetNeedsDisplay (); + return true; - case MouseFlags.WheeledRight: - ColumnOffset++; - EnsureValidScrollOffsets(); - SetNeedsDisplay(); - return true; + case MouseFlags.WheeledRight: + ColumnOffset++; + EnsureValidScrollOffsets (); + SetNeedsDisplay (); + return true; - case MouseFlags.WheeledLeft: - ColumnOffset--; - EnsureValidScrollOffsets(); - SetNeedsDisplay(); - return true; + case MouseFlags.WheeledLeft: + ColumnOffset--; + EnsureValidScrollOffsets (); + SetNeedsDisplay (); + return true; } - if(me.Flags.HasFlag(MouseFlags.Button1Clicked)) { - - var hit = ScreenToCell(me.X,me.Y); - if(hit != null) { - - SetSelection(hit.Value.X,hit.Value.Y,me.Flags.HasFlag(MouseFlags.ButtonShift)); - Update(); + if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + + var hit = ScreenToCell (me.X, me.Y); + if (hit != null) { + + SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift)); + Update (); } } // Double clicking a cell activates - if(me.Flags == MouseFlags.Button1DoubleClicked) { - var hit = ScreenToCell(me.X,me.Y); - if(hit!= null) { - OnCellActivated(new CellActivatedEventArgs(Table,hit.Value.X,hit.Value.Y)); + if (me.Flags == MouseFlags.Button1DoubleClicked) { + var hit = ScreenToCell (me.X, me.Y); + if (hit != null) { + OnCellActivated (new CellActivatedEventArgs (Table, hit.Value.X, hit.Value.Y)); } } @@ -807,29 +859,29 @@ namespace Terminal.Gui { /// public Point? ScreenToCell (int clientX, int clientY) { - if(Table == null) + if (Table == null) return null; - var viewPort = CalculateViewport(Bounds); - - var headerHeight = GetHeaderHeightIfAny(); + var viewPort = CalculateViewport (Bounds); + + var headerHeight = GetHeaderHeightIfAny (); + + var col = viewPort.LastOrDefault (c => c.X <= clientX); - var col = viewPort.LastOrDefault(c=>c.X <= clientX); - // Click is on the header section of rendered UI - if(clientY < headerHeight) + if (clientY < headerHeight) return null; var rowIdx = RowOffset - headerHeight + clientY; - if(col != null && rowIdx >= 0) { - - return new Point(col.Column.Ordinal,rowIdx); + if (col != null && rowIdx >= 0) { + + return new Point (col.Column.Ordinal, rowIdx); } return null; } - + /// /// Returns the screen position (relative to the control client area) that the given cell is rendered or null if it is outside the current scroll area or no table is loaded /// @@ -838,44 +890,44 @@ namespace Terminal.Gui { /// public Point? CellToScreen (int tableColumn, int tableRow) { - if(Table == null) + if (Table == null) return null; - var viewPort = CalculateViewport(Bounds); - - var headerHeight = GetHeaderHeightIfAny(); + var viewPort = CalculateViewport (Bounds); - var colHit = viewPort.FirstOrDefault(c=>c.Column.Ordinal == tableColumn); + var headerHeight = GetHeaderHeightIfAny (); + + var colHit = viewPort.FirstOrDefault (c => c.Column.Ordinal == tableColumn); // current column is outside the scroll area - if(colHit == null) + if (colHit == null) return null; - + // the cell is too far up above the current scroll area - if(RowOffset > tableRow) + if (RowOffset > tableRow) return null; // the cell is way down below the scroll area and off the screen - if(tableRow > RowOffset + (Bounds.Height - headerHeight)) + if (tableRow > RowOffset + (Bounds.Height - headerHeight)) return null; - - return new Point(colHit.X,tableRow + headerHeight - RowOffset); + + return new Point (colHit.X, tableRow + headerHeight - RowOffset); } /// /// Updates the view to reflect changes to and to ( / ) etc /// /// This always calls - public void Update() + public void Update () { - if(Table == null) { + if (Table == null) { SetNeedsDisplay (); return; } - EnsureValidScrollOffsets(); - EnsureValidSelection(); + EnsureValidScrollOffsets (); + EnsureValidSelection (); - EnsureSelectedCellIsVisible(); + EnsureSelectedCellIsVisible (); SetNeedsDisplay (); } @@ -886,12 +938,12 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidScrollOffsets () { - if(Table == null){ + if (Table == null) { return; } - ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0); - RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0); + ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns.Count - 1), 0); + RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows.Count - 1), 0); } @@ -899,46 +951,45 @@ namespace Terminal.Gui { /// Updates , and where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if has not been set. /// /// Changes will not be immediately visible in the display until you call - public void EnsureValidSelection() + public void EnsureValidSelection () { - if(Table == null){ + if (Table == null) { // Table doesn't exist, we should probably clear those selections - MultiSelectedRegions.Clear(); + MultiSelectedRegions.Clear (); return; } - SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); - SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); + SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0); + SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0); - var oldRegions = MultiSelectedRegions.ToArray().Reverse(); + var oldRegions = MultiSelectedRegions.ToArray ().Reverse (); - MultiSelectedRegions.Clear(); + MultiSelectedRegions.Clear (); // evaluate - foreach(var region in oldRegions) - { + foreach (var region in oldRegions) { // ignore regions entirely below current table state - if(region.Rect.Top >= Table.Rows.Count) + if (region.Rect.Top >= Table.Rows.Count) continue; // ignore regions entirely too far right of table columns - if(region.Rect.Left >= Table.Columns.Count) + if (region.Rect.Left >= Table.Columns.Count) continue; // ensure region's origin exists - region.Origin = new Point( - Math.Max(Math.Min(region.Origin.X,Table.Columns.Count -1),0), - Math.Max(Math.Min(region.Origin.Y,Table.Rows.Count -1),0)); + region.Origin = new Point ( + Math.Max (Math.Min (region.Origin.X, Table.Columns.Count - 1), 0), + Math.Max (Math.Min (region.Origin.Y, Table.Rows.Count - 1), 0)); // ensure regions do not go over edge of table bounds - region.Rect = Rect.FromLTRB(region.Rect.Left, + region.Rect = Rect.FromLTRB (region.Rect.Left, region.Rect.Top, - Math.Max(Math.Min(region.Rect.Right, Table.Columns.Count ),0), - Math.Max(Math.Min(region.Rect.Bottom,Table.Rows.Count),0) + Math.Max (Math.Min (region.Rect.Right, Table.Columns.Count), 0), + Math.Max (Math.Min (region.Rect.Bottom, Table.Rows.Count), 0) ); - MultiSelectedRegions.Push(region); + MultiSelectedRegions.Push (region); } } @@ -949,12 +1000,12 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureSelectedCellIsVisible () { - if(Table == null || Table.Columns.Count <= 0){ + if (Table == null || Table.Columns.Count <= 0) { return; } - var columnsToRender = CalculateViewport (Bounds).ToArray(); - var headerHeight = GetHeaderHeightIfAny(); + var columnsToRender = CalculateViewport (Bounds).ToArray (); + var headerHeight = GetHeaderHeightIfAny (); //if we have scrolled too far to the left if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { @@ -962,7 +1013,7 @@ namespace Terminal.Gui { } //if we have scrolled too far to the right - if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) { + if (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { ColumnOffset = SelectedColumn; } @@ -979,18 +1030,18 @@ namespace Terminal.Gui { /// /// Invokes the event /// - protected virtual void OnSelectedCellChanged(SelectedCellChangedEventArgs args) + protected virtual void OnSelectedCellChanged (SelectedCellChangedEventArgs args) { - SelectedCellChanged?.Invoke(args); + SelectedCellChanged?.Invoke (args); } - + /// /// Invokes the event /// /// protected virtual void OnCellActivated (CellActivatedEventArgs args) { - CellActivated?.Invoke(args); + CellActivated?.Invoke (args); } /// @@ -1001,53 +1052,53 @@ namespace Terminal.Gui { /// private IEnumerable CalculateViewport (Rect bounds, int padding = 1) { - if(Table == null) + if (Table == null) yield break; - + int usedSpace = 0; //if horizontal space is required at the start of the line (before the first header) - if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines) - usedSpace+=1; - + if (Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines) + usedSpace += 1; + int availableHorizontalSpace = bounds.Width; int rowsToRender = bounds.Height; // reserved for the headers row - if(ShouldRenderHeaders()) - rowsToRender -= GetHeaderHeight(); + if (ShouldRenderHeaders ()) + rowsToRender -= GetHeaderHeight (); bool first = true; var lastColumn = Table.Columns.Cast ().Last (); - foreach (var col in Table.Columns.Cast().Skip (ColumnOffset)) { + foreach (var col in Table.Columns.Cast ().Skip (ColumnOffset)) { int startingIdxForCurrentHeader = usedSpace; - var colStyle = Style.GetColumnStyleIfAny(col); + var colStyle = Style.GetColumnStyleIfAny (col); int colWidth; // is there enough space for this column (and it's data)? - usedSpace += colWidth = 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), + Math.Min (availableHorizontalSpace, colWidth), lastColumn == col); - first=false; + first = false; } } - private bool ShouldRenderHeaders() + private bool ShouldRenderHeaders () { - if(Table == null || Table.Columns.Count == 0) + if (Table == null || Table.Columns.Count == 0) return false; - return Style.AlwaysShowHeaders || rowOffset == 0; + return Style.AlwaysShowHeaders || rowOffset == 0; } /// @@ -1057,37 +1108,37 @@ namespace Terminal.Gui { /// /// /// - private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle) + private int CalculateMaxCellWidth (DataColumn col, int rowsToRender, ColumnStyle colStyle) { - int spaceRequired = col.ColumnName.Sum(c=>Rune.ColumnWidth(c)); + int spaceRequired = col.ColumnName.Sum (c => Rune.ColumnWidth (c)); // if table has no rows - if(RowOffset < 0) + 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 - spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Sum(c=>Rune.ColumnWidth(c))); + spaceRequired = Math.Max (spaceRequired, GetRepresentation (Table.Rows [i] [col], colStyle).Sum (c => Rune.ColumnWidth (c))); } // Don't require more space than the style allows - if(colStyle != null){ + if (colStyle != null) { // enforce maximum cell width based on style - if(spaceRequired > colStyle.MaxWidth) { + if (spaceRequired > colStyle.MaxWidth) { spaceRequired = colStyle.MaxWidth; } // enforce minimum cell width based on style - if(spaceRequired < colStyle.MinWidth) { + if (spaceRequired < colStyle.MinWidth) { spaceRequired = colStyle.MinWidth; } } - + // enforce maximum cell width based on global table style - if(spaceRequired > MaxCellWidth) + if (spaceRequired > MaxCellWidth) spaceRequired = MaxCellWidth; @@ -1100,15 +1151,28 @@ namespace Terminal.Gui { /// /// Optional style defining how to represent cell values /// - private string GetRepresentation(object value,ColumnStyle colStyle) + private string GetRepresentation (object value, ColumnStyle colStyle) { if (value == null || value == DBNull.Value) { return NullSymbol; } - return colStyle != null ? colStyle.GetRepresentation(value): value.ToString(); + return colStyle != null ? colStyle.GetRepresentation (value) : value.ToString (); } + /// + /// Delegate for providing color to cells based on the value being rendered + /// + /// Contains information about the cell for which color is needed + /// + public delegate ColorScheme CellColorGetterDelegate (CellColorGetterArgs args); + + /// + /// Delegate for providing color for a whole row of a + /// + /// + /// + public delegate ColorScheme RowColorGetterDelegate (RowColorGetterArgs args); #region Nested Types /// @@ -1134,6 +1198,12 @@ namespace Terminal.Gui { /// public Func RepresentationGetter; + /// + /// Defines a delegate for returning a custom color scheme per cell based on cell values. + /// Return null for the default + /// + public CellColorGetterDelegate ColorGetter; + /// /// Defines the format for values e.g. "yyyy-MM-dd" for dates /// @@ -1214,11 +1284,23 @@ namespace Terminal.Gui { /// public bool ShowVerticalHeaderLines { get; set; } = true; + /// + /// True to invert the colors of the first symbol of the selected cell in the . + /// This gives the appearance of a cursor for when the doesn't otherwise show + /// this + /// + public bool InvertSelectedCellFirstCharacter { get; set; } = false; + /// /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc) /// public Dictionary ColumnStyles { get; set; } = new Dictionary (); + /// + /// Delegate for coloring specific rows in a different color. For cell color + /// + /// + public RowColorGetterDelegate RowColorGetter {get;set;} /// /// Determines rendering when the last column in the table is visible but it's @@ -1290,6 +1372,77 @@ namespace Terminal.Gui { } + /// + /// Arguments for a . Describes a cell for which a rendering + /// is being sought + /// + public class CellColorGetterArgs { + + /// + /// The data table hosted by the control. + /// + public DataTable Table { get; } + + /// + /// The index of the row in for which color is needed + /// + public int RowIndex { get; } + + /// + /// The index of column in for which color is needed + /// + public int ColIdex { get; } + + /// + /// The hard typed value being rendered in the cell for which color is needed + /// + public object CellValue { get; } + + /// + /// The textual representation of (what will actually be drawn to the screen) + /// + public string Representation { get; } + + /// + /// the color scheme that is going to be used to render the cell if no cell specific color scheme is returned + /// + public ColorScheme RowScheme { get; } + + internal CellColorGetterArgs (DataTable table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme) + { + Table = table; + RowIndex = rowIdx; + ColIdex = colIdx; + CellValue = cellValue; + Representation = representation; + RowScheme = rowScheme; + } + + } + + /// + /// Arguments for . Describes a row of data in a + /// for which is sought. + /// + public class RowColorGetterArgs { + + /// + /// The data table hosted by the control. + /// + public DataTable Table { get; } + + /// + /// The index of the row in for which color is needed + /// + public int RowIndex { get; } + + internal RowColorGetterArgs (DataTable table, int rowIdx) + { + Table = table; + RowIndex = rowIdx; + } + } + /// /// Defines the event arguments for /// diff --git a/UICatalog/Scenarios/MultiColouredTable.cs b/UICatalog/Scenarios/MultiColouredTable.cs new file mode 100644 index 000000000..98de29597 --- /dev/null +++ b/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= 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); + } + } + } + } +} diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 229cc7c71..0dead3a70 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/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); diff --git a/UnitTests/GraphViewTests.cs b/UnitTests/GraphViewTests.cs index 51273a981..774e295c5 100644 --- a/UnitTests/GraphViewTests.cs +++ b/UnitTests/GraphViewTests.cs @@ -116,6 +116,53 @@ namespace Terminal.Gui.Views { } } +#pragma warning disable xUnit1013 // Public method should be marked as test + /// + /// Verifies the console was rendered using the given at the given locations. + /// Pass a bitmap of indexes into as and the + /// test method will verify those colors were used in the row/col of the console during rendering + /// + /// Numbers between 0 and 9 for each row/col of the console. Must be valid indexes of + /// + 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] diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 25bf235d1..b16a77696 100644 --- a/UnitTests/TableViewTests.cs +++ b/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 () {