mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-29 01:07:58 +01:00
* 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:
@@ -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>
|
||||
|
||||
@@ -148,6 +148,8 @@ namespace UICatalog.Scenarios {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
|
||||
}
|
||||
|
||||
private void ShowAllColumns ()
|
||||
|
||||
@@ -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 })]
|
||||
|
||||
Reference in New Issue
Block a user