mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
@@ -87,8 +87,8 @@ public class TreeTableSource<T> : IEnumerableTableSource<T>, IDisposable where T
|
||||
Branch<T> 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);
|
||||
Rune [] prefix = branch.GetLinePrefix ().ToArray ();
|
||||
Rune expansion = branch.GetExpandableSymbol ();
|
||||
string lineBody = _tree.AspectGetter (branch.Model) ?? "";
|
||||
|
||||
var sb = new StringBuilder ();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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
|
||||
@@ -11,9 +13,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 { })
|
||||
@@ -27,7 +29,7 @@ internal class Branch<T> where T : class
|
||||
/// 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 List<Branch<T>>? ChildBranches { get; set; }
|
||||
|
||||
/// <summary>The depth of the current branch. Depth of 0 indicates root level branches.</summary>
|
||||
public int Depth { get; }
|
||||
@@ -39,7 +41,7 @@ internal class Branch<T> where T : class
|
||||
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
|
||||
@@ -52,13 +54,13 @@ internal class Branch<T> where T : class
|
||||
if (ChildBranches is null)
|
||||
{
|
||||
//if there is a rapid method for determining whether there are children
|
||||
if (tree.TreeBuilder.SupportsCanExpand)
|
||||
if (_tree.TreeBuilder.SupportsCanExpand)
|
||||
{
|
||||
return tree.TreeBuilder.CanExpand (Model);
|
||||
return _tree.TreeBuilder.CanExpand (Model);
|
||||
}
|
||||
|
||||
//there is no way of knowing whether we can expand without fetching the children
|
||||
FetchChildren ();
|
||||
ChildBranches = FetchChildren ();
|
||||
}
|
||||
|
||||
//we fetched or already know the children, so return whether we have any
|
||||
@@ -69,32 +71,30 @@ internal class Branch<T> where T : class
|
||||
public void Collapse () { IsExpanded = false; }
|
||||
|
||||
/// <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 (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
|
||||
public virtual void Draw (int y, int availableWidth)
|
||||
{
|
||||
List<Cell> cells = new ();
|
||||
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);
|
||||
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 ? _tree.GetFocusColor () : _tree.GetHotNormalColor () : _tree.GetNormalColor ();
|
||||
Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? _tree.GetNormalColor () : 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) ?? "";
|
||||
Rune [] prefix = GetLinePrefix ().ToArray ();
|
||||
Rune expansion = GetExpandableSymbol ();
|
||||
string lineBody = _tree.AspectGetter (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)
|
||||
@@ -112,20 +112,20 @@ internal class Branch<T> where T : class
|
||||
}
|
||||
|
||||
// pick color for expanded symbol
|
||||
if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors)
|
||||
if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors)
|
||||
{
|
||||
Attribute color = symbolColor;
|
||||
Attribute color;
|
||||
|
||||
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 ? _tree.GetHotNormalColor () :
|
||||
_tree.HasFocus ? _tree.GetHotFocusColor () : _tree.GetHotNormalColor ();
|
||||
}
|
||||
else
|
||||
{
|
||||
color = tree.ColorScheme.HotNormal;
|
||||
color = _tree.GetHotNormalColor ();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -133,9 +133,9 @@ internal class Branch<T> where T : class
|
||||
color = symbolColor;
|
||||
}
|
||||
|
||||
if (tree.Style.InvertExpandSymbolColors)
|
||||
if (_tree.Style.InvertExpandSymbolColors)
|
||||
{
|
||||
color = new Attribute (color.Background, color.Foreground);
|
||||
color = new (color.Background, color.Foreground);
|
||||
}
|
||||
|
||||
attr = color;
|
||||
@@ -177,10 +177,10 @@ internal class Branch<T> where T : class
|
||||
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 ()
|
||||
);
|
||||
lineBody = new (
|
||||
lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0)
|
||||
.ToArray ()
|
||||
);
|
||||
availableWidth = 0;
|
||||
}
|
||||
else
|
||||
@@ -194,9 +194,9 @@ internal class Branch<T> where T : class
|
||||
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 { })
|
||||
@@ -206,12 +206,12 @@ internal class Branch<T> where T : class
|
||||
}
|
||||
else
|
||||
{
|
||||
modelColor = new Attribute ();
|
||||
modelColor = new ();
|
||||
}
|
||||
}
|
||||
|
||||
attr = modelColor;
|
||||
cells.AddRange (lineBody.Select (r => NewCell (attr, new Rune (r))));
|
||||
cells.AddRange (lineBody.Select (r => NewCell (attr, new (r))));
|
||||
|
||||
if (availableWidth > 0)
|
||||
{
|
||||
@@ -219,7 +219,7 @@ internal class Branch<T> where T : class
|
||||
|
||||
cells.AddRange (
|
||||
Enumerable.Repeat (
|
||||
NewCell (attr, new Rune (' ')),
|
||||
NewCell (attr, new (' ')),
|
||||
availableWidth
|
||||
)
|
||||
);
|
||||
@@ -230,32 +230,29 @@ internal class Branch<T> where T : class
|
||||
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);
|
||||
_tree.SetAttribute ((Attribute)cell.Attribute!);
|
||||
_tree.AddRune (cell.Rune);
|
||||
}
|
||||
}
|
||||
|
||||
driver?.SetAttribute (colorScheme.Normal);
|
||||
_tree.SetAttribute (_tree.GetNormalColor());
|
||||
}
|
||||
|
||||
/// <summary>Expands the current branch if possible.</summary>
|
||||
public void Expand ()
|
||||
{
|
||||
if (ChildBranches is null)
|
||||
{
|
||||
FetchChildren ();
|
||||
}
|
||||
ChildBranches ??= FetchChildren ();
|
||||
|
||||
if (ChildBranches.Any ())
|
||||
{
|
||||
@@ -264,45 +261,44 @@ internal class Branch<T> where T : class
|
||||
}
|
||||
|
||||
/// <summary>Fetch the children of this branch. This method populates <see cref="ChildBranches"/>.</summary>
|
||||
public virtual void FetchChildren ()
|
||||
private List<Branch<T>> FetchChildren ()
|
||||
{
|
||||
if (tree.TreeBuilder is null)
|
||||
if (_tree.TreeBuilder is null)
|
||||
{
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
IEnumerable<T> children;
|
||||
|
||||
if (Depth >= tree.MaxDepth)
|
||||
if (Depth >= _tree.MaxDepth)
|
||||
{
|
||||
children = Enumerable.Empty<T> ();
|
||||
children = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty<T> ();
|
||||
children = _tree.TreeBuilder.GetChildren (Model) ?? [];
|
||||
}
|
||||
|
||||
ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
|
||||
return children.Select (o => new Branch<T> (_tree, this, o)).ToList ();
|
||||
}
|
||||
|
||||
/// <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 (IConsoleDriver driver)
|
||||
public Rune GetExpandableSymbol ()
|
||||
{
|
||||
Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
|
||||
Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
|
||||
|
||||
if (IsExpanded)
|
||||
{
|
||||
return tree.Style.CollapseableSymbol ?? leafSymbol;
|
||||
return _tree.Style.CollapseableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
if (CanExpand ())
|
||||
{
|
||||
return tree.Style.ExpandableSymbol ?? leafSymbol;
|
||||
return _tree.Style.ExpandableSymbol ?? leafSymbol;
|
||||
}
|
||||
|
||||
return leafSymbol;
|
||||
@@ -313,10 +309,10 @@ internal class Branch<T> where T : class
|
||||
/// line body).
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual int GetWidth (IConsoleDriver driver)
|
||||
public virtual int GetWidth ()
|
||||
{
|
||||
return
|
||||
GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length;
|
||||
GetLinePrefix ().Sum (r => r.GetColumns ()) + GetExpandableSymbol ().GetColumns () + (_tree.AspectGetter (Model) ?? "").Length;
|
||||
}
|
||||
|
||||
/// <summary>Refreshes cached knowledge in this branch e.g. what children an object has.</summary>
|
||||
@@ -333,41 +329,46 @@ internal class Branch<T> where T : class
|
||||
//if we don't know about any children yet just use the normal method
|
||||
if (ChildBranches is null)
|
||||
{
|
||||
FetchChildren ();
|
||||
ChildBranches = FetchChildren ();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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> ();
|
||||
T [] newChildren = _tree.TreeBuilder?.GetChildren (Model).ToArray () ?? [];
|
||||
|
||||
// Children who no longer appear need to go
|
||||
foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ())
|
||||
foreach (Branch<T> toRemove in ChildBranches.Where (b => !newChildren.Contains (b.Model)).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.Model))
|
||||
{
|
||||
tree.SelectedObject = Model;
|
||||
_tree.SelectedObject = Model;
|
||||
}
|
||||
}
|
||||
|
||||
// New children need to be added
|
||||
foreach (T newChild in newChildren)
|
||||
{
|
||||
Branch<T>? existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild));
|
||||
|
||||
// If we don't know about the child, yet we need a new branch
|
||||
if (!ChildBranches.ContainsKey (newChild))
|
||||
if (existingBranch == null)
|
||||
{
|
||||
ChildBranches.Add (newChild, new Branch<T> (tree, this, newChild));
|
||||
ChildBranches.Add (new (_tree, this, newChild));
|
||||
}
|
||||
else
|
||||
{
|
||||
//we already have this object but update the reference anyway in case Equality match but the references are new
|
||||
ChildBranches [newChild].Model = newChild;
|
||||
existingBranch.Model = newChild;
|
||||
}
|
||||
}
|
||||
|
||||
// Order the list
|
||||
ChildBranches = ChildBranches.OrderBy (b => newChildren.IndexOf (b.Model)).ToList ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,9 +382,9 @@ internal class Branch<T> where T : class
|
||||
|
||||
if (ChildBranches is { })
|
||||
{
|
||||
foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
|
||||
foreach (Branch<T> child in ChildBranches)
|
||||
{
|
||||
child.Value.CollapseAll ();
|
||||
child.CollapseAll ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,9 +396,9 @@ internal class Branch<T> where T : class
|
||||
|
||||
if (ChildBranches is { })
|
||||
{
|
||||
foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
|
||||
foreach (Branch<T> child in ChildBranches)
|
||||
{
|
||||
child.Value.ExpandAll ();
|
||||
child.ExpandAll ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,16 +407,15 @@ internal class Branch<T> where T : class
|
||||
/// 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 (IConsoleDriver driver)
|
||||
internal IEnumerable<Rune> GetLinePrefix ()
|
||||
{
|
||||
// If not showing line branches or this is a root object.
|
||||
if (!tree.Style.ShowBranchLines)
|
||||
if (!_tree.Style.ShowBranchLines)
|
||||
{
|
||||
for (var i = 0; i < Depth; i++)
|
||||
{
|
||||
yield return new Rune (' ');
|
||||
yield return new (' ');
|
||||
}
|
||||
|
||||
yield break;
|
||||
@@ -426,14 +426,14 @@ internal class Branch<T> where T : class
|
||||
{
|
||||
if (cur.IsLast ())
|
||||
{
|
||||
yield return new Rune (' ');
|
||||
yield return new (' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Glyphs.VLine;
|
||||
}
|
||||
|
||||
yield return new Rune (' ');
|
||||
yield return new (' ');
|
||||
}
|
||||
|
||||
if (IsLast ())
|
||||
@@ -462,15 +462,15 @@ internal class Branch<T> where T : class
|
||||
}
|
||||
|
||||
// if we could theoretically expand
|
||||
if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?))
|
||||
if (!IsExpanded && _tree.Style.ExpandableSymbol != default (Rune?))
|
||||
{
|
||||
return x == GetLinePrefix (driver).Count ();
|
||||
return x == GetLinePrefix ().Count ();
|
||||
}
|
||||
|
||||
// if we could theoretically collapse
|
||||
if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?))
|
||||
if (IsExpanded && _tree.Style.CollapseableSymbol != default (Rune?))
|
||||
{
|
||||
return x == GetLinePrefix (driver).Count ();
|
||||
return x == GetLinePrefix ().Count ();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -487,9 +487,9 @@ internal class Branch<T> where T : class
|
||||
if (IsExpanded)
|
||||
{
|
||||
// if we are expanded we need to update the visible children
|
||||
foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
|
||||
foreach (Branch<T> child in ChildBranches)
|
||||
{
|
||||
child.Value.Rebuild ();
|
||||
child.Rebuild ();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -504,7 +504,7 @@ internal class Branch<T> where T : class
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Branch<T>> GetParentBranches ()
|
||||
{
|
||||
Branch<T> cur = Parent;
|
||||
Branch<T>? cur = Parent;
|
||||
|
||||
while (cur is { })
|
||||
{
|
||||
@@ -523,11 +523,13 @@ internal class Branch<T> where T : class
|
||||
{
|
||||
if (Parent is null)
|
||||
{
|
||||
return this == tree.roots.Values.LastOrDefault ();
|
||||
return this == _tree.roots.Values.LastOrDefault ();
|
||||
}
|
||||
|
||||
return Parent.ChildBranches.Values.LastOrDefault () == this;
|
||||
Parent.ChildBranches ??= Parent.FetchChildren ();
|
||||
|
||||
return Parent.ChildBranches.LastOrDefault () == this;
|
||||
}
|
||||
|
||||
private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; }
|
||||
private static Cell NewCell (Attribute attr, Rune r) { return new() { Rune = r, Attribute = new (attr) }; }
|
||||
}
|
||||
|
||||
@@ -847,7 +847,7 @@ public class TreeView<T> : View, ITreeView where T : class
|
||||
return new T [0];
|
||||
}
|
||||
|
||||
return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
|
||||
return branch.ChildBranches?.Select (b => b.Model)?.ToArray () ?? new T [0];
|
||||
}
|
||||
|
||||
/// <summary>Returns the maximum width line in the tree including prefix and expansion symbols.</summary>
|
||||
@@ -879,10 +879,10 @@ public class TreeView<T> : View, ITreeView where T : class
|
||||
return 0;
|
||||
}
|
||||
|
||||
return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth (Driver));
|
||||
return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth ());
|
||||
}
|
||||
|
||||
return map.Max (b => b.GetWidth (Driver));
|
||||
return map.Max (b => b.GetWidth ());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1171,7 +1171,7 @@ public class TreeView<T> : View, ITreeView where T : class
|
||||
if (idxToRender < map.Count)
|
||||
{
|
||||
// Render the line
|
||||
map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Viewport.Width);
|
||||
map.ElementAt (idxToRender).Draw (line, Viewport.Width);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1488,7 +1488,7 @@ public class TreeView<T> : View, ITreeView where T : class
|
||||
|
||||
if (currentBranch.IsExpanded)
|
||||
{
|
||||
foreach (Branch<T> subBranch in currentBranch.ChildBranches.Values)
|
||||
foreach (Branch<T> subBranch in currentBranch.ChildBranches)
|
||||
{
|
||||
foreach (Branch<T> sub in AddToLineMap (subBranch, weMatch, out bool childMatch))
|
||||
{
|
||||
|
||||
@@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -123,6 +125,10 @@ Global
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
|
||||
<PackageReference Include="xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
25
TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs
Normal file
25
TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Xunit;
|
||||
|
||||
namespace TerminalGuiFluentTesting;
|
||||
|
||||
public static class XunitContextExtensions
|
||||
{
|
||||
public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition)
|
||||
{
|
||||
context.Then (
|
||||
() =>
|
||||
{
|
||||
Assert.True (condition);
|
||||
});
|
||||
return context;
|
||||
}
|
||||
public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual)
|
||||
{
|
||||
context.Then (
|
||||
() =>
|
||||
{
|
||||
Assert.Equal (expected,actual);
|
||||
});
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,18 @@ public class GuiTestContext : IDisposable
|
||||
/// <returns></returns>
|
||||
public GuiTestContext Then (Action doAction)
|
||||
{
|
||||
doAction ();
|
||||
try
|
||||
{
|
||||
doAction ();
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
Stop ();
|
||||
_hardStop.Cancel();
|
||||
|
||||
throw;
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -360,6 +371,7 @@ public class GuiTestContext : IDisposable
|
||||
{
|
||||
SendNetKey (k);
|
||||
}
|
||||
WaitIteration ();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException ();
|
||||
@@ -550,4 +562,25 @@ public class GuiTestContext : IDisposable
|
||||
|
||||
WaitIteration ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the input focus to the given <see cref="View"/>.
|
||||
/// Throws <see cref="ArgumentException"/> if focus did not change due to system
|
||||
/// constraints e.g. <paramref name="toFocus"/>
|
||||
/// <see cref="View.CanFocus"/> is <see langword="false"/>
|
||||
/// </summary>
|
||||
/// <param name="toFocus"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public GuiTestContext Focus (View toFocus)
|
||||
{
|
||||
toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop);
|
||||
|
||||
if (!toFocus.HasFocus)
|
||||
{
|
||||
throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable");
|
||||
}
|
||||
|
||||
return WaitIteration ();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui;
|
||||
using TerminalGuiFluentTesting;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -9,17 +8,6 @@ public class BasicFluentAssertionTests
|
||||
{
|
||||
private readonly TextWriter _out;
|
||||
|
||||
public class TestOutputWriter : TextWriter
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public TestOutputWriter (ITestOutputHelper output) { _output = output; }
|
||||
|
||||
public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); }
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
}
|
||||
|
||||
public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
|
||||
|
||||
[Theory]
|
||||
|
||||
15
Tests/IntegrationTests/FluentTests/TestOutputWriter.cs
Normal file
15
Tests/IntegrationTests/FluentTests/TestOutputWriter.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace IntegrationTests.FluentTests;
|
||||
|
||||
public class TestOutputWriter : TextWriter
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public TestOutputWriter (ITestOutputHelper output) { _output = output; }
|
||||
|
||||
public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); }
|
||||
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
}
|
||||
162
Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs
Normal file
162
Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Terminal.Gui;
|
||||
using TerminalGuiFluentTesting;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace IntegrationTests.FluentTests;
|
||||
|
||||
public class TreeViewFluentTests
|
||||
{
|
||||
private readonly TextWriter _out;
|
||||
|
||||
public TreeViewFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void TreeView_AllowReOrdering (V2TestDriver d)
|
||||
{
|
||||
var tv = new TreeView
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
TreeNode car;
|
||||
TreeNode lorry;
|
||||
TreeNode bike;
|
||||
|
||||
var root = new TreeNode ("Root")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
car = new ("Car"),
|
||||
lorry = new ("Lorry"),
|
||||
bike = new ("Bike")
|
||||
]
|
||||
};
|
||||
|
||||
tv.AddObject (root);
|
||||
|
||||
using GuiTestContext context =
|
||||
With.A<Window> (40, 10, d)
|
||||
.Add (tv)
|
||||
.Focus (tv)
|
||||
.WaitIteration ()
|
||||
.ScreenShot ("Before expanding", _out)
|
||||
.AssertEqual (root, tv.GetObjectOnRow (0))
|
||||
.Then (() => Assert.Null (tv.GetObjectOnRow (1)))
|
||||
.Right ()
|
||||
.ScreenShot ("After expanding", _out)
|
||||
.AssertEqual (root, tv.GetObjectOnRow (0))
|
||||
.AssertEqual (car, tv.GetObjectOnRow (1))
|
||||
.AssertEqual (lorry, tv.GetObjectOnRow (2))
|
||||
.AssertEqual (bike, tv.GetObjectOnRow (3))
|
||||
.Then (
|
||||
() =>
|
||||
{
|
||||
// Re order
|
||||
root.Children = [bike, car, lorry];
|
||||
tv.RefreshObject (root);
|
||||
})
|
||||
.WaitIteration ()
|
||||
.ScreenShot ("After re-order", _out)
|
||||
.AssertEqual (root, tv.GetObjectOnRow (0))
|
||||
.AssertEqual (bike, tv.GetObjectOnRow (1))
|
||||
.AssertEqual (car, tv.GetObjectOnRow (2))
|
||||
.AssertEqual (lorry, tv.GetObjectOnRow (3))
|
||||
.WriteOutLogs (_out);
|
||||
|
||||
context.Stop ();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[ClassData (typeof (V2TestDrivers))]
|
||||
public void TreeViewReOrder_PreservesExpansion (V2TestDriver d)
|
||||
{
|
||||
var tv = new TreeView
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
TreeNode car;
|
||||
TreeNode lorry;
|
||||
TreeNode bike;
|
||||
|
||||
TreeNode mrA;
|
||||
TreeNode mrB;
|
||||
|
||||
TreeNode mrC;
|
||||
|
||||
TreeNode mrD;
|
||||
TreeNode mrE;
|
||||
|
||||
var root = new TreeNode ("Root")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
car = new ("Car")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
mrA = new ("Mr A"),
|
||||
mrB = new ("Mr B")
|
||||
]
|
||||
},
|
||||
lorry = new ("Lorry")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
mrC = new ("Mr C")
|
||||
]
|
||||
},
|
||||
bike = new ("Bike")
|
||||
{
|
||||
Children =
|
||||
[
|
||||
mrD = new ("Mr D"),
|
||||
mrE = new ("Mr E")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
tv.AddObject (root);
|
||||
tv.ExpandAll ();
|
||||
|
||||
using GuiTestContext context =
|
||||
With.A<Window> (40, 13, d)
|
||||
.Add (tv)
|
||||
.WaitIteration ()
|
||||
.ScreenShot ("Initial State", _out)
|
||||
.AssertEqual (root, tv.GetObjectOnRow (0))
|
||||
.AssertEqual (car, tv.GetObjectOnRow (1))
|
||||
.AssertEqual (mrA, tv.GetObjectOnRow (2))
|
||||
.AssertEqual (mrB, tv.GetObjectOnRow (3))
|
||||
.AssertEqual (lorry, tv.GetObjectOnRow (4))
|
||||
.AssertEqual (mrC, tv.GetObjectOnRow (5))
|
||||
.AssertEqual (bike, tv.GetObjectOnRow (6))
|
||||
.AssertEqual (mrD, tv.GetObjectOnRow (7))
|
||||
.AssertEqual (mrE, tv.GetObjectOnRow (8))
|
||||
.Then (
|
||||
() =>
|
||||
{
|
||||
// Re order
|
||||
root.Children = [bike, car, lorry];
|
||||
tv.RefreshObject (root);
|
||||
})
|
||||
.WaitIteration ()
|
||||
.ScreenShot ("After re-order", _out)
|
||||
.AssertEqual (root, tv.GetObjectOnRow (0))
|
||||
.AssertEqual (bike, tv.GetObjectOnRow (1))
|
||||
.AssertEqual (mrD, tv.GetObjectOnRow (2))
|
||||
.AssertEqual (mrE, tv.GetObjectOnRow (3))
|
||||
.AssertEqual (car, tv.GetObjectOnRow (4))
|
||||
.AssertEqual (mrA, tv.GetObjectOnRow (5))
|
||||
.AssertEqual (mrB, tv.GetObjectOnRow (6))
|
||||
.AssertEqual (lorry, tv.GetObjectOnRow (7))
|
||||
.AssertEqual (mrC, tv.GetObjectOnRow (8))
|
||||
.WriteOutLogs (_out);
|
||||
|
||||
context.Stop ();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
<ProjectReference Include="..\..\TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj" />
|
||||
<ProjectReference Include="..\..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
|
||||
<ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
|
||||
<ProjectReference Include="..\UnitTests\UnitTests.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user