Skip to content

Commit

Permalink
Fixes #2683 - Adds an ITableSource which wraps a TreeView<T> (#2685)
Browse files Browse the repository at this point in the history
* WIP: Add TreeTableSource

* Improve expand/collapse

* Render branch/tree properly

* Simplify TreeTableSource to only allow one TreeView

* Add TestTreeTableSource_BasicExpanding test

* Add test combining checkbox and tree together

* Move tree example into main TableEditor scenario
(deleting TreeTableExample.cs)

* Mouse support for expanding/collapsing branches

* Make TreeTableSource work with CheckBoxTableSourceWrapperByObject<T>

* Add tests for mouse expand/collapse

* Improve quality of TableEditor scenario

* Fix mouse expanding not refreshing screen

* Fixed null reference when clicking in header lines

* Add null checks to scenario now it can show trees as well as data tables

* Switch to underscore prefix on private members

* Remove accidentally committed file

* Add setup/teardown to explicitly set driver checked/unchecked glyphs

---------

Co-authored-by: Tig <tig@users.noreply.github.com>
  • Loading branch information
tznind and tig committed Jul 5, 2023
1 parent f46283b commit cd6cfd7
Show file tree
Hide file tree
Showing 9 changed files with 725 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Terminal.Gui {
/// by a property on row objects.
/// </summary>
public class CheckBoxTableSourceWrapperByObject<T> : CheckBoxTableSourceWrapperBase {
private readonly EnumerableTableSource<T> toWrap;
private readonly IEnumerableTableSource<T> toWrap;
readonly Func<T, bool> getter;
readonly Action<T, bool> setter;

Expand All @@ -20,7 +20,7 @@ public class CheckBoxTableSourceWrapperByObject<T> : CheckBoxTableSourceWrapperB
/// <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,
IEnumerableTableSource<T> toWrap,
Func<T,bool> getter,
Action<T,bool> setter) : base (tableView, toWrap)
{
Expand All @@ -32,7 +32,7 @@ public CheckBoxTableSourceWrapperByObject (
/// <inheritdoc/>
protected override bool IsChecked (int row)
{
return getter (toWrap.Data.ElementAt (row));
return getter (toWrap.GetObjectOnRow (row));
}

/// <inheritdoc/>
Expand All @@ -44,7 +44,7 @@ protected override void ToggleAllRows ()
/// <inheritdoc/>
protected override void ToggleRow (int row)
{
var d = toWrap.Data.ElementAt (row);
var d = toWrap.GetObjectOnRow (row);
setter (d, !getter(d));
}

Expand All @@ -55,20 +55,20 @@ protected override void ToggleRows (int [] range)
if (range.All (IsChecked)) {
// select none
foreach(var r in range) {
setter (toWrap.Data.ElementAt (r), false);
setter (toWrap.GetObjectOnRow (r), false);
}
} else {
// otherwise tick all
foreach (var r in range) {
setter (toWrap.Data.ElementAt (r), true);
setter (toWrap.GetObjectOnRow (r), true);
}
}
}

/// <inheritdoc/>
protected override void ClearAllToggles ()
{
foreach (var e in toWrap.Data) {
foreach (var e in toWrap.GetAllObjects()) {
setter (e, false);
}
}
Expand Down
15 changes: 13 additions & 2 deletions Terminal.Gui/Views/TableView/EnumerableTableSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
using System.Linq;

namespace Terminal.Gui {

/// <summary>
/// <see cref="ITableSource"/> implementation that wraps arbitrary data.
/// </summary>
/// <typeparam name="T"></typeparam>
public class EnumerableTableSource<T> : ITableSource {
public class EnumerableTableSource<T> : IEnumerableTableSource<T> {
private T [] data;
private string [] cols;
private Dictionary<string, Func<T, object>> lamdas;
Expand Down Expand Up @@ -55,5 +54,17 @@ public EnumerableTableSource (IEnumerable<T> data, Dictionary<string, Func<T, ob
/// Gets the object collection hosted by this wrapper.
/// </summary>
public IReadOnlyCollection<T> Data => this.data.AsReadOnly();

/// <inheritdoc/>
public IEnumerable<T> GetAllObjects ()
{
return Data;
}

/// <inheritdoc/>
public T GetObjectOnRow (int row)
{
return Data.ElementAt(row);
}
}
}
21 changes: 21 additions & 0 deletions Terminal.Gui/Views/TableView/IEnumerableTableSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;

namespace Terminal.Gui {

/// <summary>
/// Interface for all <see cref="ITableSource"/> which present
/// an object per row (of type <typeparamref name="T"/>).
/// </summary>
public interface IEnumerableTableSource<T> : ITableSource
{
/// <summary>
/// Return the object on the given row.
/// </summary>
T GetObjectOnRow(int row);

/// <summary>
/// Return all objects in the table.
/// </summary>
IEnumerable<T> GetAllObjects();
}
}
20 changes: 17 additions & 3 deletions Terminal.Gui/Views/TableView/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1288,19 +1288,31 @@ private bool HasControlOrAlt (MouseEvent me)
/// <returns>Cell clicked or null.</returns>
public Point? ScreenToCell (int clientX, int clientY)
{
return ScreenToCell (clientX, clientY, out _);
return ScreenToCell (clientX, clientY, out _, out _);
}

/// <inheritdoc cref="ScreenToCell(int, int)"/>
/// <param name="clientX">X offset from the top left of the control.</param>
/// <param name="clientY">Y offset from the top left of the control.</param>
/// <param name="headerIfAny">If the click is in a header this is the column clicked.</param>
public Point? ScreenToCell (int clientX, int clientY, out int? headerIfAny)
{
return ScreenToCell (clientX, clientY, out headerIfAny, out _);
}

/// <inheritdoc cref="ScreenToCell(int, int)"/>
/// <param name="clientX">X offset from the top left of the control.</param>
/// <param name="clientY">Y offset from the top left of the control.</param>
/// <param name="headerIfAny">If the click is in a header this is the column clicked.</param>
/// <param name="offsetX">The horizontal offset of the click within the returned cell.</param>
public Point? ScreenToCell (int clientX, int clientY, out int? headerIfAny, out int? offsetX)
{
headerIfAny = null;
offsetX = null;

if (TableIsNullOrInvisible ())
if (TableIsNullOrInvisible ()) {
return null;
}


var viewPort = CalculateViewport (Bounds);

Expand All @@ -1311,6 +1323,7 @@ private bool HasControlOrAlt (MouseEvent me)
// Click is on the header section of rendered UI
if (clientY < headerHeight) {
headerIfAny = col?.Column;
offsetX = col != null ? clientX - col.X : null;
return null;
}

Expand All @@ -1324,6 +1337,7 @@ private bool HasControlOrAlt (MouseEvent me)

if (col != null && rowIdx >= 0) {

offsetX = clientX - col.X;
return new Point (col.Column, rowIdx);
}

Expand Down
203 changes: 203 additions & 0 deletions Terminal.Gui/Views/TableView/TreeTableSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;

namespace Terminal.Gui;

/// <summary>
/// An <see cref="ITableSource"/> with expandable rows.
/// </summary>
/// <typeparam name="T"></typeparam>
public class TreeTableSource<T> : IEnumerableTableSource<T>, IDisposable where T : class {

private TreeView<T> _tree;
private string [] _cols;
private Dictionary<string, Func<T, object>> _lamdas;
private TableView _tableView;

/// <summary>
/// Creates a new instance of <see cref="TreeTableSource{T}"/> presenting the given
/// <paramref name="tree"/>. This source should only be used with <paramref name="table"/>.
/// </summary>
/// <param name="table">The table this source will provide data for.</param>
/// <param name="firstColumnName">Column name to use for the first column of the table (where
/// the tree branches/leaves will be rendered.</param>
/// <param name="tree">The tree data to render. This should be a new view and not used
/// elsewhere (e.g. via <see cref="View.Add(View)"/>).</param>
/// <param name="subsequentColumns">
/// Getter methods for each additional property you want to present in the table. For example:
/// <code>
/// new () {
/// { "Colname1", (t)=>t.SomeField},
/// { "Colname2", (t)=>t.SomeOtherField}
///}
/// </code></param>
public TreeTableSource (TableView table, string firstColumnName, TreeView<T> tree, Dictionary<string, Func<T, object>> subsequentColumns)
{
_tableView = table;
_tree = tree;
_tableView.KeyPress += Table_KeyPress;
_tableView.MouseClick += Table_MouseClick;

var colList = subsequentColumns.Keys.ToList ();
colList.Insert (0, firstColumnName);

_cols = colList.ToArray ();


_lamdas = subsequentColumns;
}


/// <inheritdoc/>
public object this [int row, int col] =>
col == 0 ? GetColumnZeroRepresentationFromTree (row) :
_lamdas [ColumnNames [col]] (RowToObject (row));

/// <inheritdoc/>
public int Rows => _tree.BuildLineMap ().Count;

/// <inheritdoc/>
public int Columns => _lamdas.Count + 1;

/// <inheritdoc/>
public string [] ColumnNames => _cols;

/// <inheritdoc/>
public void Dispose ()
{
_tree.Dispose ();
}

/// <summary>
/// Returns the tree model object rendering on the given <paramref name="row"/>
/// of the table.
/// </summary>
/// <param name="row">Row in table.</param>
/// <returns></returns>
public T RowToObject (int row)
{
return _tree.BuildLineMap ().ElementAt (row).Model;
}


private string GetColumnZeroRepresentationFromTree (int row)
{
var branch = RowToBranch (row);

// Everything on line before the expansion run and branch text
Rune [] prefix = branch.GetLinePrefix (Application.Driver).ToArray ();
Rune expansion = branch.GetExpandableSymbol (Application.Driver);
string lineBody = _tree.AspectGetter (branch.Model) ?? "";

var sb = new StringBuilder ();

foreach (var p in prefix) {
sb.Append (p);
}

sb.Append (expansion);
sb.Append (lineBody);

return sb.ToString ();
}

private void Table_KeyPress (object sender, KeyEventEventArgs e)
{
if (!IsInTreeColumn (_tableView.SelectedColumn, true)) {
return;
}

var obj = _tree.GetObjectOnRow (_tableView.SelectedRow);

if (obj == null) {
return;
}

if (e.KeyEvent.Key == Key.CursorLeft) {
if (_tree.IsExpanded (obj)) {
_tree.Collapse (obj);
e.Handled = true;
}
}
if (e.KeyEvent.Key == Key.CursorRight) {
if (_tree.CanExpand (obj) && !_tree.IsExpanded (obj)) {
_tree.Expand (obj);
e.Handled = true;
}
}

if (e.Handled) {
_tree.InvalidateLineMap ();
_tableView.SetNeedsDisplay ();
}
}

private void Table_MouseClick (object sender, MouseEventEventArgs e)
{
var hit = _tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out var headerIfAny, out var offsetX);

if (hit == null || headerIfAny != null || !IsInTreeColumn (hit.Value.X, false) || offsetX == null) {
return;
}

var branch = RowToBranch (hit.Value.Y);

if (branch.IsHitOnExpandableSymbol (Application.Driver, offsetX.Value)) {

var m = branch.Model;

if (_tree.CanExpand (m) && !_tree.IsExpanded (m)) {
_tree.Expand (m);

e.Handled = true;
} else if (_tree.IsExpanded (m)) {
_tree.Collapse (m);
e.Handled = true;
}
}

if (e.Handled) {
_tree.InvalidateLineMap ();
_tableView.SetNeedsDisplay ();
}
}

private Branch<T> RowToBranch (int row)
{
return _tree.BuildLineMap ().ElementAt (row);
}

private bool IsInTreeColumn (int column, bool isKeyboard)
{
var colNames = _tableView.Table.ColumnNames;

if (column < 0 || column >= colNames.Length) {
return false;
}

// if full row is selected then it is hard to tell which sub cell in the tree
// has focus so we should typically just always respond with expand/collapse
if (_tableView.FullRowSelect && isKeyboard) {
return true;
}

// we cannot just check that SelectedColumn is 0 because source may
// be wrapped e.g. with a CheckBoxTableSourceWrapperBase
return colNames [column] == _cols [0];
}

/// <inheritdoc/>
public T GetObjectOnRow (int row)
{
return RowToObject (row);
}

/// <inheritdoc/>
public IEnumerable<T> GetAllObjects ()
{
return _tree.BuildLineMap ().Select (b => b.Model);
}
}
4 changes: 2 additions & 2 deletions Terminal.Gui/Views/TreeView/Branch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text;

namespace Terminal.Gui {
class Branch<T> where T : class {
internal class Branch<T> where T : class {
/// <summary>
/// True if the branch is expanded to reveal child branches.
/// </summary>
Expand Down Expand Up @@ -202,7 +202,7 @@ public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y,
/// </summary>
/// <param name="driver"></param>
/// <returns></returns>
private IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
internal IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
{
// If not showing line branches or this is a root object.
if (!tree.Style.ShowBranchLines) {
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Views/TreeView/TreeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ public int GetContentWidth (bool visible)
/// <remarks>Index 0 of the returned array is the first item that should be visible in the
/// top of the control, index 1 is the next etc.</remarks>
/// <returns></returns>
private IReadOnlyCollection<Branch<T>> BuildLineMap ()
internal IReadOnlyCollection<Branch<T>> BuildLineMap ()
{
if (cachedLineMap != null) {
return cachedLineMap;
Expand Down
Loading

0 comments on commit cd6cfd7

Please sign in to comment.