mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 17:57:57 +01:00
* 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:
45
Terminal.Gui/Views/TableView/CellToggledEventArgs.cs
Normal file
45
Terminal.Gui/Views/TableView/CellToggledEventArgs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
198
Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs
Normal file
198
Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs
Normal 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 ();
|
||||
}
|
||||
}
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user