Files
Terminal.Gui/Terminal.Gui/Views/TreeView/Branch.cs
2023-12-07 15:09:20 +00:00

499 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Terminal.Gui {
internal class Branch<T> where T : class {
/// <summary>
/// True if the branch is expanded to reveal child branches.
/// </summary>
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; }
/// <summary>
/// The depth of the current branch. Depth of 0 indicates root level branches.
/// </summary>
public int Depth { get; private set; } = 0;
/// <summary>
/// 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; }
/// <summary>
/// The parent <see cref="Branch{T}"/> or null if it is a root.
/// </summary>
public Branch<T> Parent { get; private set; }
private TreeView<T> tree;
/// <summary>
/// Declares a new branch of <paramref name="tree"/> in which the users object
/// <paramref name="model"/> is presented.
/// </summary>
/// <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)
{
this.tree = tree;
this.Model = model;
if (parentBranchIfAny != null) {
Depth = parentBranchIfAny.Depth + 1;
Parent = parentBranchIfAny;
}
}
/// <summary>
/// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>.
/// </summary>
public virtual void FetchChildren ()
{
if (tree.TreeBuilder == null) {
return;
}
IEnumerable<T> children;
if (Depth >= tree.MaxDepth) {
children = Enumerable.Empty<T> ();
} else {
children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty<T> ();
}
this.ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
}
/// <summary>
/// Returns the width of the line including prefix and the results
/// of <see cref="TreeView{T}.AspectGetter"/> (the line body).
/// </summary>
/// <returns></returns>
public virtual int GetWidth (ConsoleDriver driver)
{
return
GetLinePrefix (driver).Sum (r => r.GetColumns ()) +
GetExpandableSymbol (driver).GetColumns () +
(tree.AspectGetter (Model) ?? "").Length;
}
/// <summary>
/// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>.
/// </summary>
/// <param name="driver"></param>
/// <param name="colorScheme"></param>
/// <param name="y"></param>
/// <param name="availableWidth"></param>
public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
{
var cells = new List<RuneCell> ();
int? indexOfExpandCollapseSymbol = null;
int indexOfModelText;
// true if the current line of the tree is the selected one and control has focus
bool isSelected = tree.IsSelected (Model);
Attribute 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) ?? "";
tree.Move (0, y);
// if we have scrolled to the right then bits of the prefix will have dispeared off the screen
int toSkip = tree.ScrollOffsetHorizontal;
var attr = symbolColor;
// Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol)
foreach (Rune r in prefix) {
if (toSkip > 0) {
toSkip--;
} else {
cells.Add (NewRuneCell (attr, r));
availableWidth -= r.GetColumns ();
}
}
// pick color for expanded symbol
if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) {
Attribute color = symbolColor;
if (tree.Style.ColorExpandSymbol) {
if (isSelected) {
color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : (tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal);
} else {
color = tree.ColorScheme.HotNormal;
}
} else {
color = symbolColor;
}
if (tree.Style.InvertExpandSymbolColors) {
color = new Attribute (color.Background, color.Foreground);
}
attr = color;
}
if (toSkip > 0) {
toSkip--;
} else {
indexOfExpandCollapseSymbol = cells.Count;
cells.Add (NewRuneCell (attr, expansion));
availableWidth -= expansion.GetColumns ();
}
// horizontal scrolling has already skipped the prefix but now must also skip some of the line body
if (toSkip > 0) {
// For the event record a negative location for where model text starts since it
// is pushed off to the left because of scrolling
indexOfModelText = -toSkip;
if (toSkip > lineBody.Length) {
lineBody = "";
} else {
lineBody = lineBody.Substring (toSkip);
}
} else {
indexOfModelText = cells.Count;
}
// If body of line is too long
if (lineBody.EnumerateRunes ().Sum (l => l.GetColumns ()) > availableWidth) {
// remaining space is zero and truncate the line
lineBody = new string (lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0).ToArray ());
availableWidth = 0;
} else {
// line is short so remaining width will be whatever comes after the line body
availableWidth -= lineBody.Length;
}
// default behaviour is for model to use the color scheme
// of the tree view
var modelColor = textColor;
// if custom color delegate invoke it
if (tree.ColorGetter != null) {
var modelScheme = tree.ColorGetter (Model);
// if custom color scheme is defined for this Model
if (modelScheme != null) {
// use it
modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal;
} else {
modelColor = new Attribute (Attribute.Default.Foreground, Attribute.Default.Background);
}
}
attr = modelColor;
cells.AddRange (lineBody.Select (r => NewRuneCell (attr, new Rune (r))));
if (availableWidth > 0) {
attr = symbolColor;
cells.AddRange (
Enumerable.Repeat (
NewRuneCell (attr, new Rune (' ')),
availableWidth
));
}
var e = new DrawTreeViewLineEventArgs<T> {
Model = Model,
Y = y,
RuneCells = cells,
Tree = tree,
IndexOfExpandCollapseSymbol = indexOfExpandCollapseSymbol,
IndexOfModelText = indexOfModelText,
};
tree.OnDrawLine (e);
if (!e.Handled) {
foreach (var cell in cells) {
driver.SetAttribute (cell.ColorScheme.Normal);
driver.AddRune (cell.Rune);
}
}
driver.SetAttribute (colorScheme.Normal);
}
private static RuneCell NewRuneCell (Attribute attr, Rune r)
{
return new RuneCell {
Rune = r,
ColorScheme = new ColorScheme (attr)
};
}
/// <summary>
/// Gets all characters to render prior to the current branches line. This includes indentation
/// whitespace and any tree branches (if enabled).
/// </summary>
/// <param name="driver"></param>
/// <returns></returns>
internal IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
{
// If not showing line branches or this is a root object.
if (!tree.Style.ShowBranchLines) {
for (int i = 0; i < Depth; i++) {
yield return new Rune (' ');
}
yield break;
}
// yield indentations with runes appropriate to the state of the parents
foreach (var cur in GetParentBranches ().Reverse ()) {
if (cur.IsLast ()) {
yield return new Rune (' ');
} else {
yield return CM.Glyphs.VLine;
}
yield return new Rune (' ');
}
if (IsLast ()) {
yield return CM.Glyphs.LLCorner;
} else {
yield return CM.Glyphs.LeftTee;
}
}
/// <summary>
/// Returns all parents starting with the immediate parent and ending at the root.
/// </summary>
/// <returns></returns>
private IEnumerable<Branch<T>> GetParentBranches ()
{
var cur = Parent;
while (cur != null) {
yield return cur;
cur = cur.Parent;
}
}
/// <summary>
/// Returns an appropriate symbol for displaying next to the string representation of
/// the <see cref="Model"/> object to indicate whether it <see cref="IsExpanded"/> or
/// not (or it is a leaf).
/// </summary>
/// <param name="driver"></param>
/// <returns></returns>
public Rune GetExpandableSymbol (ConsoleDriver driver)
{
var leafSymbol = tree.Style.ShowBranchLines ? CM.Glyphs.HLine : (Rune)' ';
if (IsExpanded) {
return tree.Style.CollapseableSymbol ?? (Rune)leafSymbol;
}
if (CanExpand ()) {
return tree.Style.ExpandableSymbol ?? (Rune)leafSymbol;
}
return (Rune)leafSymbol;
}
/// <summary>
/// Returns true if the current branch can be expanded according to
/// the <see cref="TreeBuilder{T}"/> or cached children already fetched.
/// </summary>
/// <returns></returns>
public bool CanExpand ()
{
// if we do not know the children yet
if (ChildBranches == null) {
//if there is a rapid method for determining whether there are children
if (tree.TreeBuilder.SupportsCanExpand) {
return 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 ();
}
/// <summary>
/// Expands the current branch if possible.
/// </summary>
public void Expand ()
{
if (ChildBranches == null) {
FetchChildren ();
}
if (ChildBranches.Any ()) {
IsExpanded = true;
}
}
/// <summary>
/// Marks the branch as collapsed (<see cref="IsExpanded"/> false).
/// </summary>
public void Collapse ()
{
IsExpanded = false;
}
/// <summary>
/// Refreshes cached knowledge in this branch e.g. what children an object has.
/// </summary>
/// <param name="startAtTop">True to also refresh all <see cref="Parent"/>
/// branches (starting with the root).</param>
public void Refresh (bool startAtTop)
{
// if we must go up and refresh from the top down
if (startAtTop) {
Parent?.Refresh (true);
}
// we don't want to loose the state of our children so lets be selective about how we refresh
//if we don't know about any children yet just use the normal method
if (ChildBranches == null) {
FetchChildren ();
} else {
// we already knew about some children so preserve the state of the old children
// first gather the new Children
var newChildren = tree.TreeBuilder?.GetChildren (this.Model) ?? Enumerable.Empty<T> ();
// Children who no longer appear need to go
foreach (var toRemove in ChildBranches.Keys.Except (newChildren).ToArray ()) {
ChildBranches.Remove (toRemove);
//also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful
if (Equals (tree.SelectedObject, toRemove)) {
tree.SelectedObject = Model;
}
}
// New children need to be added
foreach (var newChild in newChildren) {
// 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));
} else {
//we already have this object but update the reference anyway incase Equality match but the references are new
ChildBranches [newChild].Model = newChild;
}
}
}
}
/// <summary>
/// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children.
/// </summary>
internal void Rebuild ()
{
Refresh (false);
// if we know about our children
if (ChildBranches != null) {
if (IsExpanded) {
//if we are expanded we need to updatethe visible children
foreach (var child in ChildBranches) {
child.Value.Rebuild ();
}
} else {
// we are not expanded so should forget about children because they may not exist anymore
ChildBranches = null;
}
}
}
/// <summary>
/// Returns true if this branch has parents and it is the last node of it's parents
/// branches (or last root of the tree).
/// </summary>
/// <returns></returns>
private bool IsLast ()
{
if (Parent == null) {
return this == tree.roots.Values.LastOrDefault ();
}
return Parent.ChildBranches.Values.LastOrDefault () == this;
}
/// <summary>
/// Returns true if the given x offset on the branch line is the +/- symbol. Returns
/// false if not showing expansion symbols or leaf node etc.
/// </summary>
/// <param name="driver"></param>
/// <param name="x"></param>
/// <returns></returns>
internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x)
{
// if leaf node then we cannot expand
if (!CanExpand ()) {
return false;
}
// if we could theoretically expand
if (!IsExpanded && tree.Style.ExpandableSymbol != default) {
return x == GetLinePrefix (driver).Count ();
}
// if we could theoretically collapse
if (IsExpanded && tree.Style.CollapseableSymbol != default) {
return x == GetLinePrefix (driver).Count ();
}
return false;
}
/// <summary>
/// Expands the current branch and all children branches.
/// </summary>
internal void ExpandAll ()
{
Expand ();
if (ChildBranches != null) {
foreach (var child in ChildBranches) {
child.Value.ExpandAll ();
}
}
}
/// <summary>
/// Collapses the current branch and all children branches (even though those branches are
/// no longer visible they retain collapse/expansion state).
/// </summary>
internal void CollapseAll ()
{
Collapse ();
if (ChildBranches != null) {
foreach (var child in ChildBranches) {
child.Value.CollapseAll ();
}
}
}
}
}