Fixes #1842 - Add support for toggling selected cells e.g. with space (#2386)

* Add support for toggling selected cells e.g. with space

* Make toggle cell selection work better with FullRowSelect

* Test for toggle cell selection

* Test and fix for full row select toggling

* Make toggle work with shift selected blocks

* Make extend selection ignore toggled regions when considering pushing a new head to selection stack
This commit is contained in:
Thomas Nind
2023-03-02 16:35:54 +00:00
committed by GitHub
parent 9360e86961
commit 705d4307e3
3 changed files with 392 additions and 132 deletions

View File

@@ -99,7 +99,7 @@ namespace Terminal.Gui {
/// When <see cref="MultiSelect"/> is enabled this property contain all rectangles of selected cells. Rectangles describe column/rows selected in <see cref="Table"/> (not screen coordinates)
/// </summary>
/// <returns></returns>
public Stack<TableSelection> MultiSelectedRegions { get; } = new Stack<TableSelection> ();
public Stack<TableSelection> MultiSelectedRegions { get; private set; } = new Stack<TableSelection> ();
/// <summary>
/// Horizontal scroll offset. The index of the first column in <see cref="Table"/> 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));
}
/// <summary>
@@ -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);
}
///<inheritdoc/>
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 {
/// <param name="cellColor"></param>
/// <param name="render"></param>
/// <param name="isPrimaryCell"></param>
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);
}
}
}
/// <summary>
/// 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 {
/// <param name="row"></param>
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
/// </summary>
/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
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
/// </summary>
/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
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 (<see cref="SelectedColumn"/>,nY) i.e. no horizontal scrolling.
/// </summary>
/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
public void ChangeSelectionToEndOfTable(bool extend)
public void ChangeSelectionToEndOfTable (bool extend)
{
var finalColumn = Table.Columns.Count - 1;
@@ -880,10 +898,10 @@ namespace Terminal.Gui {
/// </summary>
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 {
/// <summary>
/// Returns all cells in any <see cref="MultiSelectedRegions"/> (if <see cref="MultiSelect"/> is enabled) and the selected cell
/// </summary>
/// <remarks>Return value is not affected by <see cref="FullRowSelect"/> (i.e. returned <see cref="Point"/>s are not expanded to
/// include all points on row).</remarks>
/// <returns></returns>
public IEnumerable<Point> GetAllSelectedCells ()
{
if (TableIsNullOrInvisible () || Table.Rows.Count == 0)
yield break;
{
return Enumerable.Empty<Point>();
}
EnsureValidSelection ();
var toReturn = new HashSet<Point>();
// 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;
}
/// <summary>
@@ -944,17 +966,60 @@ namespace Terminal.Gui {
/// <param name="pt1Y">Origin point for the selection in Y</param>
/// <param name="pt2X">End point for the selection in X</param>
/// <param name="pt2Y">End point for the selection in Y</param>
/// <param name="toggle">True if selection is result of <see cref="Command.ToggleChecked"/></param>
/// <returns></returns>
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)
);
}
}
}
/// <summary>
@@ -978,22 +1043,36 @@ namespace Terminal.Gui {
/// <returns></returns>
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<TableSelection> GetMultiSelectedRegionsContaining(int col, int row)
{
if(!MultiSelect)
{
return Enumerable.Empty<TableSelection>();
}
if(FullRowSelect)
{
return MultiSelectedRegions.Where (r => r.Rect.Bottom > row && r.Rect.Top <= row);
}
else
{
return MultiSelectedRegions.Where (r => r.Rect.Contains (col, row));
}
}
/// <summary>
/// Returns true if the given <paramref name="columnIndex"/> 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 {
/// <returns>Cell clicked or null.</returns>
public Point? ScreenToCell (int clientX, int clientY)
{
return ScreenToCell(clientX, clientY, out _);
return ScreenToCell (clientX, clientY, out _);
}
/// <inheritdoc cref="ScreenToCell(int, int)"/>
@@ -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 {
/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
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.</param>
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<DataColumn> ()
.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 <see cref="ColumnStyle.ColorGetter"/>
/// </summary>
/// <value></value>
public RowColorGetterDelegate RowColorGetter {get;set;}
public RowColorGetterDelegate RowColorGetter { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <value></value>
public bool ExpandLastColumn {get;set;} = true;
public bool ExpandLastColumn { get; set; } = true;
/// <summary>
/// <para>
@@ -1798,7 +1869,7 @@ namespace Terminal.Gui {
/// </para>
/// </summary>
public bool SmoothHorizontalScrolling { get; set; } = true;
/// <summary>
/// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
/// </summary>
@@ -2003,6 +2074,12 @@ namespace Terminal.Gui {
/// <value></value>
public Rect Rect { get; set; }
/// <summary>
/// True if the selection was made through <see cref="Command.ToggleChecked"/>
/// and therefore should persist even through keyboard navigation.
/// </summary>
public bool IsToggled { get; set; }
/// <summary>
/// Creates a new selected area starting at the origin corner and covering the provided rectangular area
/// </summary>

View File

@@ -148,6 +148,8 @@ namespace UICatalog.Scenarios {
}
}
};
tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
}
private void ShowAllColumns ()

View File

@@ -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 })]