mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 17:57:57 +01:00
Merge branch 'main' of tig:migueldeicaza/gui.cs
This commit is contained in:
29
Terminal.Gui/Core/ReadOnlyCollectionExtensions.cs
Normal file
29
Terminal.Gui/Core/ReadOnlyCollectionExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Terminal.Gui {
|
||||
|
||||
static class ReadOnlyCollectionExtensions {
|
||||
|
||||
public static int IndexOf<T> (this IReadOnlyCollection<T> self, Func<T, bool> predicate)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (T element in self) {
|
||||
if (predicate (element))
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
public static int IndexOf<T> (this IReadOnlyCollection<T> self, T toFind)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (T element in self) {
|
||||
if (Equals (element, toFind))
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Terminal.Gui/Core/Trees/AspectGetterDelegate.cs
Normal file
11
Terminal.Gui/Core/Trees/AspectGetterDelegate.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
public delegate string AspectGetterDelegate<T> (T toRender) where T : class;
|
||||
|
||||
}
|
||||
428
Terminal.Gui/Core/Trees/Branch.cs
Normal file
428
Terminal.Gui/Core/Trees/Branch.cs
Normal file
@@ -0,0 +1,428 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
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;
|
||||
}
|
||||
|
||||
var 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 (Rune.ColumnWidth) +
|
||||
Rune.ColumnWidth (GetExpandableSymbol (driver)) +
|
||||
(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)
|
||||
{
|
||||
// true if the current line of the tree is the selected one and control has focus
|
||||
bool isSelected = tree.IsSelected (Model) && tree.HasFocus;
|
||||
Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal;
|
||||
|
||||
driver.SetAttribute (lineColor);
|
||||
|
||||
// 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;
|
||||
|
||||
// Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol)
|
||||
foreach (Rune r in prefix) {
|
||||
|
||||
if (toSkip > 0) {
|
||||
toSkip--;
|
||||
} else {
|
||||
driver.AddRune (r);
|
||||
availableWidth -= Rune.ColumnWidth (r);
|
||||
}
|
||||
}
|
||||
|
||||
// pick color for expanded symbol
|
||||
if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) {
|
||||
Attribute color;
|
||||
|
||||
if (tree.Style.ColorExpandSymbol) {
|
||||
color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
|
||||
} else {
|
||||
color = lineColor;
|
||||
}
|
||||
|
||||
if (tree.Style.InvertExpandSymbolColors) {
|
||||
color = new Attribute (color.Background, color.Foreground);
|
||||
}
|
||||
|
||||
driver.SetAttribute (color);
|
||||
}
|
||||
|
||||
if (toSkip > 0) {
|
||||
toSkip--;
|
||||
} else {
|
||||
driver.AddRune (expansion);
|
||||
availableWidth -= Rune.ColumnWidth (expansion);
|
||||
}
|
||||
|
||||
// horizontal scrolling has already skipped the prefix but now must also skip some of the line body
|
||||
if (toSkip > 0) {
|
||||
if (toSkip > lineBody.Length) {
|
||||
lineBody = "";
|
||||
} else {
|
||||
lineBody = lineBody.Substring (toSkip);
|
||||
}
|
||||
}
|
||||
|
||||
// If body of line is too long
|
||||
if (lineBody.Sum (l => Rune.ColumnWidth (l)) > availableWidth) {
|
||||
// remaining space is zero and truncate the line
|
||||
lineBody = new string (lineBody.TakeWhile (c => (availableWidth -= Rune.ColumnWidth (c)) >= 0).ToArray ());
|
||||
availableWidth = 0;
|
||||
} else {
|
||||
|
||||
// line is short so remaining width will be whatever comes after the line body
|
||||
availableWidth -= lineBody.Length;
|
||||
}
|
||||
|
||||
//reset the line color if it was changed for rendering expansion symbol
|
||||
driver.SetAttribute (lineColor);
|
||||
driver.AddStr (lineBody);
|
||||
|
||||
if (availableWidth > 0) {
|
||||
driver.AddStr (new string (' ', availableWidth));
|
||||
}
|
||||
|
||||
driver.SetAttribute (colorScheme.Normal);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private 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 driver.VLine;
|
||||
}
|
||||
|
||||
yield return new Rune (' ');
|
||||
}
|
||||
|
||||
if (IsLast ()) {
|
||||
yield return driver.LLCorner;
|
||||
} else {
|
||||
yield return driver.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 ? driver.HLine : ' ';
|
||||
|
||||
if (IsExpanded) {
|
||||
return tree.Style.CollapseableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
if (CanExpand ()) {
|
||||
return tree.Style.ExpandableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
return 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 != null) {
|
||||
return x == GetLinePrefix (driver).Count ();
|
||||
}
|
||||
|
||||
// if we could theoretically collapse
|
||||
if (IsExpanded && tree.Style.CollapseableSymbol != null) {
|
||||
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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs
Normal file
57
Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions
|
||||
/// </summary>
|
||||
public class DelegateTreeBuilder<T> : TreeBuilder<T> {
|
||||
private Func<T, IEnumerable<T>> childGetter;
|
||||
private Func<T, bool> canExpand;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user
|
||||
/// defined method <paramref name="childGetter"/> to determine children
|
||||
/// and <paramref name="canExpand"/> to determine expandability
|
||||
/// </summary>
|
||||
/// <param name="childGetter"></param>
|
||||
/// <param name="canExpand"></param>
|
||||
/// <returns></returns>
|
||||
public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter, Func<T, bool> canExpand) : base (true)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Terminal.Gui/Core/Trees/ITreeBuilder.cs
Normal file
36
Terminal.Gui/Core/Trees/ITreeBuilder.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// Interface for supplying data to a <see cref="TreeView{T}"/> on demand as root level nodes
|
||||
/// are expanded by the user
|
||||
/// </summary>
|
||||
public interface ITreeBuilder<T> {
|
||||
/// <summary>
|
||||
/// Returns true if <see cref="CanExpand"/> is implemented by this class
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
bool SupportsCanExpand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true/false for whether a model has children. This method should be implemented
|
||||
/// when <see cref="GetChildren"/> is an expensive operation otherwise
|
||||
/// <see cref="SupportsCanExpand"/> should return false (in which case this method will not
|
||||
/// be called)
|
||||
/// </summary>
|
||||
/// <remarks>Only implement this method if you have a very fast way of determining whether
|
||||
/// an object can have children e.g. checking a Type (directories can always be expanded)
|
||||
/// </remarks>
|
||||
/// <param name="toExpand"></param>
|
||||
/// <returns></returns>
|
||||
bool CanExpand (T toExpand);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all children of a given <paramref name="forObject"/> which should be added to the
|
||||
/// tree as new branches underneath it
|
||||
/// </summary>
|
||||
/// <param name="forObject"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<T> GetChildren (T forObject);
|
||||
}
|
||||
}
|
||||
32
Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs
Normal file
32
Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// Event args for the <see cref="TreeView{T}.ObjectActivated"/> event
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class ObjectActivatedEventArgs<T> where T : class {
|
||||
|
||||
/// <summary>
|
||||
/// The tree in which the activation occurred
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public TreeView<T> Tree { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The object that was selected at the time of activation
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public T ActivatedObject { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance documenting activation of the <paramref name="activated"/> object
|
||||
/// </summary>
|
||||
/// <param name="tree">Tree in which the activation is happening</param>
|
||||
/// <param name="activated">What object is being activated</param>
|
||||
public ObjectActivatedEventArgs (TreeView<T> tree, T activated)
|
||||
{
|
||||
Tree = tree;
|
||||
ActivatedObject = activated;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs
Normal file
37
Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// Event arguments describing a change in selected object in a tree view
|
||||
/// </summary>
|
||||
public class SelectionChangedEventArgs<T> : EventArgs where T : class {
|
||||
/// <summary>
|
||||
/// The view in which the change occurred
|
||||
/// </summary>
|
||||
public TreeView<T> Tree { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The previously selected value (can be null)
|
||||
/// </summary>
|
||||
public T OldValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The newly selected value in the <see cref="Tree"/> (can be null)
|
||||
/// </summary>
|
||||
public T NewValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of event args describing a change of selection
|
||||
/// in <paramref name="tree"/>
|
||||
/// </summary>
|
||||
/// <param name="tree"></param>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
public SelectionChangedEventArgs (TreeView<T> tree, T oldValue, T newValue)
|
||||
{
|
||||
Tree = tree;
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Terminal.Gui/Core/Trees/TreeBuilder.cs
Normal file
41
Terminal.Gui/Core/Trees/TreeBuilder.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
|
||||
/// <summary>
|
||||
/// Abstract implementation of <see cref="ITreeBuilder{T}"/>.
|
||||
/// </summary>
|
||||
public abstract class TreeBuilder<T> : ITreeBuilder<T> {
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsCanExpand { get; protected set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a rapid answer as to whether <see cref="GetChildren(T)"/>
|
||||
/// returns results. If you are implementing this method ensure you passed true in base
|
||||
/// constructor or set <see cref="SupportsCanExpand"/>
|
||||
/// </summary>
|
||||
/// <param name="toExpand"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool CanExpand (T toExpand)
|
||||
{
|
||||
|
||||
return GetChildren (toExpand).Any ();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract IEnumerable<T> GetChildren (T forObject);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs base and initializes <see cref="SupportsCanExpand"/>
|
||||
/// </summary>
|
||||
/// <param name="supportsCanExpand">Pass true if you intend to
|
||||
/// implement <see cref="CanExpand(T)"/> otherwise false</param>
|
||||
public TreeBuilder (bool supportsCanExpand)
|
||||
{
|
||||
SupportsCanExpand = supportsCanExpand;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Terminal.Gui {
|
||||
namespace Terminal.Gui.Trees {
|
||||
|
||||
/// <summary>
|
||||
/// Interface to implement when you want the regular (non generic) <see cref="TreeView"/>
|
||||
28
Terminal.Gui/Core/Trees/TreeNodeBuilder.cs
Normal file
28
Terminal.Gui/Core/Trees/TreeNodeBuilder.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// <see cref="ITreeBuilder{T}"/> implementation for <see cref="ITreeNode"/> objects
|
||||
/// </summary>
|
||||
public class TreeNodeBuilder : TreeBuilder<ITreeNode> {
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of builder for any model objects of
|
||||
/// Type <see cref="ITreeNode"/>
|
||||
/// </summary>
|
||||
public TreeNodeBuilder () : base (false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
public override IEnumerable<ITreeNode> GetChildren (ITreeNode model)
|
||||
{
|
||||
return model.Children;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace Terminal.Gui {
|
||||
namespace Terminal.Gui.Trees {
|
||||
/// <summary>
|
||||
/// Defines rendering options that affect how the tree is displayed
|
||||
/// </summary>
|
||||
@@ -1,155 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Terminal.Gui {
|
||||
|
||||
/// <summary>
|
||||
/// Interface for supplying data to a <see cref="TreeView{T}"/> on demand as root level nodes
|
||||
/// are expanded by the user
|
||||
/// </summary>
|
||||
public interface ITreeBuilder<T> {
|
||||
/// <summary>
|
||||
/// Returns true if <see cref="CanExpand"/> is implemented by this class
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
bool SupportsCanExpand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns true/false for whether a model has children. This method should be implemented
|
||||
/// when <see cref="GetChildren"/> is an expensive operation otherwise
|
||||
/// <see cref="SupportsCanExpand"/> should return false (in which case this method will not
|
||||
/// be called)
|
||||
/// </summary>
|
||||
/// <remarks>Only implement this method if you have a very fast way of determining whether
|
||||
/// an object can have children e.g. checking a Type (directories can always be expanded)
|
||||
/// </remarks>
|
||||
/// <param name="toExpand"></param>
|
||||
/// <returns></returns>
|
||||
bool CanExpand (T toExpand);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all children of a given <paramref name="forObject"/> which should be added to the
|
||||
/// tree as new branches underneath it
|
||||
/// </summary>
|
||||
/// <param name="forObject"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<T> GetChildren (T forObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstract implementation of <see cref="ITreeBuilder{T}"/>.
|
||||
/// </summary>
|
||||
public abstract class TreeBuilder<T> : ITreeBuilder<T> {
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsCanExpand { get; protected set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a rapid answer as to whether <see cref="GetChildren(T)"/>
|
||||
/// returns results. If you are implementing this method ensure you passed true in base
|
||||
/// constructor or set <see cref="SupportsCanExpand"/>
|
||||
/// </summary>
|
||||
/// <param name="toExpand"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool CanExpand (T toExpand)
|
||||
{
|
||||
|
||||
return GetChildren (toExpand).Any ();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract IEnumerable<T> GetChildren (T forObject);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs base and initializes <see cref="SupportsCanExpand"/>
|
||||
/// </summary>
|
||||
/// <param name="supportsCanExpand">Pass true if you intend to
|
||||
/// implement <see cref="CanExpand(T)"/> otherwise false</param>
|
||||
public TreeBuilder (bool supportsCanExpand)
|
||||
{
|
||||
SupportsCanExpand = supportsCanExpand;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ITreeBuilder{T}"/> implementation for <see cref="ITreeNode"/> objects
|
||||
/// </summary>
|
||||
public class TreeNodeBuilder : TreeBuilder<ITreeNode> {
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of builder for any model objects of
|
||||
/// Type <see cref="ITreeNode"/>
|
||||
/// </summary>
|
||||
public TreeNodeBuilder () : base (false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
public override IEnumerable<ITreeNode> GetChildren (ITreeNode model)
|
||||
{
|
||||
return model.Children;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions
|
||||
/// </summary>
|
||||
public class DelegateTreeBuilder<T> : TreeBuilder<T> {
|
||||
private Func<T, IEnumerable<T>> childGetter;
|
||||
private Func<T, bool> canExpand;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user
|
||||
/// defined method <paramref name="childGetter"/> to determine children
|
||||
/// and <paramref name="canExpand"/> to determine expandability
|
||||
/// </summary>
|
||||
/// <param name="childGetter"></param>
|
||||
/// <param name="canExpand"></param>
|
||||
/// <returns></returns>
|
||||
public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter, Func<T, bool> canExpand) : base (true)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using NStack;
|
||||
using Terminal.Gui.Trees;
|
||||
|
||||
namespace Terminal.Gui {
|
||||
|
||||
@@ -1216,37 +1217,6 @@ namespace Terminal.Gui {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for the <see cref="TreeView{T}.ObjectActivated"/> event
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class ObjectActivatedEventArgs<T> where T : class {
|
||||
|
||||
/// <summary>
|
||||
/// The tree in which the activation occurred
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public TreeView<T> Tree { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The object that was selected at the time of activation
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public T ActivatedObject { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance documenting activation of the <paramref name="activated"/> object
|
||||
/// </summary>
|
||||
/// <param name="tree">Tree in which the activation is happening</param>
|
||||
/// <param name="activated">What object is being activated</param>
|
||||
public ObjectActivatedEventArgs (TreeView<T> tree, T activated)
|
||||
{
|
||||
Tree = tree;
|
||||
ActivatedObject = activated;
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSelection<T> where T : class {
|
||||
|
||||
public Branch<T> Origin { get; }
|
||||
@@ -1281,491 +1251,4 @@ namespace Terminal.Gui {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var 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 (Rune.ColumnWidth) +
|
||||
Rune.ColumnWidth (GetExpandableSymbol (driver)) +
|
||||
(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)
|
||||
{
|
||||
// true if the current line of the tree is the selected one and control has focus
|
||||
bool isSelected = tree.IsSelected (Model) && tree.HasFocus;
|
||||
Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal;
|
||||
|
||||
driver.SetAttribute (lineColor);
|
||||
|
||||
// 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;
|
||||
|
||||
// Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol)
|
||||
foreach (Rune r in prefix) {
|
||||
|
||||
if (toSkip > 0) {
|
||||
toSkip--;
|
||||
} else {
|
||||
driver.AddRune (r);
|
||||
availableWidth -= Rune.ColumnWidth (r);
|
||||
}
|
||||
}
|
||||
|
||||
// pick color for expanded symbol
|
||||
if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) {
|
||||
Attribute color;
|
||||
|
||||
if (tree.Style.ColorExpandSymbol) {
|
||||
color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
|
||||
} else {
|
||||
color = lineColor;
|
||||
}
|
||||
|
||||
if (tree.Style.InvertExpandSymbolColors) {
|
||||
color = new Attribute (color.Background, color.Foreground);
|
||||
}
|
||||
|
||||
driver.SetAttribute (color);
|
||||
}
|
||||
|
||||
if (toSkip > 0) {
|
||||
toSkip--;
|
||||
} else {
|
||||
driver.AddRune (expansion);
|
||||
availableWidth -= Rune.ColumnWidth (expansion);
|
||||
}
|
||||
|
||||
// horizontal scrolling has already skipped the prefix but now must also skip some of the line body
|
||||
if (toSkip > 0) {
|
||||
if (toSkip > lineBody.Length) {
|
||||
lineBody = "";
|
||||
} else {
|
||||
lineBody = lineBody.Substring (toSkip);
|
||||
}
|
||||
}
|
||||
|
||||
// If body of line is too long
|
||||
if (lineBody.Sum (l => Rune.ColumnWidth (l)) > availableWidth) {
|
||||
// remaining space is zero and truncate the line
|
||||
lineBody = new string (lineBody.TakeWhile (c => (availableWidth -= Rune.ColumnWidth (c)) >= 0).ToArray ());
|
||||
availableWidth = 0;
|
||||
} else {
|
||||
|
||||
// line is short so remaining width will be whatever comes after the line body
|
||||
availableWidth -= lineBody.Length;
|
||||
}
|
||||
|
||||
//reset the line color if it was changed for rendering expansion symbol
|
||||
driver.SetAttribute (lineColor);
|
||||
driver.AddStr (lineBody);
|
||||
|
||||
if (availableWidth > 0) {
|
||||
driver.AddStr (new string (' ', availableWidth));
|
||||
}
|
||||
|
||||
driver.SetAttribute (colorScheme.Normal);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private 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 driver.VLine;
|
||||
}
|
||||
|
||||
yield return new Rune (' ');
|
||||
}
|
||||
|
||||
if (IsLast ()) {
|
||||
yield return driver.LLCorner;
|
||||
} else {
|
||||
yield return driver.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 ? driver.HLine : ' ';
|
||||
|
||||
if (IsExpanded) {
|
||||
return tree.Style.CollapseableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
if (CanExpand ()) {
|
||||
return tree.Style.ExpandableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
return 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 != null) {
|
||||
return x == GetLinePrefix (driver).Count ();
|
||||
}
|
||||
|
||||
// if we could theoretically collapse
|
||||
if (IsExpanded && tree.Style.CollapseableSymbol != null) {
|
||||
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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <returns></returns>
|
||||
public delegate string AspectGetterDelegate<T> (T toRender) where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments describing a change in selected object in a tree view
|
||||
/// </summary>
|
||||
public class SelectionChangedEventArgs<T> : EventArgs where T : class {
|
||||
/// <summary>
|
||||
/// The view in which the change occurred
|
||||
/// </summary>
|
||||
public TreeView<T> Tree { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The previously selected value (can be null)
|
||||
/// </summary>
|
||||
public T OldValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The newly selected value in the <see cref="Tree"/> (can be null)
|
||||
/// </summary>
|
||||
public T NewValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of event args describing a change of selection
|
||||
/// in <paramref name="tree"/>
|
||||
/// </summary>
|
||||
/// <param name="tree"></param>
|
||||
/// <param name="oldValue"></param>
|
||||
/// <param name="newValue"></param>
|
||||
public SelectionChangedEventArgs (TreeView<T> tree, T oldValue, T newValue)
|
||||
{
|
||||
Tree = tree;
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
static class ReadOnlyCollectionExtensions {
|
||||
|
||||
public static int IndexOf<T> (this IReadOnlyCollection<T> self, Func<T,bool> predicate)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (T element in self) {
|
||||
if (predicate(element))
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
public static int IndexOf<T> (this IReadOnlyCollection<T> self, T toFind)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (T element in self) {
|
||||
if (Equals(element,toFind))
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
|
||||
namespace UICatalog.Scenarios {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
using static UICatalog.Scenario;
|
||||
|
||||
namespace UICatalog.Scenarios {
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
|
||||
namespace UICatalog.Scenarios {
|
||||
[ScenarioMetadata (Name: "Tree View", Description: "Simple tree view examples")]
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
|
||||
namespace UICatalog.Scenarios {
|
||||
[ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView")]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Trees;
|
||||
using Xunit;
|
||||
|
||||
namespace Terminal.Gui.Views {
|
||||
|
||||
Reference in New Issue
Block a user