Merge branch 'v2_develop' of tig:gui-cs/Terminal.Gui into v2_develop

This commit is contained in:
Tigger Kindel
2023-07-25 09:14:09 -06:00
5 changed files with 352 additions and 45 deletions

View File

@@ -65,11 +65,10 @@ namespace Terminal.Gui {
if (Depth >= tree.MaxDepth) {
children = Enumerable.Empty<T> ();
}
else {
} else {
children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty<T> ();
}
this.ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
}
@@ -95,6 +94,11 @@ namespace Terminal.Gui {
/// <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);
@@ -110,15 +114,15 @@ namespace Terminal.Gui {
// 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;
driver.SetAttribute (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 {
driver.AddRune (r);
cells.Add (NewRuneCell (attr, r));
availableWidth -= r.GetColumns ();
}
}
@@ -141,23 +145,31 @@ namespace Terminal.Gui {
color = new Attribute (color.Background, color.Foreground);
}
driver.SetAttribute (color);
attr = color;
}
if (toSkip > 0) {
toSkip--;
} else {
driver.AddRune (expansion);
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
@@ -186,16 +198,47 @@ namespace Terminal.Gui {
}
}
driver.SetAttribute (modelColor);
driver.AddStr (lineBody);
attr = modelColor;
cells.AddRange (lineBody.Select (r => NewRuneCell (attr, new Rune (r))));
if (availableWidth > 0) {
driver.SetAttribute (symbolColor);
driver.AddStr (new string (' ', availableWidth));
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).

View File

@@ -0,0 +1,66 @@
// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls
// by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design
// and code to be used in this library under the MIT license.
using System.Collections.Generic;
namespace Terminal.Gui {
/// <summary>
/// Event args for the <see cref="TreeView{T}.DrawLine"/> event
/// </summary>
/// <typeparam name="T"></typeparam>
public class DrawTreeViewLineEventArgs<T> where T : class {
/// <summary>
/// The object at this line in the tree
/// </summary>
public T Model { get; init; }
/// <summary>
/// The <see cref="TreeView{T}"/> that is performing the
/// rendering.
/// </summary>
public TreeView<T> Tree { get; init; }
/// <summary>
/// The line within tree view bounds that is being rendered
/// </summary>
public int Y { get; init; }
/// <summary>
/// Set to true to cancel drawing (e.g. if you have already manually
/// drawn content).
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// The rune and color of each symbol that will be rendered. Note
/// that only <see cref="ColorScheme.Normal"/> is respected. You
/// can modify these to change what is rendered.
/// </summary>
/// <remarks>
/// Changing the length of this collection may result in corrupt rendering
/// </remarks>
public List<RuneCell> RuneCells { get; init; }
/// <summary>
/// The notional index in <see cref="RuneCells"/> which contains the first
/// character of the <see cref="TreeView{T}.AspectGetter"/> text (i.e.
/// after all branch lines and expansion/collapse sybmols).
/// </summary>
/// <remarks>
/// May be negative or outside of bounds of <see cref="RuneCells"/> if the view
/// has been scrolled horizontally.
/// </remarks>
public int IndexOfModelText { get; init; }
/// <summary>
/// If line contains a branch that can be expanded/collapsed then this is
/// the index in <see cref="RuneCells"/> at which the symbol is (or null for
/// leaf elements).
/// </summary>
public int? IndexOfExpandCollapseSymbol { get; init; }
}
}

View File

@@ -169,6 +169,12 @@ namespace Terminal.Gui {
/// </summary>
public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
/// <summary>
/// Called once for each visible row during rendering. Can be used
/// to make last minute changes to color or text rendered
/// </summary>
public event EventHandler<DrawTreeViewLineEventArgs<T>> DrawLine;
/// <summary>
/// The root objects in the tree, note that this collection is of root objects only.
/// </summary>
@@ -557,10 +563,9 @@ namespace Terminal.Gui {
List<Branch<T>> toReturn = new List<Branch<T>> ();
foreach (var root in roots.Values) {
var toAdd = AddToLineMap (root, false, out var isMatch);
if(isMatch)
{
if (isMatch) {
toReturn.AddRange (toAdd);
}
}
@@ -574,41 +579,38 @@ namespace Terminal.Gui {
private bool IsFilterMatch (Branch<T> branch)
{
return Filter?.IsMatch(branch.Model) ?? true;
return Filter?.IsMatch (branch.Model) ?? true;
}
private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch,bool parentMatches, out bool match)
private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch, bool parentMatches, out bool match)
{
bool weMatch = IsFilterMatch(currentBranch);
bool weMatch = IsFilterMatch (currentBranch);
bool anyChildMatches = false;
var toReturn = new List<Branch<T>>();
var children = new List<Branch<T>>();
var toReturn = new List<Branch<T>> ();
var children = new List<Branch<T>> ();
if (currentBranch.IsExpanded) {
foreach (var subBranch in currentBranch.ChildBranches.Values) {
foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) {
if(childMatch)
{
children.Add(sub);
if (childMatch) {
children.Add (sub);
anyChildMatches = true;
}
}
}
}
if(parentMatches || weMatch || anyChildMatches)
{
if (parentMatches || weMatch || anyChildMatches) {
match = true;
toReturn.Add(currentBranch);
}
else{
toReturn.Add (currentBranch);
} else {
match = false;
}
toReturn.AddRange(children);
toReturn.AddRange (children);
return toReturn;
}
@@ -1421,8 +1423,17 @@ namespace Terminal.Gui {
{
SelectionChanged?.Invoke (this, e);
}
}
/// <summary>
/// Raises the DrawLine event
/// </summary>
/// <param name="e"></param>
internal void OnDrawLine (DrawTreeViewLineEventArgs<T> e)
{
DrawLine?.Invoke (this, e);
}
}
class TreeSelection<T> where T : class {
public Branch<T> Origin { get; }

View File

@@ -84,6 +84,7 @@ namespace UICatalog.Scenarios {
Width = Dim.Percent (50),
Height = Dim.Fill (),
};
treeViewFiles.DrawLine += TreeViewFiles_DrawLine;
_detailsFrame = new DetailsFrame (_iconProvider) {
X = Pos.Right (treeViewFiles),
@@ -106,7 +107,7 @@ namespace UICatalog.Scenarios {
SetupScrollBar ();
treeViewFiles.SetFocus ();
UpdateIconCheckedness ();
}
@@ -140,6 +141,23 @@ namespace UICatalog.Scenarios {
ShowPropertiesOf (e.NewValue);
}
private void TreeViewFiles_DrawLine (object sender, DrawTreeViewLineEventArgs<IFileSystemInfo> e)
{
// Render directory icons in yellow
if (e.Model is IDirectoryInfo d) {
if (_iconProvider.UseNerdIcons || _iconProvider.UseUnicodeCharacters) {
if (e.IndexOfModelText > 0 && e.IndexOfModelText < e.RuneCells.Count) {
var cell = e.RuneCells [e.IndexOfModelText];
cell.ColorScheme = new ColorScheme (
new Terminal.Gui.Attribute (
Color.BrightYellow,
cell.ColorScheme.Normal.Background)
);
}
}
}
}
private void TreeViewFiles_KeyPress (object sender, KeyEventEventArgs obj)
{
if (obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) {
@@ -195,7 +213,7 @@ namespace UICatalog.Scenarios {
private IFileSystemInfo fileInfo;
private FileSystemIconProvider _iconProvider;
public DetailsFrame (FileSystemIconProvider iconProvider)
public DetailsFrame (FileSystemIconProvider iconProvider)
{
Title = "Details";
Visible = true;
@@ -209,7 +227,7 @@ namespace UICatalog.Scenarios {
System.Text.StringBuilder sb = null;
if (fileInfo is IFileInfo f) {
Title = $"{_iconProvider.GetIconWithOptionalSpace(f)}{f.Name}".Trim();
Title = $"{_iconProvider.GetIconWithOptionalSpace (f)}{f.Name}".Trim ();
sb = new System.Text.StringBuilder ();
sb.AppendLine ($"Path:\n {f.FullName}\n");
sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n");
@@ -218,7 +236,7 @@ namespace UICatalog.Scenarios {
}
if (fileInfo is IDirectoryInfo dir) {
Title = $"{_iconProvider.GetIconWithOptionalSpace(dir)}{dir.Name}".Trim();
Title = $"{_iconProvider.GetIconWithOptionalSpace (dir)}{dir.Name}".Trim ();
sb = new System.Text.StringBuilder ();
sb.AppendLine ($"Path:\n {dir?.FullName}\n");
sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n");
@@ -241,7 +259,7 @@ namespace UICatalog.Scenarios {
var scrollBar = new ScrollBarView (treeViewFiles, true);
scrollBar.ChangedPosition += (s,e) => {
scrollBar.ChangedPosition += (s, e) => {
treeViewFiles.ScrollOffsetVertical = scrollBar.Position;
if (treeViewFiles.ScrollOffsetVertical != scrollBar.Position) {
scrollBar.Position = treeViewFiles.ScrollOffsetVertical;
@@ -249,7 +267,7 @@ namespace UICatalog.Scenarios {
treeViewFiles.SetNeedsDisplay ();
};
scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
scrollBar.OtherScrollBarView.ChangedPosition += (s, e) => {
treeViewFiles.ScrollOffsetHorizontal = scrollBar.OtherScrollBarView.Position;
if (treeViewFiles.ScrollOffsetHorizontal != scrollBar.OtherScrollBarView.Position) {
scrollBar.OtherScrollBarView.Position = treeViewFiles.ScrollOffsetHorizontal;
@@ -257,7 +275,7 @@ namespace UICatalog.Scenarios {
treeViewFiles.SetNeedsDisplay ();
};
treeViewFiles.DrawContent += (s,e) => {
treeViewFiles.DrawContent += (s, e) => {
scrollBar.Size = treeViewFiles.ContentHeight;
scrollBar.Position = treeViewFiles.ScrollOffsetVertical;
scrollBar.OtherScrollBarView.Size = treeViewFiles.GetContentWidth (true);
@@ -269,20 +287,20 @@ namespace UICatalog.Scenarios {
private void SetupFileTree ()
{
// setup how to build tree
var fs = new FileSystem();
var rootDirs = DriveInfo.GetDrives ().Select (d=>fs.DirectoryInfo.New(d.RootDirectory.FullName));
var fs = new FileSystem ();
var rootDirs = DriveInfo.GetDrives ().Select (d => fs.DirectoryInfo.New (d.RootDirectory.FullName));
treeViewFiles.TreeBuilder = new FileSystemTreeBuilder ();
treeViewFiles.AddObjects (rootDirs);
// Determines how to represent objects as strings on the screen
treeViewFiles.AspectGetter = AspectGetter;
_iconProvider.IsOpenGetter = treeViewFiles.IsExpanded;
}
private string AspectGetter (IFileSystemInfo f)
{
return (_iconProvider.GetIconWithOptionalSpace(f) + f.Name).Trim();
return (_iconProvider.GetIconWithOptionalSpace (f) + f.Name).Trim ();
}
private void ShowLines ()

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
@@ -939,7 +940,7 @@ namespace Terminal.Gui.ViewsTests {
Assert.False (tv.CanExpand ("6"));
Assert.False (tv.IsExpanded ("6"));
tv.Collapse("6");
tv.Collapse ("6");
Assert.False (tv.CanExpand ("6"));
Assert.False (tv.IsExpanded ("6"));
@@ -992,6 +993,174 @@ namespace Terminal.Gui.ViewsTests {
└-2
└-3
└─4
", output);
}
[Fact, AutoInitShutdown]
public void TestTreeView_DrawLineEvent ()
{
var tv = new TreeView { Width = 20, Height = 10 };
var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
tv.DrawLine += (s, e) => {
eventArgs.Add (e);
};
var n1 = new TreeNode ("root one");
var n1_1 = new TreeNode ("leaf 1");
var n1_2 = new TreeNode ("leaf 2");
n1.Children.Add (n1_1);
n1.Children.Add (n1_2);
var n2 = new TreeNode ("root two");
tv.AddObject (n1);
tv.AddObject (n2);
tv.Expand (n1);
tv.ColorScheme = new ColorScheme ();
tv.LayoutSubviews ();
tv.Draw ();
// Normal drawing of the tree view
TestHelpers.AssertDriverContentsAre (
@"
├-root one
│ ├─leaf 1
│ └─leaf 2
└─root two
", output);
Assert.Equal (4, eventArgs.Count ());
Assert.Equal (0, eventArgs [0].Y);
Assert.Equal (1, eventArgs [1].Y);
Assert.Equal (2, eventArgs [2].Y);
Assert.Equal (3, eventArgs [3].Y);
Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
Assert.All (eventArgs, ea => Assert.False (ea.Handled));
Assert.Equal ("├-root one", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal ("│ ├─leaf 1", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal ("│ └─leaf 2", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal ("└─root two", eventArgs [3].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol);
Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol);
Assert.Equal (3, eventArgs [2].IndexOfExpandCollapseSymbol);
Assert.Equal (1, eventArgs [3].IndexOfExpandCollapseSymbol);
Assert.Equal (2, eventArgs [0].IndexOfModelText);
Assert.Equal (4, eventArgs [1].IndexOfModelText);
Assert.Equal (4, eventArgs [2].IndexOfModelText);
Assert.Equal (2, eventArgs [3].IndexOfModelText);
Assert.Equal ("root one", eventArgs [0].Model.Text);
Assert.Equal ("leaf 1", eventArgs [1].Model.Text);
Assert.Equal ("leaf 2", eventArgs [2].Model.Text);
Assert.Equal ("root two", eventArgs [3].Model.Text);
}
[Fact, AutoInitShutdown]
public void TestTreeView_DrawLineEvent_WithScrolling ()
{
var tv = new TreeView { Width = 20, Height = 10 };
var eventArgs = new List<DrawTreeViewLineEventArgs<ITreeNode>> ();
tv.DrawLine += (s, e) => {
eventArgs.Add (e);
};
tv.ScrollOffsetHorizontal = 3;
tv.ScrollOffsetVertical = 1;
var n1 = new TreeNode ("root one");
var n1_1 = new TreeNode ("leaf 1");
var n1_2 = new TreeNode ("leaf 2");
n1.Children.Add (n1_1);
n1.Children.Add (n1_2);
var n2 = new TreeNode ("root two");
tv.AddObject (n1);
tv.AddObject (n2);
tv.Expand (n1);
tv.ColorScheme = new ColorScheme ();
tv.LayoutSubviews ();
tv.Draw ();
// Normal drawing of the tree view
TestHelpers.AssertDriverContentsAre (
@"
─leaf 1
─leaf 2
oot two
", output);
Assert.Equal (3, eventArgs.Count ());
Assert.Equal (0, eventArgs [0].Y);
Assert.Equal (1, eventArgs [1].Y);
Assert.Equal (2, eventArgs [2].Y);
Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv));
Assert.All (eventArgs, ea => Assert.False (ea.Handled));
Assert.Equal ("─leaf 1", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal ("─leaf 2", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal ("oot two", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ());
Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol);
Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol);
Assert.Null (eventArgs [2].IndexOfExpandCollapseSymbol);
Assert.Equal (1, eventArgs [0].IndexOfModelText);
Assert.Equal (1, eventArgs [1].IndexOfModelText);
Assert.Equal (-1, eventArgs [2].IndexOfModelText);
Assert.Equal ("leaf 1", eventArgs [0].Model.Text);
Assert.Equal ("leaf 2", eventArgs [1].Model.Text);
Assert.Equal ("root two", eventArgs [2].Model.Text);
}
[Fact, AutoInitShutdown]
public void TestTreeView_DrawLineEvent_Handled ()
{
var tv = new TreeView { Width = 20, Height = 10 };
tv.DrawLine += (s, e) => {
if(e.Model.Text.Equals("leaf 1")) {
e.Handled = true;
for (int i = 0; i < 10; i++) {
e.Tree.AddRune (i,e.Y,new System.Text.Rune('F'));
}
}
};
var n1 = new TreeNode ("root one");
var n1_1 = new TreeNode ("leaf 1");
var n1_2 = new TreeNode ("leaf 2");
n1.Children.Add (n1_1);
n1.Children.Add (n1_2);
var n2 = new TreeNode ("root two");
tv.AddObject (n1);
tv.AddObject (n2);
tv.Expand (n1);
tv.ColorScheme = new ColorScheme ();
tv.LayoutSubviews ();
tv.Draw ();
// Normal drawing of the tree view
TestHelpers.AssertDriverContentsAre (
@"
├-root one
FFFFFFFFFF
│ └─leaf 2
└─root two
", output);
}