Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3824 - Modernizes TreeView #3827

Draft
wants to merge 3 commits into
base: v2_develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace Terminal.Gui;
#nullable enable
namespace Terminal.Gui;

/// <summary>Delegates of this type are used to fetch string representations of user's model objects</summary>
/// <param name="toRender">The object that is being rendered</param>
Expand Down
104 changes: 55 additions & 49 deletions Terminal.Gui/Views/TreeView/Branch.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
namespace Terminal.Gui;
#nullable enable
namespace Terminal.Gui;

internal class Branch<T> where T : class
{
private readonly TreeView<T> tree;
private readonly TreeView<T>? _tree;

/// <summary>
/// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is
Expand All @@ -11,9 +12,9 @@ internal class Branch<T> where T : class
/// <param name="tree">The UI control in which the branch resides.</param>
/// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent.</param>
/// <param name="model">The user's object that should be displayed.</param>
public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
public Branch (TreeView<T>? tree, Branch<T>? parentBranchIfAny, T? model)
{
this.tree = tree;
_tree = tree;
Model = model;

if (parentBranchIfAny is { })
Expand All @@ -27,7 +28,7 @@ public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
/// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid
/// enumerating the entire underlying hierarchy.
/// </summary>
public Dictionary<T, Branch<T>> ChildBranches { get; set; }
public Dictionary<T, Branch<T>>? ChildBranches { get; set; }

/// <summary>The depth of the current branch. Depth of 0 indicates root level branches.</summary>
public int Depth { get; }
Expand All @@ -36,10 +37,10 @@ public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
public bool IsExpanded { get; set; }

/// <summary>The users object that is being displayed by this branch of the tree.</summary>
public T Model { get; private set; }
public T? Model { get; private set; }

/// <summary>The parent <see cref="Branch{T}"/> or null if it is a root.</summary>
public Branch<T> Parent { get; }
public Branch<T>? Parent { get; }

/// <summary>
/// Returns true if the current branch can be expanded according to the <see cref="TreeBuilder{T}"/> or cached
Expand All @@ -52,17 +53,17 @@ public bool CanExpand ()
if (ChildBranches is null)
{
//if there is a rapid method for determining whether there are children
if (tree.TreeBuilder.SupportsCanExpand)
if (_tree is { TreeBuilder.SupportsCanExpand: true })
{
return tree.TreeBuilder.CanExpand (Model);
return Model is { } && _tree.TreeBuilder.CanExpand (Model);
}

//there is no way of knowing whether we can expand without fetching the children
FetchChildren ();
}

//we fetched or already know the children, so return whether we have any
return ChildBranches.Any ();
return ChildBranches is { } && ChildBranches.Any ();
}

/// <summary>Marks the branch as collapsed (<see cref="IsExpanded"/> false).</summary>
Expand All @@ -80,21 +81,21 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
int indexOfModelText;

// true if the current line of the tree is the selected one and control has focus
bool isSelected = tree.IsSelected (Model);
bool isSelected = _tree!.IsSelected (Model);

Attribute textColor =
isSelected ? tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal;
Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor;
isSelected ? _tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal;
Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor;

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

tree.Move (0, y);
_tree.Move (0, y);

// if we have scrolled to the right then bits of the prefix will have disappeared off the screen
int toSkip = tree.ScrollOffsetHorizontal;
int toSkip = _tree.ScrollOffsetHorizontal;
Attribute attr = symbolColor;

// Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol)
Expand All @@ -112,28 +113,28 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
}

// pick color for expanded symbol
if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors)
if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors)
{
Attribute color = symbolColor;

if (tree.Style.ColorExpandSymbol)
if (_tree.Style.ColorExpandSymbol)
{
if (isSelected)
{
color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal :
tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
color = _tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal :
_tree.HasFocus ? _tree.GetHotFocusColor() : _tree.GetHotNormalColor();
}
else
{
color = tree.ColorScheme.HotNormal;
color = _tree.GetHotNormalColor();
}
}
else
{
color = symbolColor;
}

if (tree.Style.InvertExpandSymbolColors)
if (_tree.Style.InvertExpandSymbolColors)
{
color = new Attribute (color.Background, color.Foreground);
}
Expand Down Expand Up @@ -194,9 +195,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
Attribute modelColor = textColor;

// if custom color delegate invoke it
if (tree.ColorGetter is { })
if (_tree.ColorGetter is { })
{
ColorScheme modelScheme = tree.ColorGetter (Model);
ColorScheme? modelScheme = _tree.ColorGetter (Model!);

// if custom color scheme is defined for this Model
if (modelScheme is { })
Expand Down Expand Up @@ -230,19 +231,19 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
Model = Model,
Y = y,
Cells = cells,
Tree = tree,
Tree = _tree,
IndexOfExpandCollapseSymbol =
indexOfExpandCollapseSymbol,
IndexOfModelText = indexOfModelText
};
tree.OnDrawLine (e);
_tree.OnDrawLine (e);

if (!e.Handled && driver != null)
if (!e.Handled)
{
foreach (Cell cell in cells)
{
driver.SetAttribute ((Attribute)cell.Attribute!);
driver.AddRune (cell.Rune);
driver?.SetAttribute ((Attribute)cell.Attribute!);
driver?.AddRune (cell.Rune);
}
}

Expand All @@ -257,7 +258,7 @@ public void Expand ()
FetchChildren ();
}

if (ChildBranches.Any ())
if (ChildBranches is { } && ChildBranches.Any ())
{
IsExpanded = true;
}
Expand All @@ -266,23 +267,23 @@ public void Expand ()
/// <summary>Fetch the children of this branch. This method populates <see cref="ChildBranches"/>.</summary>
public virtual void FetchChildren ()
{
if (tree.TreeBuilder is null)
if (_tree?.TreeBuilder is null)
{
return;
}

IEnumerable<T> children;

if (Depth >= tree.MaxDepth)
if (Depth >= _tree.MaxDepth)
{
children = Enumerable.Empty<T> ();
}
else
{
children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty<T> ();
children = _tree.TreeBuilder.GetChildren (Model!) ?? Enumerable.Empty<T> ();
}

ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (_tree, this, val));
}

/// <summary>
Expand All @@ -293,16 +294,16 @@ public virtual void FetchChildren ()
/// <returns></returns>
public Rune GetExpandableSymbol (IConsoleDriver driver)
{
Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
Rune leafSymbol = _tree is { Style.ShowBranchLines: true } ? Glyphs.HLine : (Rune)' ';

if (IsExpanded)
{
return tree.Style.CollapseableSymbol ?? leafSymbol;
return _tree!.Style.CollapsableSymbol ?? leafSymbol;
}

if (CanExpand ())
{
return tree.Style.ExpandableSymbol ?? leafSymbol;
return _tree!.Style.ExpandableSymbol ?? leafSymbol;
}

return leafSymbol;
Expand All @@ -315,8 +316,13 @@ public Rune GetExpandableSymbol (IConsoleDriver driver)
/// <returns></returns>
public virtual int GetWidth (IConsoleDriver driver)
{
return
GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length;
if (_tree is { })
{
return
GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (_tree.AspectGetter?.Invoke (Model!) ?? "").Length;
}

return 0;
}

/// <summary>Refreshes cached knowledge in this branch e.g. what children an object has.</summary>
Expand All @@ -340,17 +346,17 @@ public void Refresh (bool startAtTop)
// we already knew about some children so preserve the state of the old children

// first gather the new Children
IEnumerable<T> newChildren = tree.TreeBuilder?.GetChildren (Model) ?? Enumerable.Empty<T> ();
IEnumerable<T> newChildren = _tree?.TreeBuilder?.GetChildren (Model!) ?? Enumerable.Empty<T> ();

// Children who no longer appear need to go
foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ())
{
ChildBranches.Remove (toRemove);

//also if the user has this node selected (its disappearing) so lets change selection to us (the parent object) to be helpful
if (Equals (tree.SelectedObject, toRemove))
if (Equals (_tree?.SelectedObject, toRemove))
{
tree.SelectedObject = Model;
_tree.SelectedObject = Model;
}
}

Expand All @@ -360,7 +366,7 @@ public void Refresh (bool startAtTop)
// If we don't know about the child, yet we need a new branch
if (!ChildBranches.ContainsKey (newChild))
{
ChildBranches.Add (newChild, new Branch<T> (tree, this, newChild));
ChildBranches.Add (newChild, new Branch<T> (_tree, this, newChild));
}
else
{
Expand Down Expand Up @@ -411,7 +417,7 @@ internal void ExpandAll ()
internal IEnumerable<Rune> GetLinePrefix (IConsoleDriver driver)
{
// If not showing line branches or this is a root object.
if (!tree.Style.ShowBranchLines)
if (_tree is { Style.ShowBranchLines: false })
{
for (var i = 0; i < Depth; i++)
{
Expand Down Expand Up @@ -462,13 +468,13 @@ internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x)
}

// if we could theoretically expand
if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?))
if (!IsExpanded && _tree?.Style.ExpandableSymbol != default (Rune?))
{
return x == GetLinePrefix (driver).Count ();
}

// if we could theoretically collapse
if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?))
if (IsExpanded && _tree!.Style.CollapsableSymbol != default (Rune?))
{
return x == GetLinePrefix (driver).Count ();
}
Expand Down Expand Up @@ -504,7 +510,7 @@ internal void Rebuild ()
/// <returns></returns>
private IEnumerable<Branch<T>> GetParentBranches ()
{
Branch<T> cur = Parent;
Branch<T>? cur = Parent;

while (cur is { })
{
Expand All @@ -523,10 +529,10 @@ private bool IsLast ()
{
if (Parent is null)
{
return this == tree.roots.Values.LastOrDefault ();
return this == _tree?.Roots?.Values.LastOrDefault ();
}

return Parent.ChildBranches.Values.LastOrDefault () == this;
return Parent.ChildBranches?.Values.LastOrDefault () == this;
}

private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; }
Expand Down
14 changes: 7 additions & 7 deletions Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
/// <summary>Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions</summary>
public class DelegateTreeBuilder<T> : TreeBuilder<T>
{
private readonly Func<T, bool> canExpand;
private readonly Func<T, IEnumerable<T>> childGetter;
private readonly Func<T, bool> _canExpand;
private readonly Func<T, IEnumerable<T>> _childGetter;

/// <summary>
/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user defined method
/// <paramref name="childGetter"/> to determine children
/// </summary>
/// <param name="childGetter"></param>
/// <returns></returns>
public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter) : base (false) { this.childGetter = childGetter; }
public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter) : base (false) { this._childGetter = childGetter; }

/// <summary>
/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user defined method
Expand All @@ -23,17 +23,17 @@ public class DelegateTreeBuilder<T> : TreeBuilder<T>
/// <returns></returns>
public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter, Func<T, bool> canExpand) : base (true)
{
this.childGetter = childGetter;
this.canExpand = canExpand;
this._childGetter = childGetter;
this._canExpand = canExpand;
}

/// <summary>Returns whether a node can be expanded based on the delegate passed during construction</summary>
/// <param name="toExpand"></param>
/// <returns></returns>
public override bool CanExpand (T toExpand) { return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); }
public override bool CanExpand (T toExpand) { return _canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); }

/// <summary>Returns children using the delegate method passed during construction</summary>
/// <param name="forObject"></param>
/// <returns></returns>
public override IEnumerable<T> GetChildren (T forObject) { return childGetter.Invoke (forObject); }
public override IEnumerable<T> GetChildren (T forObject) { return _childGetter.Invoke (forObject); }
}
9 changes: 5 additions & 4 deletions Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls
#nullable enable
// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls
// by [email protected]). Phillip has explicitly granted permission for his design
// and code to be used in this library under the MIT license.

Expand All @@ -25,17 +26,17 @@ public class DrawTreeViewLineEventArgs<T> where T : class
public int IndexOfModelText { get; init; }

/// <summary>The object at this line in the tree</summary>
public T Model { get; init; }
public T? Model { get; init; }

/// <summary>
/// The rune and color of each symbol that will be rendered. Note that only <see cref="ColorScheme.Normal"/> is
/// respected. You can modify these to change what is rendered.
/// </summary>
/// <remarks>Changing the length of this collection may result in corrupt rendering</remarks>
public List<Cell> Cells { get; init; }
public List<Cell>? Cells { get; init; }

/// <summary>The <see cref="TreeView{T}"/> that is performing the rendering.</summary>
public TreeView<T> Tree { get; init; }
public TreeView<T>? Tree { get; init; }

/// <summary>The line within tree view bounds that is being rendered</summary>
public int Y { get; init; }
Expand Down
Loading
Loading