// 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; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using NStack; using Terminal.Gui.Trees; namespace Terminal.Gui { /// /// Interface for all non generic members of /// /// See TreeView Deep Dive for more information. /// public interface ITreeView { /// /// Contains options for changing how the tree is rendered /// TreeStyle Style { get; set; } /// /// Removes all objects from the tree and clears selection /// void ClearObjects (); /// /// Sets a flag indicating this view needs to be redisplayed because its state has changed. /// void SetNeedsDisplay (); } /// /// Convenience implementation of generic for any tree were all nodes /// implement . /// /// See TreeView Deep Dive for more information. /// public class TreeView : TreeView { /// /// Creates a new instance of the tree control with absolute positioning and initialises /// with default based builder /// public TreeView () { TreeBuilder = new TreeNodeBuilder (); AspectGetter = o => o == null ? "Null" : (o.Text ?? o?.ToString () ?? "Unamed Node"); } } /// /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined /// when expanded using a user defined /// /// See TreeView Deep Dive for more information. /// public class TreeView : View, ITreeView where T : class { private int scrollOffsetVertical; private int scrollOffsetHorizontal; /// /// Determines how sub branches of the tree are dynamically built at runtime as the user /// expands root nodes /// /// public ITreeBuilder TreeBuilder { get; set; } /// /// private variable for /// T selectedObject; /// /// Contains options for changing how the tree is rendered /// public TreeStyle Style { get; set; } = new TreeStyle (); /// /// True to allow multiple objects to be selected at once /// /// public bool MultiSelect { get; set; } = true; /// /// True makes a letter key press navigate to the next visible branch that begins with /// that letter/digit /// /// public bool AllowLetterBasedNavigation { get; set; } = true; /// /// The currently selected object in the tree. When is true this /// is the object at which the cursor is at /// public T SelectedObject { get => selectedObject; set { var oldValue = selectedObject; selectedObject = value; if (!ReferenceEquals (oldValue, value)) { OnSelectionChanged (new SelectionChangedEventArgs (this, oldValue, value)); } } } /// /// This event is raised when an object is activated e.g. by double clicking or /// pressing /// public event Action> ObjectActivated; /// /// Key which when pressed triggers . /// Defaults to Enter /// public Key ObjectActivationKey { get; set; } = Key.Enter; /// /// Mouse event to trigger . /// Defaults to double click (). /// Set to null to disable this feature. /// /// public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; /// /// Secondary selected regions of tree when is true /// private Stack> multiSelectedRegions = new Stack> (); /// /// Cached result of /// private IReadOnlyCollection> cachedLineMap; /// /// Error message to display when the control is not properly initialized at draw time /// (nodes added but no tree builder set) /// public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set"; /// /// Called when the changes /// public event EventHandler> SelectionChanged; /// /// The root objects in the tree, note that this collection is of root objects only /// public IEnumerable Objects { get => roots.Keys; } /// /// Map of root objects to the branches under them. All objects have /// a even if that branch has no children /// internal Dictionary> roots { get; set; } = new Dictionary> (); /// /// The amount of tree view that has been scrolled off the top of the screen (by the user /// scrolling down) /// /// Setting a value of less than 0 will result in a offset of 0. To see changes /// in the UI call public int ScrollOffsetVertical { get => scrollOffsetVertical; set { scrollOffsetVertical = Math.Max (0, value); } } /// /// The amount of tree view that has been scrolled to the right (horizontally) /// /// Setting a value of less than 0 will result in a offset of 0. To see changes /// in the UI call public int ScrollOffsetHorizontal { get => scrollOffsetHorizontal; set { scrollOffsetHorizontal = Math.Max (0, value); } } /// /// The current number of rows in the tree (ignoring the controls bounds) /// public int ContentHeight => BuildLineMap ().Count (); /// /// Returns the string representation of model objects hosted in the tree. Default /// implementation is to call /// /// public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; /// /// Creates a new tree view with absolute positioning. /// Use to set set root objects for the tree. /// Children will not be rendered until you set /// public TreeView () : base () { CanFocus = true; } /// /// Initialises .Creates a new tree view with absolute /// positioning. Use to set set root /// objects for the tree. /// public TreeView (ITreeBuilder builder) : this () { TreeBuilder = builder; } /// /// Adds a new root level object unless it is already a root of the tree /// /// public void AddObject (T o) { if (!roots.ContainsKey (o)) { roots.Add (o, new Branch (this, null, o)); InvalidateLineMap (); SetNeedsDisplay (); } } /// /// Removes all objects from the tree and clears /// public void ClearObjects () { SelectedObject = default (T); multiSelectedRegions.Clear (); roots = new Dictionary> (); InvalidateLineMap (); SetNeedsDisplay (); } /// /// Removes the given root object from the tree /// /// If is the currently then the /// selection is cleared /// public void Remove (T o) { if (roots.ContainsKey (o)) { roots.Remove (o); InvalidateLineMap (); SetNeedsDisplay (); if (Equals (SelectedObject, o)) { SelectedObject = default (T); } } } /// /// Adds many new root level objects. Objects that are already root objects are ignored /// /// Objects to add as new root level objects public void AddObjects (IEnumerable collection) { bool objectsAdded = false; foreach (var o in collection) { if (!roots.ContainsKey (o)) { roots.Add (o, new Branch (this, null, o)); objectsAdded = true; } } if (objectsAdded) { InvalidateLineMap (); SetNeedsDisplay (); } } /// /// Refreshes the state of the object in the tree. This will /// recompute children, string representation etc /// /// This has no effect if the object is not exposed in the tree. /// /// True to also refresh all ancestors of the objects branch /// (starting with the root). False to refresh only the passed node public void RefreshObject (T o, bool startAtTop = false) { var branch = ObjectToBranch (o); if (branch != null) { branch.Refresh (startAtTop); InvalidateLineMap (); SetNeedsDisplay (); } } /// /// Rebuilds the tree structure for all exposed objects starting with the root objects. /// Call this method when you know there are changes to the tree but don't know which /// objects have changed (otherwise use ) /// public void RebuildTree () { foreach (var branch in roots.Values) { branch.Rebuild (); } InvalidateLineMap (); SetNeedsDisplay (); } /// /// Returns the currently expanded children of the passed object. Returns an empty /// collection if the branch is not exposed or not expanded /// /// An object in the tree /// public IEnumerable GetChildren (T o) { var branch = ObjectToBranch (o); if (branch == null || !branch.IsExpanded) { return new T [0]; } return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// /// Returns the parent object of in the tree. Returns null if /// the object is not exposed in the tree /// /// An object in the tree /// public T GetParent (T o) { return ObjectToBranch (o)?.Parent?.Model; } /// public override void Redraw (Rect bounds) { if (roots == null) { return; } if (TreeBuilder == null) { Move (0, 0); Driver.AddStr (NoBuilderError); return; } var map = BuildLineMap (); for (int line = 0; line < bounds.Height; line++) { var idxToRender = ScrollOffsetVertical + line; // Is there part of the tree view to render? if (idxToRender < map.Count) { // Render the line map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, bounds.Width); } else { // Else clear the line to prevent stale symbols due to scrolling etc Move (0, line); Driver.SetAttribute (GetNormalColor ()); Driver.AddStr (new string (' ', bounds.Width)); } } } /// /// Returns the index of the object if it is currently exposed (it's /// parent(s) have been expanded). This can be used with /// and to scroll to a specific object /// /// Uses the Equals method and returns the first index at which the object is found /// or -1 if it is not found /// An object that appears in your tree and is currently exposed /// The index the object was found at or -1 if it is not currently revealed or /// not in the tree at all public int GetScrollOffsetOf (T o) { var map = BuildLineMap (); for (int i = 0; i < map.Count; i++) { if (map.ElementAt (i).Model.Equals (o)) { return i; } } //object not found return -1; } /// /// Returns the maximum width line in the tree including prefix and expansion symbols /// /// True to consider only rows currently visible (based on window /// bounds and . False to calculate the width of /// every exposed branch in the tree /// public int GetContentWidth (bool visible) { var map = BuildLineMap (); if (map.Count == 0) { return 0; } if (visible) { //Somehow we managed to scroll off the end of the control if (ScrollOffsetVertical >= map.Count) { return 0; } // If control has no height to it then there is no visible area for content if (Bounds.Height == 0) { return 0; } return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver)); } else { return map.Max (b => b.GetWidth (Driver)); } } /// /// Calculates all currently visible/expanded branches (including leafs) and outputs them /// by index from the top of the screen /// /// Index 0 of the returned array is the first item that should be visible in the /// top of the control, index 1 is the next etc. /// private IReadOnlyCollection> BuildLineMap () { if (cachedLineMap != null) { return cachedLineMap; } List> toReturn = new List> (); foreach (var root in roots.Values) { toReturn.AddRange (AddToLineMap (root)); } return cachedLineMap = new ReadOnlyCollection> (toReturn); } private IEnumerable> AddToLineMap (Branch currentBranch) { yield return currentBranch; if (currentBranch.IsExpanded) { foreach (var subBranch in currentBranch.ChildBranches.Values) { foreach (var sub in AddToLineMap (subBranch)) { yield return sub; } } } } /// public override bool ProcessKey (KeyEvent keyEvent) { if (keyEvent.Key == ObjectActivationKey) { var o = SelectedObject; if (o != null) { OnObjectActivated (new ObjectActivatedEventArgs (this, o)); PositionCursor (); return true; } } if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { var character = (char)keyEvent.KeyValue; // if it is a single character pressed without any control keys if (char.IsLetterOrDigit (character) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { // search for next branch that begins with that letter var characterAsStr = character.ToString (); AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, StringComparison.CurrentCultureIgnoreCase)); PositionCursor (); return true; } } switch (keyEvent.Key) { case Key.CursorRight: Expand (SelectedObject); break; case Key.CursorRight | Key.CtrlMask: ExpandAll (SelectedObject); break; case Key.CursorLeft: case Key.CursorLeft | Key.CtrlMask: CursorLeft (keyEvent.Key.HasFlag (Key.CtrlMask)); break; case Key.CursorUp: case Key.CursorUp | Key.ShiftMask: AdjustSelection (-1, keyEvent.Key.HasFlag (Key.ShiftMask)); break; case Key.CursorDown: case Key.CursorDown | Key.ShiftMask: AdjustSelection (1, keyEvent.Key.HasFlag (Key.ShiftMask)); break; case Key.CursorUp | Key.CtrlMask: AdjustSelectionToBranchStart (); break; case Key.CursorDown | Key.CtrlMask: AdjustSelectionToBranchEnd (); break; case Key.PageUp: case Key.PageUp | Key.ShiftMask: AdjustSelection (-Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask)); break; case Key.PageDown: case Key.PageDown | Key.ShiftMask: AdjustSelection (Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask)); break; case Key.A | Key.CtrlMask: SelectAll (); break; case Key.Home: GoToFirst (); break; case Key.End: GoToEnd (); break; default: // we don't care about this keystroke return false; } PositionCursor (); return true; } /// /// Raises the event /// /// protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) { ObjectActivated?.Invoke (e); } /// public override bool MouseEvent (MouseEvent me) { // If it is not an event we care about if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) && !me.Flags.HasFlag (MouseFlags.WheeledDown) && !me.Flags.HasFlag (MouseFlags.WheeledUp) && !me.Flags.HasFlag (MouseFlags.WheeledRight) && !me.Flags.HasFlag (MouseFlags.WheeledLeft)) { // do nothing return false; } if (!HasFocus && CanFocus) { SetFocus (); } if (me.Flags == MouseFlags.WheeledDown) { ScrollOffsetVertical++; SetNeedsDisplay (); return true; } else if (me.Flags == MouseFlags.WheeledUp) { ScrollOffsetVertical--; SetNeedsDisplay (); return true; } if (me.Flags == MouseFlags.WheeledRight) { ScrollOffsetHorizontal++; SetNeedsDisplay (); return true; } else if (me.Flags == MouseFlags.WheeledLeft) { ScrollOffsetHorizontal--; SetNeedsDisplay (); return true; } if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { // The line they clicked on a branch var clickedBranch = HitTest (me.Y); if (clickedBranch == null) { return false; } bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X); // If we are already selected (double click) if (Equals (SelectedObject, clickedBranch.Model)) { isExpandToggleAttempt = true; } // if they clicked on the +/- expansion symbol if (isExpandToggleAttempt) { if (clickedBranch.IsExpanded) { clickedBranch.Collapse (); InvalidateLineMap (); } else if (clickedBranch.CanExpand ()) { clickedBranch.Expand (); InvalidateLineMap (); } else { SelectedObject = clickedBranch.Model; // It is a leaf node multiSelectedRegions.Clear (); } } else { // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt SelectedObject = clickedBranch.Model; multiSelectedRegions.Clear (); } SetNeedsDisplay (); return true; } // If it is activation via mouse (e.g. double click) if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) { // The line they clicked on a branch var clickedBranch = HitTest (me.Y); if (clickedBranch == null) { return false; } // Double click changes the selection to the clicked node as well as triggering // activation otherwise it feels wierd SelectedObject = clickedBranch.Model; SetNeedsDisplay (); // trigger activation event OnObjectActivated (new ObjectActivatedEventArgs (this, clickedBranch.Model)); // mouse event is handled. return true; } return false; } /// /// Returns the branch at the given client /// coordinate e.g. following a click event /// /// Client Y position in the controls bounds /// The clicked branch or null if outside of tree region private Branch HitTest (int y) { var map = BuildLineMap (); var idx = y + ScrollOffsetVertical; // click is outside any visible nodes if (idx < 0 || idx >= map.Count) { return null; } // The line they clicked on return map.ElementAt (idx); } /// /// Positions the cursor at the start of the selected objects line (if visible) /// public override void PositionCursor () { if (CanFocus && HasFocus && Visible && SelectedObject != null) { var map = BuildLineMap (); var idx = map.IndexOf(b => b.Model.Equals (SelectedObject)); // if currently selected line is visible if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) { Move (0, idx - ScrollOffsetVertical); } else { base.PositionCursor (); } } else { base.PositionCursor (); } } /// /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is /// to collapse the current tree node if possible otherwise changes selection to current /// branches parent /// protected virtual void CursorLeft (bool ctrl) { if (IsExpanded (SelectedObject)) { if (ctrl) { CollapseAll (SelectedObject); } else { Collapse (SelectedObject); } } else { var parent = GetParent (SelectedObject); if (parent != null) { SelectedObject = parent; AdjustSelection (0); SetNeedsDisplay (); } } } /// /// Changes the to the first root object and resets /// the to 0 /// public void GoToFirst () { ScrollOffsetVertical = 0; SelectedObject = roots.Keys.FirstOrDefault (); SetNeedsDisplay (); } /// /// Changes the to the last object in the tree and scrolls so /// that it is visible /// public void GoToEnd () { var map = BuildLineMap (); ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1); SelectedObject = map.Last ().Model; SetNeedsDisplay (); } /// /// Changes the to and scrolls to ensure /// it is visible. Has no effect if is not exposed in the tree (e.g. /// its parents are collapsed) /// /// public void GoTo (T toSelect) { if (ObjectToBranch (toSelect) == null) { return; } SelectedObject = toSelect; EnsureVisible (toSelect); SetNeedsDisplay (); } /// /// The number of screen lines to move the currently selected object by. Supports negative /// . Each branch occupies 1 line on screen /// /// If nothing is currently selected or the selected object is no longer in the tree /// then the first object in the tree is selected instead /// Positive to move the selection down the screen, negative to move it up /// True to expand the selection (assuming /// is enabled). False to replace public void AdjustSelection (int offset, bool expandSelection = false) { // if it is not a shift click or we don't allow multi select if (!expandSelection || !MultiSelect) { multiSelectedRegions.Clear (); } if (SelectedObject == null) { SelectedObject = roots.Keys.FirstOrDefault (); } else { var map = BuildLineMap (); var idx = map.IndexOf(b => b.Model.Equals (SelectedObject)); if (idx == -1) { // The current selection has disapeared! SelectedObject = roots.Keys.FirstOrDefault (); } else { var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1); var newBranch = map.ElementAt(newIdx); // If it is a multi selection if (expandSelection && MultiSelect) { if (multiSelectedRegions.Any ()) { // expand the existing head selection var head = multiSelectedRegions.Pop (); multiSelectedRegions.Push (new TreeSelection (head.Origin, newIdx, map)); } else { // or start a new multi selection region multiSelectedRegions.Push (new TreeSelection (map.ElementAt(idx), newIdx, map)); } } SelectedObject = newBranch.Model; EnsureVisible (SelectedObject); } } SetNeedsDisplay (); } /// /// Moves the selection to the first child in the currently selected level /// public void AdjustSelectionToBranchStart () { var o = SelectedObject; if (o == null) { return; } var map = BuildLineMap (); int currentIdx = map.IndexOf(b => Equals (b.Model, o)); if (currentIdx == -1) { return; } var currentBranch = map.ElementAt(currentIdx); var next = currentBranch; for (; currentIdx >= 0; currentIdx--) { //if it is the beginning of the current depth of branch if (currentBranch.Depth != next.Depth) { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); SetNeedsDisplay (); return; } // look at next branch up for consideration currentBranch = next; next = map.ElementAt(currentIdx); } // We ran all the way to top of tree GoToFirst (); } /// /// Moves the selection to the last child in the currently selected level /// public void AdjustSelectionToBranchEnd () { var o = SelectedObject; if (o == null) { return; } var map = BuildLineMap (); int currentIdx = map.IndexOf(b => Equals (b.Model, o)); if (currentIdx == -1) { return; } var currentBranch = map.ElementAt(currentIdx); var next = currentBranch; for (; currentIdx < map.Count; currentIdx++) { //if it is the end of the current depth of branch if (currentBranch.Depth != next.Depth) { SelectedObject = currentBranch.Model; EnsureVisible (currentBranch.Model); SetNeedsDisplay (); return; } // look at next branch for consideration currentBranch = next; next = map.ElementAt(currentIdx); } GoToEnd (); } /// /// Sets the selection to the next branch that matches the /// /// private void AdjustSelectionToNext (Func, bool> predicate) { var map = BuildLineMap (); // empty map means we can't select anything anyway if (map.Count == 0) { return; } // Start searching from the first element in the map var idxStart = 0; // or the current selected branch if (SelectedObject != null) { idxStart = map.IndexOf(b => Equals (b.Model, SelectedObject)); } // if currently selected object mysteriously vanished, search from beginning if (idxStart == -1) { idxStart = 0; } // loop around all indexes and back to first index for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) { if (predicate (map.ElementAt(idxCur))) { SelectedObject = map.ElementAt(idxCur).Model; EnsureVisible (map.ElementAt(idxCur).Model); SetNeedsDisplay (); return; } } } /// /// Adjusts the to ensure the given /// is visible. Has no effect if already visible /// public void EnsureVisible (T model) { var map = BuildLineMap (); var idx = map.IndexOf(b => Equals (b.Model, model)); if (idx == -1) { return; } /*this -1 allows for possible horizontal scroll bar in the last row of the control*/ int leaveSpace = Style.LeaveLastRow ? 1 : 0; if (idx < ScrollOffsetVertical) { //if user has scrolled up too far to see their selection ScrollOffsetVertical = idx; } else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) { //if user has scrolled off bottom of visible tree ScrollOffsetVertical = Math.Max (0, (idx + 1) - (Bounds.Height - leaveSpace)); } } /// /// Expands the supplied object if it is contained in the tree (either as a root object or /// as an exposed branch object) /// /// The object to expand public void Expand (T toExpand) { if (toExpand == null) { return; } ObjectToBranch (toExpand)?.Expand (); InvalidateLineMap (); SetNeedsDisplay (); } /// /// Expands the supplied object and all child objects /// /// The object to expand public void ExpandAll (T toExpand) { if (toExpand == null) { return; } ObjectToBranch (toExpand)?.ExpandAll (); InvalidateLineMap (); SetNeedsDisplay (); } /// /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this /// may take a while (e.g. for file system) /// public void ExpandAll () { foreach (var item in roots) { item.Value.ExpandAll (); } InvalidateLineMap (); SetNeedsDisplay (); } /// /// Returns true if the given object is exposed in the tree and can be /// expanded otherwise false /// /// /// public bool CanExpand (T o) { return ObjectToBranch (o)?.CanExpand () ?? false; } /// /// Returns true if the given object is exposed in the tree and /// expanded otherwise false /// /// /// public bool IsExpanded (T o) { return ObjectToBranch (o)?.IsExpanded ?? false; } /// /// Collapses the supplied object if it is currently expanded /// /// The object to collapse public void Collapse (T toCollapse) { CollapseImpl (toCollapse, false); } /// /// Collapses the supplied object if it is currently expanded. Also collapses all children /// branches (this will only become apparent when/if the user expands it again) /// /// The object to collapse public void CollapseAll (T toCollapse) { CollapseImpl (toCollapse, true); } /// /// Collapses all root nodes in the tree /// public void CollapseAll () { foreach (var item in roots) { item.Value.Collapse (); } InvalidateLineMap (); SetNeedsDisplay (); } /// /// Implementation of and . Performs /// operation and updates selection if disapeared /// /// /// protected void CollapseImpl (T toCollapse, bool all) { if (toCollapse == null) { return; } var branch = ObjectToBranch (toCollapse); // Nothing to collapse if (branch == null) { return; } if (all) { branch.CollapseAll (); } else { branch.Collapse (); } if (SelectedObject != null && ObjectToBranch (SelectedObject) == null) { // If the old selection suddenly became invalid then clear it SelectedObject = null; } InvalidateLineMap (); SetNeedsDisplay (); } /// /// Clears any cached results of /// protected void InvalidateLineMap () { cachedLineMap = null; } /// /// Returns the corresponding in the tree for /// . This will not work for objects hidden /// by their parent being collapsed /// /// /// The branch for or null if it is not currently /// exposed in the tree private Branch ObjectToBranch (T toFind) { return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); } /// /// Returns true if the is either the /// or part of a /// /// /// public bool IsSelected (T model) { return Equals (SelectedObject, model) || (MultiSelect && multiSelectedRegions.Any (s => s.Contains (model))); } /// /// Returns (if not null) and all multi selected objects if /// is true /// /// public IEnumerable GetAllSelectedObjects () { var map = BuildLineMap (); // To determine multi selected objects, start with the line map, that avoids yielding // hidden nodes that were selected then the parent collapsed e.g. programmatically or // with mouse click if (MultiSelect) { foreach (var m in map.Select (b => b.Model).Where (IsSelected)) { yield return m; } } else { if (SelectedObject != null) { yield return SelectedObject; } } } /// /// Selects all objects in the tree when is enabled otherwise /// does nothing /// public void SelectAll () { if (!MultiSelect) { return; } multiSelectedRegions.Clear (); var map = BuildLineMap (); if (map.Count == 0) { return; } multiSelectedRegions.Push (new TreeSelection (map.ElementAt(0), map.Count, map)); SetNeedsDisplay (); OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } /// /// Raises the SelectionChanged event /// /// protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) { SelectionChanged?.Invoke (this, e); } } class TreeSelection where T : class { public Branch Origin { get; } private HashSet included = new HashSet (); /// /// Creates a new selection between two branches in the tree /// /// /// /// public TreeSelection (Branch from, int toIndex, IReadOnlyCollection> map) { Origin = from; included.Add (Origin.Model); var oldIdx = map.IndexOf(from); var lowIndex = Math.Min (oldIdx, toIndex); var highIndex = Math.Max (oldIdx, toIndex); // Select everything between the old and new indexes foreach (var alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex)) { included.Add (alsoInclude.Model); } } public bool Contains (T model) { return included.Contains (model); } } }