using System; using System.Collections.Generic; using System.Linq; namespace Terminal.Gui { class Branch where T : class { /// /// True if the branch is expanded to reveal child branches. /// public bool IsExpanded { get; set; } /// /// The users object that is being displayed by this branch of the tree. /// public T Model { get; private set; } /// /// The depth of the current branch. Depth of 0 indicates root level branches. /// public int Depth { get; private set; } = 0; /// /// The children of the current branch. This is null until the first call to /// to avoid enumerating the entire underlying hierarchy. /// public Dictionary> ChildBranches { get; set; } /// /// The parent or null if it is a root. /// public Branch Parent { get; private set; } private TreeView tree; /// /// Declares a new branch of in which the users object /// is presented. /// /// The UI control in which the branch resides. /// Pass null for root level branches, otherwise /// pass the parent. /// The user's object that should be displayed. public Branch (TreeView tree, Branch parentBranchIfAny, T model) { this.tree = tree; this.Model = model; if (parentBranchIfAny != null) { Depth = parentBranchIfAny.Depth + 1; Parent = parentBranchIfAny; } } /// /// Fetch the children of this branch. This method populates . /// public virtual void FetchChildren () { if (tree.TreeBuilder == null) { return; } var children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty (); this.ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val)); } /// /// Returns the width of the line including prefix and the results /// of (the line body). /// /// public virtual int GetWidth (ConsoleDriver driver) { return GetLinePrefix (driver).Sum (Rune.ColumnWidth) + Rune.ColumnWidth (GetExpandableSymbol (driver)) + (tree.AspectGetter (Model) ?? "").Length; } /// /// Renders the current on the specified line . /// /// /// /// /// 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); Attribute textColor = isSelected ? (tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal) : colorScheme.Normal; Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; // Everything on line before the expansion run and branch text Rune [] prefix = GetLinePrefix (driver).ToArray (); Rune expansion = GetExpandableSymbol (driver); string lineBody = tree.AspectGetter (Model) ?? ""; tree.Move (0, y); // if we have scrolled to the right then bits of the prefix will have dispeared off the screen int toSkip = tree.ScrollOffsetHorizontal; 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); availableWidth -= Rune.ColumnWidth (r); } } // pick color for expanded symbol if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) { Attribute color = symbolColor; if (tree.Style.ColorExpandSymbol) { if (isSelected) { color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : (tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal); } else { color = tree.ColorScheme.HotNormal; } } else { color = symbolColor; } if (tree.Style.InvertExpandSymbolColors) { color = new Attribute (color.Background, color.Foreground); } 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; } // default behaviour is for model to use the color scheme // of the tree view var modelColor = textColor; // if custom color delegate invoke it if (tree.ColorGetter != null) { var modelScheme = tree.ColorGetter (Model); // if custom color scheme is defined for this Model if (modelScheme != null) { // use it modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal; } } driver.SetAttribute (modelColor); driver.AddStr (lineBody); if (availableWidth > 0) { driver.SetAttribute (symbolColor); driver.AddStr (new string (' ', availableWidth)); } driver.SetAttribute (colorScheme.Normal); } /// /// Gets all characters to render prior to the current branches line. This includes indentation /// whitespace and any tree branches (if enabled). /// /// /// private IEnumerable 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; } } /// /// Returns all parents starting with the immediate parent and ending at the root. /// /// private IEnumerable> GetParentBranches () { var cur = Parent; while (cur != null) { yield return cur; cur = cur.Parent; } } /// /// Returns an appropriate symbol for displaying next to the string representation of /// the object to indicate whether it or /// not (or it is a leaf). /// /// /// 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; } /// /// Returns true if the current branch can be expanded according to /// the or cached children already fetched. /// /// 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 (); } /// /// Expands the current branch if possible. /// public void Expand () { if (ChildBranches == null) { FetchChildren (); } if (ChildBranches.Any ()) { IsExpanded = true; } } /// /// Marks the branch as collapsed ( false). /// public void Collapse () { IsExpanded = false; } /// /// Refreshes cached knowledge in this branch e.g. what children an object has. /// /// True to also refresh all /// branches (starting with the root). 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 (); // 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 (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; } } } } /// /// Calls on the current branch and all expanded children. /// 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; } } } /// /// Returns true if this branch has parents and it is the last node of it's parents /// branches (or last root of the tree). /// /// private bool IsLast () { if (Parent == null) { return this == tree.roots.Values.LastOrDefault (); } return Parent.ChildBranches.Values.LastOrDefault () == this; } /// /// 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. /// /// /// /// 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; } /// /// Expands the current branch and all children branches. /// internal void ExpandAll () { Expand (); if (ChildBranches != null) { foreach (var child in ChildBranches) { child.Value.ExpandAll (); } } } /// /// Collapses the current branch and all children branches (even though those branches are /// no longer visible they retain collapse/expansion state). /// internal void CollapseAll () { Collapse (); if (ChildBranches != null) { foreach (var child in ChildBranches) { child.Value.CollapseAll (); } } } } }