using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Text; namespace Terminal.Gui; /// /// An with expandable rows. /// /// public class TreeTableSource : IEnumerableTableSource, IDisposable where T : class { private TreeView _tree; private string [] _cols; private Dictionary> _lamdas; private TableView _tableView; /// /// Creates a new instance of presenting the given /// . This source should only be used with . /// /// The table this source will provide data for. /// Column name to use for the first column of the table (where /// the tree branches/leaves will be rendered. /// The tree data to render. This should be a new view and not used /// elsewhere (e.g. via ). /// /// Getter methods for each additional property you want to present in the table. For example: /// /// new () { /// { "Colname1", (t)=>t.SomeField}, /// { "Colname2", (t)=>t.SomeOtherField} ///} /// public TreeTableSource (TableView table, string firstColumnName, TreeView tree, Dictionary> subsequentColumns) { _tableView = table; _tree = tree; _tableView.KeyDown += Table_KeyPress; _tableView.MouseClick += Table_MouseClick; var colList = subsequentColumns.Keys.ToList (); colList.Insert (0, firstColumnName); _cols = colList.ToArray (); _lamdas = subsequentColumns; } /// public object this [int row, int col] => col == 0 ? GetColumnZeroRepresentationFromTree (row) : _lamdas [ColumnNames [col]] (RowToObject (row)); /// public int Rows => _tree.BuildLineMap ().Count; /// public int Columns => _lamdas.Count + 1; /// public string [] ColumnNames => _cols; /// public void Dispose () { _tableView.KeyDown -= Table_KeyPress; _tableView.MouseClick -= Table_MouseClick; _tree.Dispose (); } /// /// Returns the tree model object rendering on the given /// of the table. /// /// Row in table. /// 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, Key e) { if (!IsInTreeColumn (_tableView.SelectedColumn, true)) { return; } var obj = _tree.GetObjectOnRow (_tableView.SelectedRow); if (obj == null) { return; } if (e.KeyCode == KeyCode.CursorLeft) { if (_tree.IsExpanded (obj)) { _tree.Collapse (obj); e.Handled = true; } } if (e.KeyCode == KeyCode.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 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]; } /// public T GetObjectOnRow (int row) { return RowToObject (row); } /// public IEnumerable GetAllObjects () { return _tree.BuildLineMap ().Select (b => b.Model); } }