Fixes #2587 - Add CheckBoxTableSourceWrapper for TableView checkboxes (#2589)

* Add CheckBoxTableSourceWrapper

* Fix column offsets when there are checkboxes column

* Fix index

* Add CellToggledEventArgs and handle in CheckBoxTableSourceWrapper

* Add xmldoc for CheckBoxTableSourceWrapper

* Add tests and default keybinding for toggle to CheckBoxTableSourceWrapper

* Add unit tests for TableView checkboxes

* Split CheckBoxTableSource to two subclasses, one by index the other by object

* Add more tests for CheckBoxTableSourceWrapperByObject

* Refactor for readability

* Add UseRadioButtons

* Add test for radio buttons in table view

* Fix xmldoc

* Fix regression during radio refactoring

* Fix build errors for new glyph and draw method names

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
This commit is contained in:
Thomas Nind
2023-05-10 06:33:19 +01:00
committed by GitHub
parent 5317950928
commit 6cd79ada3a
8 changed files with 980 additions and 10 deletions

View File

@@ -0,0 +1,45 @@
using System;
namespace Terminal.Gui {
/// <summary>
/// Event args for the <see cref="TableView.CellToggled"/> event.
/// </summary>
public class CellToggledEventArgs : EventArgs
{
/// <summary>
/// The current table to which the new indexes refer. May be null e.g. if selection change is the result of clearing the table from the view
/// </summary>
/// <value></value>
public ITableSource Table { get; }
/// <summary>
/// The column index of the <see cref="Table"/> cell that is being toggled
/// </summary>
/// <value></value>
public int Col { get; }
/// <summary>
/// The row index of the <see cref="Table"/> cell that is being toggled
/// </summary>
/// <value></value>
public int Row { get; }
/// <summary>
/// Gets or sets whether to cancel the processing of this event
/// </summary>
public bool Cancel { get; set; }
/// <summary>
/// Creates a new instance of arguments describing a cell being toggled in <see cref="TableView"/>
/// </summary>
/// <param name="t"></param>
/// <param name="col"></param>
/// <param name="row"></param>
public CellToggledEventArgs (ITableSource t, int col, int row)
{
Table = t;
Col = col;
Row = row;
}
}
}

View File

@@ -0,0 +1,198 @@
using System;
using System.Data;
using System.Linq;
namespace Terminal.Gui {
/// <summary>
/// <see cref="ITableSource"/> for a <see cref="TableView"/> which adds a
/// checkbox column as an additional column in the table.
/// </summary>
/// <remarks>This class wraps another <see cref="ITableSource"/> and dynamically
/// serves its rows/cols plus an extra column. Data in the wrapped source can be
/// dynamic (change over time).</remarks>
public abstract class CheckBoxTableSourceWrapperBase : ITableSource {
private readonly TableView tableView;
/// <summary>
/// Creates a new instance of the class presenting the data in <paramref name="toWrap"/>
/// plus an additional checkbox column.
/// </summary>
/// <param name="tableView">The <see cref="TableView"/> this source will be used with.
/// This is required for event registration.</param>
/// <param name="toWrap">The original data source of the <see cref="TableView"/> that you
/// want to add checkboxes to.</param>
public CheckBoxTableSourceWrapperBase (TableView tableView, ITableSource toWrap)
{
this.Wrapping = toWrap;
this.tableView = tableView;
tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
tableView.MouseClick += TableView_MouseClick;
tableView.CellToggled += TableView_CellToggled;
}
/// <summary>
/// Gets or sets the character to use for checked entries. Defaults to <see cref="GlyphDefinitions.Checked"/>
/// </summary>
public Rune CheckedRune { get; set; } = CM.Glyphs.Checked;
/// <summary>
/// Gets or sets the character to use for UnChecked entries. Defaults to <see cref="GlyphDefinitions.UnChecked"/>
/// </summary>
public Rune UnCheckedRune { get; set; } = CM.Glyphs.UnChecked;
/// <summary>
/// Gets or sets whether to only allow a single row to be toggled at once (Radio button).
/// </summary>
public bool UseRadioButtons { get; set; }
/// <summary>
/// Gets or sets the character to use for checked entry when <see cref="UseRadioButtons"/> is true.
/// Defaults to <see cref="GlyphDefinitions.Selected"/>
/// </summary>
public Rune RadioCheckedRune { get; set; } = CM.Glyphs.Selected;
/// <summary>
/// Gets or sets the character to use for unchecked entries when <see cref="UseRadioButtons"/> is true.
/// Defaults to <see cref="GlyphDefinitions.UnSelected"/>
/// </summary>
public Rune RadioUnCheckedRune { get; set; } = CM.Glyphs.UnSelected;
/// <summary>
/// Gets the <see cref="ITableSource"/> that this instance is wrapping.
/// </summary>
public ITableSource Wrapping { get; }
/// <inheritdoc/>
public object this [int row, int col] {
get {
if (col == 0) {
if(UseRadioButtons) {
return IsChecked (row) ? RadioCheckedRune : RadioUnCheckedRune;
}
return IsChecked(row) ? CheckedRune : UnCheckedRune;
}
return Wrapping [row, col - 1];
}
}
/// <inheritdoc/>
public int Rows => Wrapping.Rows;
/// <inheritdoc/>
public int Columns => Wrapping.Columns + 1;
/// <inheritdoc/>
public string [] ColumnNames {
get {
var toReturn = Wrapping.ColumnNames.ToList ();
toReturn.Insert (0, " ");
return toReturn.ToArray ();
}
}
private void TableView_MouseClick (object sender, MouseEventEventArgs e)
{
// we only care about clicks (not movements)
if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button1Clicked)) {
return;
}
var hit = tableView.ScreenToCell (e.MouseEvent.X,e.MouseEvent.Y, out int? headerIfAny);
if(headerIfAny.HasValue && headerIfAny.Value == 0) {
// clicking in header with radio buttons does nothing
if(UseRadioButtons) {
return;
}
// otherwise it ticks all rows
ToggleAllRows ();
e.Handled = true;
tableView.SetNeedsDisplay ();
}
else
if(hit.HasValue && hit.Value.X == 0) {
if(UseRadioButtons) {
ClearAllToggles ();
ToggleRow (hit.Value.Y);
} else {
ToggleRow (hit.Value.Y);
}
e.Handled = true;
tableView.SetNeedsDisplay ();
}
}
private void TableView_CellToggled (object sender, CellToggledEventArgs e)
{
// Suppress default toggle behavior when using checkboxes
// and instead handle ourselves
var range = tableView.GetAllSelectedCells ().Select (c => c.Y).Distinct ().ToArray();
if(UseRadioButtons) {
// multi selection makes it unclear what to toggle in this situation
if(range.Length != 1) {
e.Cancel = true;
return;
}
ClearAllToggles ();
ToggleRow (range.Single ());
}
else {
ToggleRows (range);
}
e.Cancel = true;
tableView.SetNeedsDisplay ();
}
/// <summary>
/// Returns true if <paramref name="row"/> is checked.
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
protected abstract bool IsChecked (int row);
/// <summary>
/// Flips the checked state for a collection of rows. If
/// some (but not all) are selected they should flip to all
/// selected.
/// </summary>
/// <param name="range"></param>
protected abstract void ToggleRows (int [] range);
/// <summary>
/// Flips the checked state of the given <paramref name="row"/>/
/// </summary>
/// <param name="row"></param>
protected abstract void ToggleRow (int row);
/// <summary>
/// Called when the 'toggled all' action is performed.
/// This should change state from 'some selected' to
/// 'all selected' or clear selection if all area already
/// selected.
/// </summary>
protected abstract void ToggleAllRows ();
/// <summary>
/// Clears the toggled state of all rows.
/// </summary>
protected abstract void ClearAllToggles ();
}
}

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
namespace Terminal.Gui {
/// <summary>
/// Implementation of <see cref="CheckBoxTableSourceWrapperBase"/> which records toggled rows
/// by their row number.
/// </summary>
public class CheckBoxTableSourceWrapperByIndex : CheckBoxTableSourceWrapperBase {
/// <inheritdoc/>
public CheckBoxTableSourceWrapperByIndex (TableView tableView, ITableSource toWrap) : base (tableView, toWrap)
{
}
/// <summary>
/// Gets the collection of all the checked rows in the
/// <see cref="CheckBoxTableSourceWrapperBase.Wrapping"/> <see cref="ITableSource"/>.
/// </summary>
public HashSet<int> CheckedRows { get; private set; } = new HashSet<int> ();
/// <inheritdoc/>
protected override bool IsChecked (int row)
{
return CheckedRows.Contains (row);
}
/// <inheritdoc/>
protected override void ToggleRows (int [] range)
{
// if all are ticked untick them
if (range.All (CheckedRows.Contains)) {
// select none
foreach (var r in range) {
CheckedRows.Remove (r);
}
} else {
// otherwise tick all
foreach (var r in range) {
CheckedRows.Add (r);
}
}
}
/// <inheritdoc/>
protected override void ToggleRow (int row)
{
if (CheckedRows.Contains (row)) {
CheckedRows.Remove (row);
} else {
CheckedRows.Add (row);
}
}
/// <inheritdoc/>
protected override void ToggleAllRows ()
{
if (CheckedRows.Count == Rows) {
// select none
ClearAllToggles ();
} else {
// select all
CheckedRows = new HashSet<int> (Enumerable.Range (0, Rows));
}
}
/// <inheritdoc/>
protected override void ClearAllToggles ()
{
CheckedRows.Clear ();
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Linq;
namespace Terminal.Gui {
/// <summary>
/// Implementation of <see cref="CheckBoxTableSourceWrapperBase"/> which records toggled rows
/// by a property on row objects.
/// </summary>
public class CheckBoxTableSourceWrapperByObject<T> : CheckBoxTableSourceWrapperBase {
private readonly EnumerableTableSource<T> toWrap;
readonly Func<T, bool> getter;
readonly Action<T, bool> setter;
/// <summary>
/// Creates a new instance of the class wrapping the collection <see cref="toWrap"/>.
/// </summary>
/// <param name="tableView">The table you will use the source with.</param>
/// <param name="toWrap">The collection of objects you will record checked state for</param>
/// <param name="getter">Delegate method for retrieving checked state from your objects of type <typeparamref name="T"/>.</param>
/// <param name="setter">Delegate method for setting new checked states on your objects of type <typeparamref name="T"/>.</param>
public CheckBoxTableSourceWrapperByObject (
TableView tableView,
EnumerableTableSource<T> toWrap,
Func<T,bool> getter,
Action<T,bool> setter) : base (tableView, toWrap)
{
this.toWrap = toWrap;
this.getter = getter;
this.setter = setter;
}
/// <inheritdoc/>
protected override bool IsChecked (int row)
{
return getter (toWrap.Data.ElementAt (row));
}
/// <inheritdoc/>
protected override void ToggleAllRows ()
{
ToggleRows (Enumerable.Range (0, toWrap.Rows).ToArray());
}
/// <inheritdoc/>
protected override void ToggleRow (int row)
{
var d = toWrap.Data.ElementAt (row);
setter (d, !getter(d));
}
/// <inheritdoc/>
protected override void ToggleRows (int [] range)
{
// if all are ticked untick them
if (range.All (IsChecked)) {
// select none
foreach(var r in range) {
setter (toWrap.Data.ElementAt (r), false);
}
} else {
// otherwise tick all
foreach (var r in range) {
setter (toWrap.Data.ElementAt (r), true);
}
}
}
/// <inheritdoc/>
protected override void ClearAllToggles ()
{
foreach (var e in toWrap.Data) {
setter (e, false);
}
}
}
}

View File

@@ -50,5 +50,10 @@ namespace Terminal.Gui {
/// <inheritdoc/>
public string [] ColumnNames => cols;
/// <summary>
/// Gets the object collection hosted by this wrapper.
/// </summary>
public IReadOnlyCollection<T> Data => this.data.AsReadOnly();
}
}

View File

@@ -151,6 +151,11 @@ namespace Terminal.Gui {
/// </summary>
public event EventHandler<CellActivatedEventArgs> CellActivated;
/// <summary>
/// This event is raised when a cell is toggled (see <see cref="Command.ToggleChecked"/>
/// </summary>
public event EventHandler<CellToggledEventArgs> CellToggled;
/// <summary>
/// The key which when pressed should trigger <see cref="CellActivated"/> event. Defaults to Enter.
/// </summary>
@@ -1046,6 +1051,13 @@ namespace Terminal.Gui {
private void ToggleCurrentCellSelection ()
{
var e = new CellToggledEventArgs (Table, selectedColumn, selectedRow);
OnCellToggled (e);
if (e.Cancel) {
return;
}
if (!MultiSelect) {
return;
}
@@ -1578,6 +1590,14 @@ namespace Terminal.Gui {
{
CellActivated?.Invoke (this, args);
}
/// <summary>
/// Invokes the <see cref="CellToggled"/> event
/// </summary>
/// <param name="args"></param>
protected virtual void OnCellToggled(CellToggledEventArgs args)
{
CellToggled?.Invoke (this, args);
}
/// <summary>
/// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>

View File

@@ -17,6 +17,7 @@ namespace UICatalog.Scenarios {
public class TableEditor : Scenario {
TableView tableView;
DataTable currentTable;
private MenuItem _miShowHeaders;
private MenuItem _miAlwaysShowHeaders;
private MenuItem _miHeaderOverline;
@@ -31,6 +32,8 @@ namespace UICatalog.Scenarios {
private MenuItem _miAlternatingColors;
private MenuItem _miCursor;
private MenuItem _miBottomline;
private MenuItem _miCheckboxes;
private MenuItem _miRadioboxes;
ColorScheme redColorScheme;
ColorScheme redColorSchemeAlt;
@@ -73,6 +76,8 @@ namespace UICatalog.Scenarios {
_miSmoothScrolling = new MenuItem ("_SmoothHorizontalScrolling", "", () => ToggleSmoothScrolling()){Checked = tableView.Style.SmoothHorizontalScrolling, CheckType = MenuItemCheckStyle.Checked },
new MenuItem ("_AllLines", "", () => ToggleAllCellLines()),
new MenuItem ("_NoLines", "", () => ToggleNoCellLines()),
_miCheckboxes = new MenuItem ("_Checkboxes", "", () => ToggleCheckboxes(false)){Checked = false, CheckType = MenuItemCheckStyle.Checked },
_miRadioboxes = new MenuItem ("_Radioboxes", "", () => ToggleCheckboxes(true)){Checked = false, CheckType = MenuItemCheckStyle.Checked },
_miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors()){CheckType = MenuItemCheckStyle.Checked},
_miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter()){Checked = tableView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked},
new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()),
@@ -170,6 +175,12 @@ namespace UICatalog.Scenarios {
{
var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
// don't try to sort on the toggled column
if (HasCheckboxes () && clickedCol == 0) {
return;
}
SortColumn (clickedCol, sort, isAsc);
}
@@ -211,7 +222,7 @@ namespace UICatalog.Scenarios {
{
// work out new sort order
var sort = currentTable.DefaultView.Sort;
var colName = currentTable.Columns[clickedCol];
var colName = tableView.Table.ColumnNames[clickedCol];
if (sort?.EndsWith ("ASC") ?? false) {
sort = $"{colName} DESC";
@@ -226,6 +237,10 @@ namespace UICatalog.Scenarios {
private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
{
if(HasCheckboxes() && clickedCol == 0) {
return;
}
var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
var colName = tableView.Table.ColumnNames[clickedCol];
@@ -246,7 +261,7 @@ namespace UICatalog.Scenarios {
tableView.Update ();
}
private DataColumn GetColumn ()
private int? GetColumn ()
{
if (tableView.Table == null)
return null;
@@ -254,7 +269,7 @@ namespace UICatalog.Scenarios {
if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns)
return null;
return currentTable.Columns [tableView.SelectedColumn];
return tableView.SelectedColumn;
}
private void SetMinAcceptableWidthToOne ()
@@ -282,8 +297,12 @@ namespace UICatalog.Scenarios {
RunColumnWidthDialog (col, "MaxWidth", (s, v) => s.MaxWidth = v, (s) => s.MaxWidth);
}
private void RunColumnWidthDialog (DataColumn col, string prompt, Action<ColumnStyle, int> setter, Func<ColumnStyle, int> getter)
private void RunColumnWidthDialog (int? col, string prompt, Action<ColumnStyle, int> setter, Func<ColumnStyle, int> getter)
{
if(col == null) {
return;
}
var accepted = false;
var ok = new Button ("Ok", is_default: true);
ok.Clicked += (s,e) => { accepted = true; Application.RequestStop (); };
@@ -291,12 +310,12 @@ namespace UICatalog.Scenarios {
cancel.Clicked += (s,e) => { Application.RequestStop (); };
var d = new Dialog (ok, cancel) { Title = prompt };
var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
var style = tableView.Style.GetOrCreateColumnStyle (col.Value);
var lbl = new Label () {
X = 0,
Y = 1,
Text = col.ColumnName
Text = tableView.Table.ColumnNames[col.Value]
};
var tf = new TextField () {
@@ -441,6 +460,42 @@ namespace UICatalog.Scenarios {
}
private void ToggleCheckboxes (bool radio)
{
if (tableView.Table is CheckBoxTableSourceWrapperByIndex wrapper) {
// unwrap it to remove check boxes
tableView.Table = wrapper.Wrapping;
_miCheckboxes.Checked = false;
_miRadioboxes.Checked = false;
// if toggling off checkboxes/radio
if(wrapper.UseRadioButtons == radio) {
return;
}
}
// Either toggling on checkboxes/radio or switching from radio to checkboxes (or vice versa)
var source = new CheckBoxTableSourceWrapperByIndex (tableView, tableView.Table) {
UseRadioButtons = radio
};
tableView.Table = source;
if (radio) {
_miRadioboxes.Checked = true;
_miCheckboxes.Checked = false;
}
else {
_miRadioboxes.Checked = false;
_miCheckboxes.Checked = true;
}
}
private void ToggleAlwaysUseNormalColorForVerticalCellLines()
{
_miAlwaysUseNormalColorForVerticalCellLines.Checked = !_miAlwaysUseNormalColorForVerticalCellLines.Checked;
@@ -787,11 +842,17 @@ namespace UICatalog.Scenarios {
{
if (e.Table == null)
return;
var o = currentTable.Rows [e.Row] [e.Col];
var tableCol = ToTableCol(e.Col);
if (tableCol < 0) {
return;
}
var o = currentTable.Rows [e.Row] [tableCol];
var title = o is uint u ? GetUnicodeCategory (u) + $"(0x{o:X4})" : "Enter new value";
var oldValue = currentTable.Rows [e.Row] [e.Col].ToString ();
var oldValue = currentTable.Rows [e.Row] [tableCol].ToString ();
bool okPressed = false;
var ok = new Button ("Ok", is_default: true);
@@ -803,7 +864,7 @@ namespace UICatalog.Scenarios {
var lbl = new Label () {
X = 0,
Y = 1,
Text = currentTable.Columns [e.Col].ColumnName
Text = tableView.Table.ColumnNames[e.Col]
};
var tf = new TextField () {
@@ -821,7 +882,7 @@ namespace UICatalog.Scenarios {
if (okPressed) {
try {
currentTable.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text;
currentTable.Rows [e.Row] [tableCol] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text;
} catch (Exception ex) {
MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
}
@@ -830,6 +891,20 @@ namespace UICatalog.Scenarios {
}
}
private int ToTableCol (int col)
{
if (HasCheckboxes ()) {
return col - 1;
}
return col;
}
private bool HasCheckboxes ()
{
return tableView.Table is CheckBoxTableSourceWrapperBase;
}
private string GetUnicodeCategory (uint u)
{
return Ranges.FirstOrDefault (r => u >= r.Start && u <= r.End)?.Category ?? "Unknown";

View File

@@ -2288,6 +2288,446 @@ namespace Terminal.Gui.ViewsTests {
TestHelpers.AssertDriverColorsAre (expected, normal, focus);
}
[Fact, AutoInitShutdown]
public void TestTableViewCheckboxes_Simple()
{
var tv = GetTwoRowSixColumnTable (out var dt);
dt.Rows.Add (1, 2, 3, 4, 5, 6);
tv.LayoutSubviews ();
var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table);
tv.Table = wrapper;
tv.Draw ();
string expected =
@"
│ │A│B│
├─┼─┼─►
│╴│1│2│
│╴│1│2│
│╴│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Empty (wrapper.CheckedRows);
//toggle the top cell
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.Single (wrapper.CheckedRows, 0);
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│√│1│2│
│╴│1│2│
│╴│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.Contains (0,wrapper.CheckedRows);
Assert.Contains (1,wrapper.CheckedRows);
Assert.Equal (2, wrapper.CheckedRows.Count);
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│√│1│2│
│√│1│2│
│╴│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
// untoggle top one
tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.Single (wrapper.CheckedRows, 1);
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│╴│1│2│
│√│1│2│
│╴│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
}
[Fact, AutoInitShutdown]
public void TestTableViewCheckboxes_SelectAllToggle ()
{
var tv = GetTwoRowSixColumnTable (out var dt);
dt.Rows.Add (1, 2, 3, 4, 5, 6);
tv.LayoutSubviews ();
var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table);
tv.Table = wrapper;
//toggle all cells
tv.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers { Ctrl = true }));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
tv.Draw();
string expected =
@"
│ │A│B│
├─┼─┼─►
│√│1│2│
│√│1│2│
│√│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Contains (0, wrapper.CheckedRows);
Assert.Contains (1, wrapper.CheckedRows);
Assert.Contains (2, wrapper.CheckedRows);
Assert.Equal (3, wrapper.CheckedRows.Count);
// Untoggle all again
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│╴│1│2│
│╴│1│2│
│╴│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Empty (wrapper.CheckedRows);
}
[Fact, AutoInitShutdown]
public void TestTableViewCheckboxes_MultiSelectIsUnion_WhenToggling ()
{
var tv = GetTwoRowSixColumnTable (out var dt);
dt.Rows.Add (1, 2, 3, 4, 5, 6);
tv.LayoutSubviews ();
var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table);
tv.Table = wrapper;
wrapper.CheckedRows.Add (0);
wrapper.CheckedRows.Add (2);
tv.Draw();
string expected =
@"
│ │A│B│
├─┼─┼─►
│√│1│2│
│╴│1│2│
│√│1│2│";
//toggle top two at once
tv.ProcessKey (new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers { Shift = true }));
Assert.True (tv.IsSelected (0, 0));
Assert.True (tv.IsSelected (0, 1));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
// Because at least 1 of the rows is not yet ticked we toggle them all to ticked
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Contains (0, wrapper.CheckedRows);
Assert.Contains (1, wrapper.CheckedRows);
Assert.Contains (2, wrapper.CheckedRows);
Assert.Equal (3, wrapper.CheckedRows.Count);
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│√│1│2│
│√│1│2│
│√│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
// Untoggle the top 2
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
tv.Draw();
expected =
@"
│ │A│B│
├─┼─┼─►
│╴│1│2│
│╴│1│2│
│√│1│2│";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Single (wrapper.CheckedRows, 2);
}
[Fact, AutoInitShutdown]
public void TestTableViewCheckboxes_ByObject ()
{
var tv = GetPetTable (out var source);
tv.LayoutSubviews ();
var pets = source.Data;
var wrapper = new CheckBoxTableSourceWrapperByObject<PickablePet>(
tv,
source,
(p)=>p.IsPicked,
(p,b)=>p.IsPicked = b);
tv.Table = wrapper;
tv.Draw();
string expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│╴│Tammy │Cat │
│╴│Tibbles│Cat │
│╴│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Empty (pets.Where(p=>p.IsPicked));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.True (pets.First ().IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│√│Tammy │Cat │
│╴│Tibbles│Cat │
│╴│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.True (pets.ElementAt(0).IsPicked);
Assert.True (pets.ElementAt (1).IsPicked);
Assert.False (pets.ElementAt (2).IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│√│Tammy │Cat │
│√│Tibbles│Cat │
│╴│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.False (pets.ElementAt (0).IsPicked);
Assert.True (pets.ElementAt (1).IsPicked);
Assert.False (pets.ElementAt (2).IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│╴│Tammy │Cat │
│√│Tibbles│Cat │
│╴│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
}
[Fact, AutoInitShutdown]
public void TestTableViewCheckboxes_SelectAllToggle_ByObject ()
{
var tv = GetPetTable (out var source);
tv.LayoutSubviews ();
var pets = source.Data;
var wrapper = new CheckBoxTableSourceWrapperByObject<PickablePet> (
tv,
source,
(p) => p.IsPicked,
(p, b) => p.IsPicked = b);
tv.Table = wrapper;
Assert.DoesNotContain (pets, p => p.IsPicked);
//toggle all cells
tv.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers { Ctrl = true }));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.True (pets.All (p => p.IsPicked));
tv.Draw();
string expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│√│Tammy │Cat │
│√│Tibbles│Cat │
│√│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.Empty (pets.Where (p => p.IsPicked));
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│╴│Tammy │Cat │
│╴│Tibbles│Cat │
│╴│Ripper │Dog │
";
TestHelpers.AssertDriverContentsAre (expected, output);
}
[Fact, AutoInitShutdown]
public void TestTableViewRadioBoxes_Simple_ByObject ()
{
var tv = GetPetTable (out var source);
tv.LayoutSubviews ();
var pets = source.Data;
var wrapper = new CheckBoxTableSourceWrapperByObject<PickablePet> (
tv,
source,
(p) => p.IsPicked,
(p, b) => p.IsPicked = b);
wrapper.UseRadioButtons = true;
tv.Table = wrapper;
tv.Draw();
string expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│◌│Tammy │Cat │
│◌│Tibbles│Cat │
│◌│Ripper │Dog │
";
TestHelpers.AssertDriverContentsAre (expected, output);
Assert.Empty (pets.Where (p => p.IsPicked));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.True (pets.First ().IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│●│Tammy │Cat │
│◌│Tibbles│Cat │
│◌│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.False (pets.ElementAt (0).IsPicked);
Assert.True (pets.ElementAt (1).IsPicked);
Assert.False (pets.ElementAt (2).IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│◌│Tammy │Cat │
│●│Tibbles│Cat │
│◌│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()));
tv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()));
Assert.True (pets.ElementAt (0).IsPicked);
Assert.False (pets.ElementAt (1).IsPicked);
Assert.False (pets.ElementAt (2).IsPicked);
tv.Draw();
expected =
@"
┌─┬───────┬─────────────┐
│ │Name │Kind │
├─┼───────┼─────────────┤
│●│Tammy │Cat │
│◌│Tibbles│Cat │
│◌│Ripper │Dog │";
TestHelpers.AssertDriverContentsAre (expected, output);
}
[Fact, AutoInitShutdown]
public void TestFullRowSelect_SelectionColorDoesNotStop_WhenShowVerticalCellLinesIsFalse ()
{
@@ -2763,5 +3203,43 @@ A B C
tableView.Table = new DataTableSource (dt);
return tableView;
}
private class PickablePet {
public bool IsPicked { get; set; }
public string Name{ get; set; }
public string Kind { get; set; }
public PickablePet (bool isPicked, string name, string kind)
{
IsPicked = isPicked;
Name = name;
Kind = kind;
}
}
private TableView GetPetTable (out EnumerableTableSource<PickablePet> source)
{
var tv = new TableView ();
tv.ColorScheme = Colors.TopLevel;
tv.Bounds = new Rect (0, 0, 25, 6);
var pets = new List<PickablePet> {
new PickablePet(false,"Tammy","Cat"),
new PickablePet(false,"Tibbles","Cat"),
new PickablePet(false,"Ripper","Dog")};
tv.Table = source = new EnumerableTableSource<PickablePet> (
pets,
new () {
{ "Name", (p) => p.Name},
{ "Kind", (p) => p.Kind},
});
tv.LayoutSubviews ();
return tv;
}
}
}