diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 1a69dec07..c1647437b 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -99,7 +99,7 @@ namespace Terminal.Gui { /// 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; private set; } = new Stack (); /// /// Horizontal scroll offset. The index of the first column in to display when when rendering the view. @@ -109,7 +109,7 @@ namespace Terminal.Gui { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = TableIsNullOrInvisible() ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); + set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); } /// @@ -186,7 +186,7 @@ namespace Terminal.Gui { set { if (cellActivationKey != value) { ReplaceKeyBinding (cellActivationKey, value); - + // of API user is mixing and matching old and new methods of keybinding then they may have lost // the old binding (e.g. with ClearKeybindings) so ReplaceKeyBinding alone will fail AddKeyBinding (value, Command.Accept); @@ -218,9 +218,9 @@ namespace Terminal.Gui { AddCommand (Command.LineDown, () => { ChangeSelectionByOffset (0, 1, false); return true; }); AddCommand (Command.PageUp, () => { PageUp (false); return true; }); AddCommand (Command.PageDown, () => { PageDown (false); return true; }); - AddCommand (Command.LeftHome, () => { ChangeSelectionToStartOfRow (false); return true; }); + AddCommand (Command.LeftHome, () => { ChangeSelectionToStartOfRow (false); return true; }); AddCommand (Command.RightEnd, () => { ChangeSelectionToEndOfRow (false); return true; }); - AddCommand (Command.TopHome, () => { ChangeSelectionToStartOfTable(false); return true; }); + AddCommand (Command.TopHome, () => { ChangeSelectionToStartOfTable (false); return true; }); AddCommand (Command.BottomEnd, () => { ChangeSelectionToEndOfTable (false); return true; }); AddCommand (Command.RightExtend, () => { ChangeSelectionByOffset (1, 0, true); return true; }); @@ -234,8 +234,10 @@ namespace Terminal.Gui { AddCommand (Command.TopHomeExtend, () => { ChangeSelectionToStartOfTable (true); return true; }); AddCommand (Command.BottomEndExtend, () => { ChangeSelectionToEndOfTable (true); return true; }); - AddCommand (Command.SelectAll, () => { SelectAll(); return true; }); - AddCommand (Command.Accept, () => { OnCellActivated(new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); return true; }); + AddCommand (Command.SelectAll, () => { SelectAll (); return true; }); + AddCommand (Command.Accept, () => { OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); return true; }); + + AddCommand (Command.ToggleChecked, () => { ToggleCurrentCellSelection (); return true; }); // Default keybindings for this view AddKeyBinding (Key.CursorLeft, Command.Left); @@ -252,7 +254,7 @@ namespace Terminal.Gui { AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); - AddKeyBinding (Key.CursorDown| Key.ShiftMask, Command.LineDownExtend); + AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend); @@ -264,33 +266,34 @@ namespace Terminal.Gui { AddKeyBinding (CellActivationKey, Command.Accept); } + /// public override void Redraw (Rect bounds) - { - Move (0, 0); - var frame = Frame; + { + Move (0, 0); + var frame = Frame; - scrollRightPoint = null; - scrollLeftPoint = null; + scrollRightPoint = null; + scrollLeftPoint = null; - // What columns to render at what X offset in viewport - var columnsToRender = CalculateViewport (bounds).ToArray (); + // What columns to render at what X offset in viewport + var columnsToRender = CalculateViewport (bounds).ToArray (); - Driver.SetAttribute (GetNormalColor ()); + Driver.SetAttribute (GetNormalColor ()); - //invalidate current row (prevents scrolling around leaving old characters in the frame - Driver.AddStr (new string (' ', bounds.Width)); + //invalidate current row (prevents scrolling around leaving old characters in the frame + Driver.AddStr (new string (' ', bounds.Width)); - int line = 0; + int line = 0; - if (ShouldRenderHeaders ()) { - // Render something like: - /* - ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ - │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ - └────────────────────┴──────────┴───────────┴──────────────┴─────────┘ - */ - if (Style.ShowHorizontalHeaderOverline) { + if (ShouldRenderHeaders ()) { + // Render something like: + /* + ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ + │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ + └────────────────────┴──────────┴───────────┴──────────────┴─────────┘ + */ + if (Style.ShowHorizontalHeaderOverline) { RenderHeaderOverline (line, bounds.Width, columnsToRender); line++; } @@ -436,19 +439,19 @@ namespace Terminal.Gui { bool moreColumnsToLeft = ColumnOffset > 0; // if we moved left would we find a new column (or are they all invisible?) - if(!TryGetNearestVisibleColumn (ColumnOffset-1, false, false, out _)) { + if (!TryGetNearestVisibleColumn (ColumnOffset - 1, false, false, out _)) { moreColumnsToLeft = false; } // are there visible columns to the right that have not yet been reached? // lets find out, what is the column index of the last column we are rendering int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1; - + // are there more valid indexes? bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count; // if we went right from the last column would we find a new visible column? - if(!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) { + if (!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) { // no we would not moreColumnsToRight = false; } @@ -466,7 +469,7 @@ namespace Terminal.Gui { // whole way but update to instead draw a header indicator // or scroll arrow etc var rune = Driver.HLine; - + if (Style.ShowVerticalHeaderLines) { if (c == 0) { // for first character render line @@ -475,12 +478,11 @@ namespace Terminal.Gui { // unless we have horizontally scrolled along // in which case render an arrow, to indicate user // can scroll left - if(Style.ShowHorizontalScrollIndicators && moreColumnsToLeft) - { + if (Style.ShowHorizontalScrollIndicators && moreColumnsToLeft) { rune = Driver.LeftArrow; - scrollLeftPoint = new Point(c,row); + scrollLeftPoint = new Point (c, row); } - + } // if the next column is the start of a header else if (columnsToRender.Any (r => r.X == c + 1)) { @@ -495,10 +497,9 @@ namespace Terminal.Gui { // unless there is more of the table we could horizontally // scroll along to see. In which case render an arrow, // to indicate user can scroll right - if(Style.ShowHorizontalScrollIndicators && moreColumnsToRight) - { + if (Style.ShowHorizontalScrollIndicators && moreColumnsToRight) { rune = Driver.RightArrow; - scrollRightPoint = new Point(c,row); + scrollRightPoint = new Point (c, row); } } @@ -518,7 +519,7 @@ namespace Terminal.Gui { var focused = HasFocus; var rowScheme = (Style.RowColorGetter?.Invoke ( - new RowColorGetterArgs(Table,rowToRender))) ?? ColorScheme; + new RowColorGetterArgs (Table, rowToRender))) ?? ColorScheme; //render start of line if (style.ShowVerticalCellLines) @@ -529,11 +530,9 @@ namespace Terminal.Gui { Attribute color; - if(FullRowSelect && IsSelected (0, rowToRender)) { + if (FullRowSelect && IsSelected (0, rowToRender)) { color = focused ? rowScheme.HotFocus : rowScheme.HotNormal; - } - else - { + } else { color = Enabled ? rowScheme.Normal : rowScheme.Disabled; } @@ -562,17 +561,16 @@ namespace Terminal.Gui { var colorSchemeGetter = colStyle?.ColorGetter; ColorScheme scheme; - if(colorSchemeGetter != null) { + 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)); + 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) { + if (scheme == null) { scheme = rowScheme; } - } - else { + } else { // There is no custom cell coloring delegate so use the scheme for the row scheme = rowScheme; } @@ -588,16 +586,15 @@ namespace Terminal.Gui { // 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); - + + RenderCell (cellColor, render, isPrimaryCell); + // Reset color scheme to normal for drawing separators if we drew text with custom scheme if (scheme != rowScheme) { - if(isSelectedCell) { + if (isSelectedCell) { color = focused ? rowScheme.HotFocus : rowScheme.HotNormal; - } - else { + } else { color = Enabled ? rowScheme.Normal : rowScheme.Disabled; } Driver.SetAttribute (color); @@ -629,7 +626,7 @@ namespace Terminal.Gui { /// /// /// - protected virtual void RenderCell (Attribute cellColor, string render,bool isPrimaryCell) + protected virtual void RenderCell (Attribute cellColor, string render, bool isPrimaryCell) { // 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 @@ -740,12 +737,15 @@ namespace Terminal.Gui { col = GetNearestVisibleColumn (col, lookRight, true); - if (!MultiSelect || !extendExistingSelection) - MultiSelectedRegions.Clear (); + if (!MultiSelect || !extendExistingSelection) { + ClearMultiSelectedRegions (true); + } + if (extendExistingSelection) { + // If we are extending current selection but there isn't one - if (MultiSelectedRegions.Count == 0) { + if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All(m=>m.IsToggled)) { // Create a new region between the old active cell and the new cell var rect = CreateTableSelection (SelectedColumn, SelectedRow, col, row); MultiSelectedRegions.Push (rect); @@ -761,6 +761,24 @@ namespace Terminal.Gui { SelectedRow = row; } + private void ClearMultiSelectedRegions (bool keepToggledSelections) + { + if (!keepToggledSelections) { + MultiSelectedRegions.Clear (); + return; + } + + var oldRegions = MultiSelectedRegions.ToArray ().Reverse (); + + MultiSelectedRegions.Clear (); + + foreach (var region in oldRegions) { + if (region.IsToggled) { + MultiSelectedRegions.Push (region); + } + } + } + /// /// Unions the current selected cell (and/or regions) with the provided cell and makes /// it the active one. @@ -769,10 +787,10 @@ namespace Terminal.Gui { /// private void UnionSelection (int col, int row) { - if (!MultiSelect || TableIsNullOrInvisible()) { + if (!MultiSelect || TableIsNullOrInvisible ()) { return; } - + EnsureValidSelection (); var oldColumn = SelectedColumn; @@ -812,7 +830,7 @@ namespace Terminal.Gui { /// Moves the selection up by one page /// /// true to extend the current selection (if any) instead of replacing - public void PageUp(bool extend) + public void PageUp (bool extend) { ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), extend); Update (); @@ -822,7 +840,7 @@ namespace Terminal.Gui { /// Moves the selection down by one page /// /// true to extend the current selection (if any) instead of replacing - public void PageDown(bool extend) + public void PageDown (bool extend) { ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), extend); Update (); @@ -846,7 +864,7 @@ namespace Terminal.Gui { /// to (,nY) i.e. no horizontal scrolling. /// /// true to extend the current selection (if any) instead of replacing - public void ChangeSelectionToEndOfTable(bool extend) + public void ChangeSelectionToEndOfTable (bool extend) { var finalColumn = Table.Columns.Count - 1; @@ -880,10 +898,10 @@ namespace Terminal.Gui { /// public void SelectAll () { - if (TableIsNullOrInvisible() || !MultiSelect || Table.Rows.Count == 0) + if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows.Count == 0) return; - MultiSelectedRegions.Clear (); + ClearMultiSelectedRegions (true); // 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))); @@ -893,16 +911,18 @@ namespace Terminal.Gui { /// /// Returns all cells in any (if is enabled) and the selected cell /// - /// Return value is not affected by (i.e. returned s are not expanded to - /// include all points on row). /// public IEnumerable GetAllSelectedCells () { if (TableIsNullOrInvisible () || Table.Rows.Count == 0) - yield break; + { + return Enumerable.Empty(); + } EnsureValidSelection (); + var toReturn = new HashSet(); + // If there are one or more rectangular selections if (MultiSelect && MultiSelectedRegions.Any ()) { @@ -916,25 +936,27 @@ namespace Terminal.Gui { for (int y = yMin; y < yMax; y++) { for (int x = xMin; x < xMax; x++) { if (IsSelected (x, y)) { - yield return new Point (x, y); + toReturn.Add(new Point (x, y)); } } } - } else { + } - // if there are no region selections then it is just the active cell - // if we are selecting the full row - if (FullRowSelect) { - // all cells in active row are selected - for (int x = 0; x < Table.Columns.Count; x++) { - yield return new Point (x, SelectedRow); - } - } else { - // Not full row select and no multi selections - yield return new Point (SelectedColumn, SelectedRow); + // if there are no region selections then it is just the active cell + + // if we are selecting the full row + if (FullRowSelect) { + // all cells in active row are selected + for (int x = 0; x < Table.Columns.Count; x++) { + toReturn.Add(new Point (x, SelectedRow)); } + } else { + // Not full row select and no multi selections + toReturn.Add(new Point (SelectedColumn, SelectedRow)); } + + return toReturn; } /// @@ -944,17 +966,60 @@ namespace Terminal.Gui { /// Origin point for the selection in Y /// End point for the selection in X /// End point for the selection in Y + /// True if selection is result of /// - private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y) + private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y, bool toggle = false) { - var top = Math.Max(Math.Min (pt1Y, pt2Y), 0); - var bot = Math.Max(Math.Max (pt1Y, pt2Y), 0); + var top = Math.Max (Math.Min (pt1Y, pt2Y), 0); + var bot = Math.Max (Math.Max (pt1Y, pt2Y), 0); - var left = Math.Max(Math.Min (pt1X, pt2X), 0); - var right = Math.Max(Math.Max (pt1X, pt2X), 0); + var left = Math.Max (Math.Min (pt1X, pt2X), 0); + var right = Math.Max (Math.Max (pt1X, pt2X), 0); // 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)) { + IsToggled = toggle + }; + } + + private void ToggleCurrentCellSelection () + { + if (!MultiSelect) { + return; + } + + var regions = GetMultiSelectedRegionsContaining(selectedColumn, selectedRow).ToArray(); + var toggles = regions.Where(s=>s.IsToggled).ToArray (); + + // Toggle it off + if (toggles.Any ()) { + + var oldRegions = MultiSelectedRegions.ToArray ().Reverse (); + MultiSelectedRegions.Clear (); + + foreach (var region in oldRegions) { + if (!toggles.Contains (region)) + MultiSelectedRegions.Push (region); + } + } else { + + // user is toggling selection within a rectangular + // select. So toggle the full region + if(regions.Any()) + { + foreach(var r in regions) + { + r.IsToggled = true; + } + } + else{ + // Toggle on a single cell selection + MultiSelectedRegions.Push ( + CreateTableSelection (selectedColumn, SelectedRow, selectedColumn, selectedRow, true) + ); + } + + } } /// @@ -978,22 +1043,36 @@ namespace Terminal.Gui { /// public bool IsSelected (int col, int row) { - if(!IsColumnVisible(col)) { + if (!IsColumnVisible (col)) { return false; - } + } - // Cell is also selected if in any multi selection region - 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(GetMultiSelectedRegionsContaining(col,row).Any()) + { return true; + } return row == SelectedRow && (col == SelectedColumn || FullRowSelect); } + private IEnumerable GetMultiSelectedRegionsContaining(int col, int row) + { + if(!MultiSelect) + { + return Enumerable.Empty(); + } + + if(FullRowSelect) + { + return MultiSelectedRegions.Where (r => r.Rect.Bottom > row && r.Rect.Top <= row); + } + else + { + return MultiSelectedRegions.Where (r => r.Rect.Contains (col, row)); + } + } + /// /// Returns true if the given indexes a visible /// column otherwise false. Returns false for indexes that are out of bounds. @@ -1071,19 +1150,17 @@ namespace Terminal.Gui { if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { - if (scrollLeftPoint != null + if (scrollLeftPoint != null && scrollLeftPoint.Value.X == me.X - && scrollLeftPoint.Value.Y == me.Y) - { + && scrollLeftPoint.Value.Y == me.Y) { ColumnOffset--; EnsureValidScrollOffsets (); SetNeedsDisplay (); } - if (scrollRightPoint != null + if (scrollRightPoint != null && scrollRightPoint.Value.X == me.X - && scrollRightPoint.Value.Y == me.Y) - { + && scrollRightPoint.Value.Y == me.Y) { ColumnOffset++; EnsureValidScrollOffsets (); SetNeedsDisplay (); @@ -1092,8 +1169,8 @@ namespace Terminal.Gui { var hit = ScreenToCell (me.X, me.Y); if (hit != null) { - if(MultiSelect && HasControlOrAlt(me)) { - UnionSelection(hit.Value.X, hit.Value.Y); + if (MultiSelect && HasControlOrAlt (me)) { + UnionSelection (hit.Value.X, hit.Value.Y); } else { SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift)); } @@ -1128,7 +1205,7 @@ namespace Terminal.Gui { /// Cell clicked or null. public Point? ScreenToCell (int clientX, int clientY) { - return ScreenToCell(clientX, clientY, out _); + return ScreenToCell (clientX, clientY, out _); } /// @@ -1153,7 +1230,7 @@ namespace Terminal.Gui { headerIfAny = col?.Column; return null; } - + var rowIdx = RowOffset - headerHeight + clientY; @@ -1161,7 +1238,7 @@ namespace Terminal.Gui { // invalid index back to user! if (rowIdx >= Table.Rows.Count) { return null; - } + } if (col != null && rowIdx >= 0) { @@ -1242,10 +1319,10 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidSelection () { - if (TableIsNullOrInvisible()) { + if (TableIsNullOrInvisible ()) { // Table doesn't exist, we should probably clear those selections - MultiSelectedRegions.Clear (); + ClearMultiSelectedRegions (false); return; } @@ -1315,8 +1392,7 @@ namespace Terminal.Gui { /// Use false if you are primarily interested in learning about directional column visibility. private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection) { - if(TryGetNearestVisibleColumn(columnIndex,lookRight,allowBumpingInOppositeDirection, out var answer)) - { + if (TryGetNearestVisibleColumn (columnIndex, lookRight, allowBumpingInOppositeDirection, out var answer)) { return answer; } @@ -1335,7 +1411,7 @@ namespace Terminal.Gui { // get the column visibility by index (if no style visible is true) bool [] columnVisibility = Table.Columns.Cast () .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true) - .ToArray(); + .ToArray (); // column is visible if (columnVisibility [columnIndex]) { @@ -1346,10 +1422,9 @@ namespace Terminal.Gui { int increment = lookRight ? 1 : -1; // move in that direction - for (int i = columnIndex; i >=0 && i < columnVisibility.Length; i += increment) { + for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) { // if we find a visible column - if(columnVisibility [i]) - { + if (columnVisibility [i]) { idx = i; return true; } @@ -1357,7 +1432,7 @@ namespace Terminal.Gui { // Caller only wants to look in one direction and we did not find any // visible columns in that direction - if(!allowBumpingInOppositeDirection) { + if (!allowBumpingInOppositeDirection) { idx = columnIndex; return false; } @@ -1400,10 +1475,10 @@ namespace Terminal.Gui { //if we have scrolled too far to the right if (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { - if(Style.SmoothHorizontalScrolling) { + if (Style.SmoothHorizontalScrolling) { // Scroll right 1 column at a time until the users selected column is visible - while(SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { + while (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { ColumnOffset++; columnsToRender = CalculateViewport (Bounds).ToArray (); @@ -1414,11 +1489,10 @@ namespace Terminal.Gui { break; } - } - else { + } else { ColumnOffset = SelectedColumn; } - + } //if we have scrolled too far down @@ -1482,7 +1556,7 @@ namespace Terminal.Gui { int colWidth; // if column is not being rendered - if(colStyle?.Visible == false) { + if (colStyle?.Visible == false) { // do not add it to the returned columns continue; } @@ -1492,16 +1566,14 @@ namespace Terminal.Gui { // there is not enough space for this columns // visible content - if (usedSpace + colWidth > availableHorizontalSpace) - { + 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 && + if (colStyle != null && colStyle.MinAcceptableWidth > 0 && // is there enough space to meet the MinAcceptableWidth - (availableHorizontalSpace - usedSpace) >= colStyle.MinAcceptableWidth) - { + (availableHorizontalSpace - usedSpace) >= colStyle.MinAcceptableWidth) { // show column and use use whatever space is // left for rendering it showColumn = true; @@ -1510,14 +1582,13 @@ namespace Terminal.Gui { // If its the only column we are able to render then // accept it anyway (that must be one massively wide column!) - if (first) - { + if (first) { showColumn = true; } // no special exceptions and we are out of space // so stop accepting new columns for the render area - if(!showColumn) + if (!showColumn) break; } @@ -1771,7 +1842,7 @@ namespace Terminal.Gui { /// Delegate for coloring specific rows in a different color. For cell color /// /// - public RowColorGetterDelegate RowColorGetter {get;set;} + public RowColorGetterDelegate RowColorGetter { get; set; } /// /// Determines rendering when the last column in the table is visible but it's @@ -1781,7 +1852,7 @@ namespace Terminal.Gui { /// and leave a blank column that cannot be selected in the remaining space. /// /// - public bool ExpandLastColumn {get;set;} = true; + public bool ExpandLastColumn { get; set; } = true; /// /// @@ -1798,7 +1869,7 @@ namespace Terminal.Gui { /// /// public bool SmoothHorizontalScrolling { get; set; } = true; - + /// /// Returns the entry from for the given or null if no custom styling is defined for it /// @@ -2003,6 +2074,12 @@ namespace Terminal.Gui { /// public Rect Rect { get; set; } + /// + /// True if the selection was made through + /// and therefore should persist even through keyboard navigation. + /// + public bool IsToggled { get; set; } + /// /// Creates a new selected area starting at the origin corner and covering the provided rectangular area /// diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 9f50ccbad..f34b81da4 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -148,6 +148,8 @@ namespace UICatalog.Scenarios { } } }; + + tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); } private void ShowAllColumns () diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index bb08a22ff..2bcc46d95 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -321,6 +321,8 @@ namespace Terminal.Gui.ViewTests { Bounds = new Rect (0, 0, 10, 5) }; + tableView.ChangeSelectionToEndOfTable(false); + // select the last row tableView.MultiSelectedRegions.Clear (); tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (0, 3), new Rect (0, 3, 4, 1))); @@ -1506,6 +1508,185 @@ namespace Terminal.Gui.ViewTests { Assert.DoesNotContain (new Point (1, 0), tableView.GetAllSelectedCells ()); } + + [Fact, AutoInitShutdown] + public void TestToggleCells_MultiSelectOn () + { + // 2 row table + var tableView = GetABCDEFTableView (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + + tableView.MultiSelect = true; + tableView.AddKeyBinding(Key.Space,Command.ToggleChecked); + + var selectedCell = tableView.GetAllSelectedCells().Single(); + Assert.Equal(0,selectedCell.X); + Assert.Equal(0,selectedCell.Y); + + // Go Right + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight }); + + selectedCell = tableView.GetAllSelectedCells().Single(); + Assert.Equal(1,selectedCell.X); + Assert.Equal(0,selectedCell.Y); + + // Toggle Select + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + var m = tableView.MultiSelectedRegions.Single(); + Assert.True(m.IsToggled); + Assert.Equal(1,m.Origin.X); + Assert.Equal(0,m.Origin.Y); + selectedCell = tableView.GetAllSelectedCells().Single(); + Assert.Equal(1,selectedCell.X); + Assert.Equal(0,selectedCell.Y); + + // Go Left + tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft }); + + // Both Toggled and Moved to should be selected + Assert.Equal(2,tableView.GetAllSelectedCells().Count()); + var s1 = tableView.GetAllSelectedCells().ElementAt(0); + var s2 = tableView.GetAllSelectedCells().ElementAt(1); + Assert.Equal(1,s1.X); + Assert.Equal(0,s1.Y); + Assert.Equal(0,s2.X); + Assert.Equal(0,s2.Y); + + // Go Down + tableView.ProcessKey (new KeyEvent { Key = Key.CursorDown }); + + // Both Toggled and Moved to should be selected but not 0,0 + // which we moved down from + Assert.Equal(2,tableView.GetAllSelectedCells().Count()); + s1 = tableView.GetAllSelectedCells().ElementAt(0); + s2 = tableView.GetAllSelectedCells().ElementAt(1); + Assert.Equal(1,s1.X); + Assert.Equal(0,s1.Y); + Assert.Equal(0,s2.X); + Assert.Equal(1,s2.Y); + + + // Go back to the toggled cell + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight}); + tableView.ProcessKey (new KeyEvent { Key = Key.CursorUp}); + + // Toggle off + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + + // Go Left + tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft}); + + selectedCell = tableView.GetAllSelectedCells().Single(); + Assert.Equal(0,selectedCell.X); + Assert.Equal(0,selectedCell.Y); + } + + [Fact, AutoInitShutdown] + public void TestToggleCells_MultiSelectOn_FullRowSelect () + { + // 2 row table + var tableView = GetABCDEFTableView (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + tableView.FullRowSelect = true; + tableView.MultiSelect = true; + tableView.AddKeyBinding(Key.Space,Command.ToggleChecked); + + // Toggle Select Cell 0,0 + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + + // Go Down + tableView.ProcessKey (new KeyEvent { Key = Key.CursorDown }); + + var m = tableView.MultiSelectedRegions.Single(); + Assert.True(m.IsToggled); + Assert.Equal(0,m.Origin.X); + Assert.Equal(0,m.Origin.Y); + + //First row toggled and Second row active = 12 selected cells + Assert.Equal(12,tableView.GetAllSelectedCells().Count()); + + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight }); + tableView.ProcessKey (new KeyEvent { Key = Key.CursorUp }); + + Assert.Single(tableView.MultiSelectedRegions.Where(r=>r.IsToggled)); + + // Can untoggle at 1,0 even though 0,0 was initial toggle because FullRowSelect is on + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + + Assert.Empty(tableView.MultiSelectedRegions.Where(r=>r.IsToggled)); + + } + + + [Fact, AutoInitShutdown] + public void TestToggleCells_MultiSelectOn_SquareSelectToggled () + { + // 3 row table + var tableView = GetABCDEFTableView (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + tableView.MultiSelect = true; + tableView.AddKeyBinding(Key.Space,Command.ToggleChecked); + + // Make a square selection + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorDown}); + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorRight}); + + Assert.Equal(4,tableView.GetAllSelectedCells().Count()); + + // Toggle the square selected region on + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + + // Go Right + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight }); + + //Toggled on square + the active cell (x=2,y=1) + Assert.Equal(5,tableView.GetAllSelectedCells().Count()); + Assert.Equal(2,tableView.SelectedColumn); + Assert.Equal(1,tableView.SelectedRow); + + // Untoggle the rectangular region by hitting toggle in + // any cell in that rect + tableView.ProcessKey (new KeyEvent { Key = Key.CursorUp }); + tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft }); + + Assert.Equal(4,tableView.GetAllSelectedCells().Count()); + tableView.ProcessKey (new KeyEvent { Key = Key.Space }); + Assert.Equal(1,tableView.GetAllSelectedCells().Count()); + } + + + + [Fact, AutoInitShutdown] + public void TestToggleCells_MultiSelectOn_Two_SquareSelects_BothToggled () + { + // 6 row table + var tableView = GetABCDEFTableView (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + dt.Rows.Add (1, 2, 3, 4, 5, 6); + tableView.MultiSelect = true; + tableView.AddKeyBinding(Key.Space,Command.ToggleChecked); + + // Make first square selection (0,0 to 1,1) + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorDown}); + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorRight}); + tableView.ProcessKey (new KeyEvent { Key = Key.Space}); + Assert.Equal(4,tableView.GetAllSelectedCells().Count()); + + // Make second square selection leaving 1 unselected line between them + tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft }); + tableView.ProcessKey (new KeyEvent { Key = Key.CursorDown }); + tableView.ProcessKey (new KeyEvent { Key = Key.CursorDown }); + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorDown}); + tableView.ProcessKey (new KeyEvent { Key = Key.ShiftMask | Key.CursorRight}); + + // 2 square selections + Assert.Equal(8,tableView.GetAllSelectedCells().Count()); + } + [Theory, AutoInitShutdown] [InlineData(new object[] { true,true })]