浏览代码

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

Tigger Kindel 2 年之前
父节点
当前提交
6be97f9f0e

+ 54 - 11
Terminal.Gui/Views/TreeView/Branch.cs

@@ -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).

+ 66 - 0
Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs

@@ -0,0 +1,66 @@
+// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls 
+// by [email protected]). Phillip has explicitly granted permission for his design
+// and code to be used in this library under the MIT license.
+
+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; }
+
+	}
+}

+ 32 - 21
Terminal.Gui/Views/TreeView/TreeView.cs

@@ -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; }

+ 29 - 11
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -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 ()

+ 171 - 2
UnitTests/Views/TreeViewTests.cs

@@ -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);
 		}