namespace Terminal.Gui; /// An with expandable rows. /// public class TreeTableSource : IEnumerableTableSource, IDisposable where T : class { private readonly Dictionary> _lambdas; private readonly TableView _tableView; private readonly TreeView _tree; /// /// 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; List colList = subsequentColumns.Keys.ToList (); colList.Insert (0, firstColumnName); ColumnNames = colList.ToArray (); _lambdas = subsequentColumns; } /// public void Dispose () { _tableView.KeyDown -= Table_KeyPress; _tableView.MouseClick -= Table_MouseClick; _tree.Dispose (); } /// public object this [int row, int col] => col == 0 ? GetColumnZeroRepresentationFromTree (row) : _lambdas [ColumnNames [col]] (RowToObject (row)); /// public int Rows => _tree.BuildLineMap ().Count; /// public int Columns => _lambdas.Count + 1; /// public string [] ColumnNames { get; } /// public T GetObjectOnRow (int row) { return RowToObject (row); } /// public IEnumerable GetAllObjects () { return _tree.BuildLineMap ().Select (b => b.Model); } /// 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) { Branch 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 (Rune p in prefix) { sb.Append (p); } sb.Append (expansion); sb.Append (lineBody); return sb.ToString (); } private bool IsInTreeColumn (int column, bool isKeyboard) { string [] 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] == ColumnNames [0]; } private Branch RowToBranch (int row) { return _tree.BuildLineMap ().ElementAt (row); } private void Table_KeyPress (object sender, Key e) { if (!IsInTreeColumn (_tableView.SelectedColumn, true)) { return; } T obj = _tree.GetObjectOnRow (_tableView.SelectedRow); if (obj is 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) { Point? hit = _tableView.ScreenToCell (e.MouseEvent.Position.X, e.MouseEvent.Position.Y, out int? headerIfAny, out int? offsetX); if (hit is null || headerIfAny is { } || !IsInTreeColumn (hit.Value.X, false) || offsetX is null) { return; } Branch branch = RowToBranch (hit.Value.Y); if (branch.IsHitOnExpandableSymbol (Application.Driver, offsetX.Value)) { T 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 (); } } }