diff --git a/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs b/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs
index 5865be9a35..14ab2f648a 100644
--- a/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs
+++ b/Terminal.Gui/Views/TreeView/AspectGetterDelegate.cs
@@ -1,4 +1,5 @@
-namespace Terminal.Gui;
+#nullable enable
+namespace Terminal.Gui;
/// Delegates of this type are used to fetch string representations of user's model objects
/// The object that is being rendered
diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs
index e7a5eb4ca4..60edfd636d 100644
--- a/Terminal.Gui/Views/TreeView/Branch.cs
+++ b/Terminal.Gui/Views/TreeView/Branch.cs
@@ -1,8 +1,9 @@
-namespace Terminal.Gui;
+#nullable enable
+namespace Terminal.Gui;
internal class Branch where T : class
{
- private readonly TreeView tree;
+ private readonly TreeView? _tree;
///
/// Declares a new branch of in which the users object is
@@ -11,9 +12,9 @@ internal class Branch where T : class
/// 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)
+ public Branch (TreeView? tree, Branch? parentBranchIfAny, T? model)
{
- this.tree = tree;
+ _tree = tree;
Model = model;
if (parentBranchIfAny is { })
@@ -27,7 +28,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model)
/// 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; }
+ public Dictionary>? ChildBranches { get; set; }
/// The depth of the current branch. Depth of 0 indicates root level branches.
public int Depth { get; }
@@ -36,10 +37,10 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model)
public bool IsExpanded { get; set; }
/// The users object that is being displayed by this branch of the tree.
- public T Model { get; private set; }
+ public T? Model { get; private set; }
/// The parent or null if it is a root.
- public Branch Parent { get; }
+ public Branch? Parent { get; }
///
/// Returns true if the current branch can be expanded according to the or cached
@@ -52,9 +53,9 @@ public bool CanExpand ()
if (ChildBranches is null)
{
//if there is a rapid method for determining whether there are children
- if (tree.TreeBuilder.SupportsCanExpand)
+ if (_tree is { TreeBuilder.SupportsCanExpand: true })
{
- return tree.TreeBuilder.CanExpand (Model);
+ return Model is { } && _tree.TreeBuilder.CanExpand (Model);
}
//there is no way of knowing whether we can expand without fetching the children
@@ -62,7 +63,7 @@ public bool CanExpand ()
}
//we fetched or already know the children, so return whether we have any
- return ChildBranches.Any ();
+ return ChildBranches is { } && ChildBranches.Any ();
}
/// Marks the branch as collapsed ( false).
@@ -80,21 +81,21 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
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 ? 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) ?? "";
+ string lineBody = _tree.AspectGetter?.Invoke (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 +113,20 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
}
// pick color for expanded symbol
- if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors)
+ if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors)
{
Attribute color = symbolColor;
- 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 ? colorScheme.HotNormal :
+ _tree.HasFocus ? _tree.GetHotFocusColor() : _tree.GetHotNormalColor();
}
else
{
- color = tree.ColorScheme.HotNormal;
+ color = _tree.GetHotNormalColor();
}
}
else
@@ -133,7 +134,7 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
color = symbolColor;
}
- if (tree.Style.InvertExpandSymbolColors)
+ if (_tree.Style.InvertExpandSymbolColors)
{
color = new Attribute (color.Background, color.Foreground);
}
@@ -194,9 +195,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
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 { })
@@ -230,19 +231,19 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y,
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);
+ driver?.SetAttribute ((Attribute)cell.Attribute!);
+ driver?.AddRune (cell.Rune);
}
}
@@ -257,7 +258,7 @@ public void Expand ()
FetchChildren ();
}
- if (ChildBranches.Any ())
+ if (ChildBranches is { } && ChildBranches.Any ())
{
IsExpanded = true;
}
@@ -266,23 +267,23 @@ public void Expand ()
/// Fetch the children of this branch. This method populates .
public virtual void FetchChildren ()
{
- if (tree.TreeBuilder is null)
+ if (_tree?.TreeBuilder is null)
{
return;
}
IEnumerable children;
- if (Depth >= tree.MaxDepth)
+ if (Depth >= _tree.MaxDepth)
{
children = Enumerable.Empty ();
}
else
{
- children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty ();
+ children = _tree.TreeBuilder.GetChildren (Model!) ?? Enumerable.Empty ();
}
- ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val));
+ ChildBranches = children.ToDictionary (k => k, val => new Branch (_tree, this, val));
}
///
@@ -293,16 +294,16 @@ public virtual void FetchChildren ()
///
public Rune GetExpandableSymbol (IConsoleDriver driver)
{
- Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
+ Rune leafSymbol = _tree is { Style.ShowBranchLines: true } ? Glyphs.HLine : (Rune)' ';
if (IsExpanded)
{
- return tree.Style.CollapseableSymbol ?? leafSymbol;
+ return _tree!.Style.CollapsableSymbol ?? leafSymbol;
}
if (CanExpand ())
{
- return tree.Style.ExpandableSymbol ?? leafSymbol;
+ return _tree!.Style.ExpandableSymbol ?? leafSymbol;
}
return leafSymbol;
@@ -315,8 +316,13 @@ public Rune GetExpandableSymbol (IConsoleDriver driver)
///
public virtual int GetWidth (IConsoleDriver driver)
{
- return
- GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length;
+ if (_tree is { })
+ {
+ return
+ GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (_tree.AspectGetter?.Invoke (Model!) ?? "").Length;
+ }
+
+ return 0;
}
/// Refreshes cached knowledge in this branch e.g. what children an object has.
@@ -340,7 +346,7 @@ public void Refresh (bool startAtTop)
// we already knew about some children so preserve the state of the old children
// first gather the new Children
- IEnumerable newChildren = tree.TreeBuilder?.GetChildren (Model) ?? Enumerable.Empty ();
+ IEnumerable newChildren = _tree?.TreeBuilder?.GetChildren (Model!) ?? Enumerable.Empty ();
// Children who no longer appear need to go
foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ())
@@ -348,9 +354,9 @@ public void Refresh (bool startAtTop)
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))
{
- tree.SelectedObject = Model;
+ _tree.SelectedObject = Model;
}
}
@@ -360,7 +366,7 @@ public void Refresh (bool startAtTop)
// 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));
+ ChildBranches.Add (newChild, new Branch (_tree, this, newChild));
}
else
{
@@ -411,7 +417,7 @@ internal void ExpandAll ()
internal IEnumerable GetLinePrefix (IConsoleDriver driver)
{
// If not showing line branches or this is a root object.
- if (!tree.Style.ShowBranchLines)
+ if (_tree is { Style.ShowBranchLines: false })
{
for (var i = 0; i < Depth; i++)
{
@@ -462,13 +468,13 @@ internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x)
}
// if we could theoretically expand
- if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?))
+ if (!IsExpanded && _tree?.Style.ExpandableSymbol != default (Rune?))
{
return x == GetLinePrefix (driver).Count ();
}
// if we could theoretically collapse
- if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?))
+ if (IsExpanded && _tree!.Style.CollapsableSymbol != default (Rune?))
{
return x == GetLinePrefix (driver).Count ();
}
@@ -504,7 +510,7 @@ internal void Rebuild ()
///
private IEnumerable> GetParentBranches ()
{
- Branch cur = Parent;
+ Branch? cur = Parent;
while (cur is { })
{
@@ -523,10 +529,10 @@ private bool IsLast ()
{
if (Parent is null)
{
- return this == tree.roots.Values.LastOrDefault ();
+ return this == _tree?.Roots?.Values.LastOrDefault ();
}
- return Parent.ChildBranches.Values.LastOrDefault () == this;
+ return Parent.ChildBranches?.Values.LastOrDefault () == this;
}
private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; }
diff --git a/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs b/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs
index f3edef285a..358b72054d 100644
--- a/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs
+++ b/Terminal.Gui/Views/TreeView/DelegateTreeBuilder.cs
@@ -3,8 +3,8 @@
/// Implementation of that uses user defined functions
public class DelegateTreeBuilder : TreeBuilder
{
- private readonly Func canExpand;
- private readonly Func> childGetter;
+ private readonly Func _canExpand;
+ private readonly Func> _childGetter;
///
/// Constructs an implementation of that calls the user defined method
@@ -12,7 +12,7 @@ public class DelegateTreeBuilder : TreeBuilder
///
///
///
- public DelegateTreeBuilder (Func> childGetter) : base (false) { this.childGetter = childGetter; }
+ public DelegateTreeBuilder (Func> childGetter) : base (false) { this._childGetter = childGetter; }
///
/// Constructs an implementation of that calls the user defined method
@@ -23,17 +23,17 @@ public class DelegateTreeBuilder : TreeBuilder
///
public DelegateTreeBuilder (Func> childGetter, Func canExpand) : base (true)
{
- this.childGetter = childGetter;
- this.canExpand = canExpand;
+ this._childGetter = childGetter;
+ this._canExpand = canExpand;
}
/// Returns whether a node can be expanded based on the delegate passed during construction
///
///
- public override bool CanExpand (T toExpand) { return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); }
+ public override bool CanExpand (T toExpand) { return _canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); }
/// Returns children using the delegate method passed during construction
///
///
- public override IEnumerable GetChildren (T forObject) { return childGetter.Invoke (forObject); }
+ public override IEnumerable GetChildren (T forObject) { return _childGetter.Invoke (forObject); }
}
diff --git a/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs b/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs
index 6cd03747d1..ca947a1fd7 100644
--- a/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs
+++ b/Terminal.Gui/Views/TreeView/DrawTreeViewLineEventArgs.cs
@@ -1,4 +1,5 @@
-// This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls
+#nullable enable
+// 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.
@@ -25,17 +26,17 @@ public class DrawTreeViewLineEventArgs where T : class
public int IndexOfModelText { get; init; }
/// The object at this line in the tree
- public T Model { get; init; }
+ public T? Model { get; init; }
///
/// The rune and color of each symbol that will be rendered. Note that only is
/// respected. You can modify these to change what is rendered.
///
/// Changing the length of this collection may result in corrupt rendering
- public List Cells { get; init; }
+ public List? Cells { get; init; }
/// The that is performing the rendering.
- public TreeView Tree { get; init; }
+ public TreeView? Tree { get; init; }
/// The line within tree view bounds that is being rendered
public int Y { get; init; }
diff --git a/Terminal.Gui/Views/TreeView/ITreeView.cs b/Terminal.Gui/Views/TreeView/ITreeView.cs
new file mode 100644
index 0000000000..91e169e5ff
--- /dev/null
+++ b/Terminal.Gui/Views/TreeView/ITreeView.cs
@@ -0,0 +1,17 @@
+namespace Terminal.Gui;
+
+///
+/// Interface for all non-generic members of .
+/// See TreeView Deep Dive for more information.
+///
+public interface ITreeView
+{
+ /// Removes all objects from the tree and clears selection.
+ void ClearObjects ();
+
+ /// Sets a flag indicating this view needs to be drawn because its state has changed.
+ void SetNeedsDraw ();
+
+ /// Contains options for changing how the tree is rendered.
+ TreeStyle Style { get; set; }
+}
diff --git a/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs b/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs
index 5253f3d404..622ebd7b28 100644
--- a/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs
+++ b/Terminal.Gui/Views/TreeView/SelectionChangedEventArgs.cs
@@ -1,13 +1,14 @@
-namespace Terminal.Gui;
+#nullable enable
+namespace Terminal.Gui;
/// Event arguments describing a change in selected object in a tree view
-public class SelectionChangedEventArgs : EventArgs where T : class
+public class SelectionChangedEventArgs : EventArgs where T : class?
{
/// Creates a new instance of event args describing a change of selection in
///
///
///
- public SelectionChangedEventArgs (TreeView tree, T oldValue, T newValue)
+ public SelectionChangedEventArgs (TreeView? tree, T? oldValue, T? newValue)
{
Tree = tree;
OldValue = oldValue;
@@ -15,11 +16,11 @@ public SelectionChangedEventArgs (TreeView tree, T oldValue, T newValue)
}
/// The newly selected value in the (can be null)
- public T NewValue { get; }
+ public T? NewValue { get; }
/// The previously selected value (can be null)
- public T OldValue { get; }
+ public T? OldValue { get; }
/// The view in which the change occurred
- public TreeView Tree { get; }
+ public TreeView? Tree { get; }
}
diff --git a/Terminal.Gui/Views/TreeView/TreeSelection.cs b/Terminal.Gui/Views/TreeView/TreeSelection.cs
new file mode 100644
index 0000000000..2d918f0785
--- /dev/null
+++ b/Terminal.Gui/Views/TreeView/TreeSelection.cs
@@ -0,0 +1,46 @@
+#nullable enable
+namespace Terminal.Gui;
+
+internal class TreeSelection where T : class
+{
+ /// Creates a new selection between two branches in the tree
+ ///
+ ///
+ ///
+ public TreeSelection (Branch? from, int toIndex, IReadOnlyCollection>? map)
+ {
+ Origin = from;
+
+ if (Origin?.Model is null)
+ {
+ return;
+ }
+
+ if (map is null)
+ {
+ return;
+ }
+
+ _included.Add (Origin.Model);
+
+ int oldIdx = map.IndexOf (from);
+
+ int lowIndex = Math.Min (oldIdx, toIndex);
+ int highIndex = Math.Max (oldIdx, toIndex);
+
+ // Select everything between the old and new indexes
+
+ foreach (Branch alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex))
+ {
+ if (alsoInclude.Model is { })
+ {
+ _included.Add (alsoInclude.Model);
+ }
+ }
+ }
+
+ private readonly HashSet _included = new ();
+ public bool Contains (T? model) { return _included.Contains (model); }
+
+ public Branch? Origin { get; }
+}
diff --git a/Terminal.Gui/Views/TreeView/TreeStyle.cs b/Terminal.Gui/Views/TreeView/TreeStyle.cs
index 0b560f2a9e..1c7a19e947 100644
--- a/Terminal.Gui/Views/TreeView/TreeStyle.cs
+++ b/Terminal.Gui/Views/TreeView/TreeStyle.cs
@@ -7,7 +7,7 @@ public class TreeStyle
/// Symbol to use for branch nodes that can be collapsed (are currently expanded). Defaults to '-'. Set to null to
/// hide.
/// | |
- public Rune? CollapseableSymbol { get; set; } = Glyphs.Collapse;
+ public Rune? CollapsableSymbol { get; set; } = Glyphs.Collapse;
/// Set to to highlight expand/collapse symbols in hot key color.
public bool ColorExpandSymbol { get; set; }
diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs
index 728a411bdb..cb7f32fc32 100644
--- a/Terminal.Gui/Views/TreeView/TreeView.cs
+++ b/Terminal.Gui/Views/TreeView/TreeView.cs
@@ -1,28 +1,12 @@
+#nullable enable
// 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.ObjectModel;
-using static Terminal.Gui.SpinnerStyle;
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 drawn because its state has changed.
- void SetNeedsDraw ();
-}
-
///
/// Convenience implementation of generic for any tree were all nodes implement
/// . See TreeView Deep Dive for more information.
@@ -41,7 +25,6 @@ public TreeView ()
AspectGetter = o => o is null ? "Null" : o.Text ?? o?.ToString () ?? "Unnamed Node";
}
-
bool IDesignable.EnableForDesign ()
{
var root1 = new TreeNode ("Root1");
@@ -56,6 +39,7 @@ bool IDesignable.EnableForDesign ()
AddObject (root2);
ExpandAll ();
+
return true;
}
}
@@ -73,25 +57,6 @@ public class TreeView : View, ITreeView where T : class
///
public static string NoBuilderError = "ERROR: TreeBuilder Not Set";
- ///
- /// Interface for filtering which lines of the tree are displayed e.g. to provide text searching. Defaults to
- /// (no filtering).
- ///
- public ITreeViewFilter Filter = null;
-
- /// Secondary selected regions of tree when is true.
- private readonly Stack> multiSelectedRegions = new ();
-
- /// Cached result of
- private IReadOnlyCollection> cachedLineMap;
-
- private KeyCode objectActivationKey = KeyCode.Enter;
- private int scrollOffsetHorizontal;
- private int scrollOffsetVertical;
-
- /// private variable for
- private T selectedObject;
-
///
/// Creates a new tree view with absolute positioning. Use to set
/// root objects for the tree. Children will not be rendered until you set .
@@ -324,125 +289,26 @@ public TreeView ()
/// Initialises .Creates a new tree view with absolute positioning. Use
/// to set root objects for the tree.
///
- public TreeView (ITreeBuilder builder) : this () { TreeBuilder = builder; }
-
- /// True makes a letter key press navigate to the next visible branch that begins with that letter/digit.
- ///
- public bool AllowLetterBasedNavigation { get; set; } = true;
-
- ///
- /// Returns the string representation of model objects hosted in the tree. Default implementation is to call
- /// .
- ///
- ///
- public AspectGetterDelegate AspectGetter { get; set; } = o => o.ToString () ?? "";
-
- ///
- /// Delegate for multi-colored tree views. Return the to use for each passed object or
- /// null to use the default.
- ///
- public Func ColorGetter { get; set; }
-
- /// The current number of rows in the tree (ignoring the controls bounds).
- public int ContentHeight => BuildLineMap ().Count ();
-
- ///
- /// Gets the that searches the collection as the user
- /// types.
- ///
- public CollectionNavigator KeystrokeNavigator { get; } = new ();
-
- /// Maximum number of nodes that can be expanded in any given branch.
- public int MaxDepth { get; set; } = 100;
-
- /// True to allow multiple objects to be selected at once.
- ///
- public bool MultiSelect { get; set; } = true;
+ public TreeView (ITreeBuilder? builder) : this () { TreeBuilder = builder; }
///
- /// Mouse event to trigger . Defaults to double click (
- /// ). Set to null to disable this feature.
+ /// Interface for filtering which lines of the tree are displayed e.g. to provide text searching. Defaults to
+ /// (no filtering).
///
- ///
- public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
-
- // TODO: Update to use Key instead of KeyCode
- /// Key which when pressed triggers . Defaults to Enter.
- public KeyCode ObjectActivationKey
- {
- get => objectActivationKey;
- set
- {
- if (objectActivationKey != value)
- {
- KeyBindings.ReplaceKey (ObjectActivationKey, value);
- objectActivationKey = value;
- SetNeedsDraw ();
- }
- }
- }
-
- /// The root objects in the tree, note that this collection is of root objects only.
- public IEnumerable Objects => roots.Keys;
-
- /// 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);
- SetNeedsDraw ();
- }
- }
-
- /// 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 an offset of 0. To see changes in the UI call
- /// .
- ///
- public int ScrollOffsetVertical
- {
- get => scrollOffsetVertical;
- set
- {
- scrollOffsetVertical = Math.Max (0, value);
- SetNeedsDraw ();
- }
- }
+ public ITreeViewFilter? Filter { get; set; } = null;
- ///
- /// 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
- {
- T oldValue = selectedObject;
- selectedObject = value;
+ /// Secondary selected regions of tree when is true.
+ private readonly Stack> _multiSelectedRegions = new ();
- if (!ReferenceEquals (oldValue, value))
- {
- OnSelectionChanged (new SelectionChangedEventArgs (this, oldValue, value));
- }
- }
- }
+ /// Cached result of
+ private IReadOnlyCollection>? _cachedLineMap;
- /// Determines how sub-branches of the tree are dynamically built at runtime as the user expands root nodes.
- ///
- public ITreeBuilder TreeBuilder { get; set; }
+ private KeyCode _objectActivationKey = KeyCode.Enter;
+ private int _scrollOffsetHorizontal;
+ private int _scrollOffsetVertical;
- ///
- /// 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 ();
+ /// private variable for
+ private T? _selectedObject;
/// Contains options for changing how the tree is rendered.
public TreeStyle Style { get; set; } = new ();
@@ -451,8 +317,8 @@ public T SelectedObject
public void ClearObjects ()
{
SelectedObject = default (T);
- multiSelectedRegions.Clear ();
- roots = new Dictionary> ();
+ _multiSelectedRegions.Clear ();
+ Roots = new ();
InvalidateLineMap ();
SetNeedsDraw ();
}
@@ -470,15 +336,17 @@ public void ClearObjects ()
return true;
}
- T o = SelectedObject;
+ T? o = SelectedObject;
if (o is { })
{
// TODO: Should this be cancelable?
ObjectActivatedEventArgs e = new (this, o);
OnObjectActivated (e);
+
return true;
}
+
return false;
}
@@ -486,9 +354,9 @@ public void ClearObjects ()
///
public void AddObject (T o)
{
- if (!roots.ContainsKey (o))
+ if (Roots is { } && !Roots.ContainsKey (o))
{
- roots.Add (o, new Branch (this, null, o));
+ Roots.Add (o, new (this, null, o));
InvalidateLineMap ();
SetNeedsDraw ();
}
@@ -503,9 +371,9 @@ public void AddObjects (IEnumerable collection)
foreach (T o in collection)
{
- if (!roots.ContainsKey (o))
+ if (Roots is { } && !Roots.ContainsKey (o))
{
- roots.Add (o, new Branch (this, null, o));
+ Roots.Add (o, new (this, null, o));
objectsAdded = true;
}
}
@@ -535,47 +403,50 @@ 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 ();
+ _multiSelectedRegions.Clear ();
}
if (SelectedObject is null)
{
- SelectedObject = roots.Keys.FirstOrDefault ();
+ SelectedObject = Roots?.Keys.FirstOrDefault ();
}
else
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
- int idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
+ int idx = map.IndexOf (b => b.Model!.Equals (SelectedObject));
if (idx == -1)
{
// The current selection has disappeared!
- SelectedObject = roots.Keys.FirstOrDefault ();
+ SelectedObject = Roots?.Keys.FirstOrDefault ();
}
else
{
- int newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
+ if (map is { })
+ {
+ int newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1);
- Branch newBranch = map.ElementAt (newIdx);
+ Branch newBranch = map.ElementAt (newIdx);
- // If it is a multi selection
- if (expandSelection && MultiSelect)
- {
- if (multiSelectedRegions.Any ())
- {
- // expand the existing head selection
- TreeSelection head = multiSelectedRegions.Pop ();
- multiSelectedRegions.Push (new TreeSelection (head.Origin, newIdx, map));
- }
- else
+ // If it is a multi selection
+ if (expandSelection && MultiSelect)
{
- // or start a new multi selection region
- multiSelectedRegions.Push (new TreeSelection (map.ElementAt (idx), newIdx, map));
+ if (_multiSelectedRegions.Any ())
+ {
+ // expand the existing head selection
+ TreeSelection head = _multiSelectedRegions.Pop ();
+ _multiSelectedRegions.Push (new (head.Origin, newIdx, map));
+ }
+ else
+ {
+ // or start a new multi selection region
+ _multiSelectedRegions.Push (new (map.ElementAt (idx), newIdx, map));
+ }
}
- }
- SelectedObject = newBranch.Model;
+ SelectedObject = newBranch.Model;
+ }
EnsureVisible (SelectedObject);
}
@@ -587,14 +458,18 @@ public void AdjustSelection (int offset, bool expandSelection = false)
/// Moves the selection to the last child in the currently selected level.
public void AdjustSelectionToBranchEnd ()
{
- T o = SelectedObject;
+ T? o = SelectedObject;
if (o is null)
{
return;
}
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
+ if (map is null)
+ {
+ return;
+ }
int currentIdx = map.IndexOf (b => Equals (b.Model, o));
@@ -629,14 +504,19 @@ public void AdjustSelectionToBranchEnd ()
/// Moves the selection to the first child in the currently selected level.
public void AdjustSelectionToBranchStart ()
{
- T o = SelectedObject;
+ T? o = SelectedObject;
if (o is null)
{
return;
}
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
+
+ if (map is null)
+ {
+ return;
+ }
int currentIdx = map.IndexOf (b => Equals (b.Model, o));
@@ -682,35 +562,51 @@ public void AdjustSelectionToNextItemBeginningWith (
{
// search for next branch that begins with that letter
var characterAsStr = character.ToString ();
- AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity));
+ AdjustSelectionToNext (b => AspectGetter is { } && AspectGetter (b.Model!).StartsWith (characterAsStr, caseSensitivity));
}
+ /// True makes a letter key press navigate to the next visible branch that begins with that letter/digit.
+ ///
+ public bool AllowLetterBasedNavigation { get; set; } = true;
+
+ ///
+ /// Returns the string representation of model objects hosted in the tree. Default implementation is to call
+ /// .
+ ///
+ ///
+ public AspectGetterDelegate? AspectGetter { get; set; } = o => o.ToString () ?? "";
+
///
/// 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; }
+ public bool CanExpand (T? o) { return ObjectToBranch (o)?.CanExpand () ?? false; }
/// Collapses the
- public void Collapse () { Collapse (selectedObject); }
+ public void Collapse () { Collapse (_selectedObject); }
/// Collapses the supplied object if it is currently expanded .
/// The object to collapse.
- public void Collapse (T toCollapse) { CollapseImpl (toCollapse, false); }
+ 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); }
+ public void CollapseAll (T? toCollapse) { CollapseImpl (toCollapse, true); }
/// Collapses all root nodes in the tree.
public void CollapseAll ()
{
- foreach (KeyValuePair> item in roots)
+ if (Roots is null)
+ {
+ return;
+ }
+
+ foreach (KeyValuePair> item in Roots)
{
item.Value.Collapse ();
}
@@ -719,19 +615,28 @@ public void CollapseAll ()
SetNeedsDraw ();
}
+ ///
+ /// Delegate for multi-colored tree views. Return the to use for each passed object or
+ /// null to use the default.
+ ///
+ public Func? ColorGetter { get; set; }
+
+ /// The current number of rows in the tree (ignoring the controls bounds).
+ public int ContentHeight => BuildLineMap ()!.Count ();
+
///
/// Called once for each visible row during rendering. Can be used to make last minute changes to color or text
/// rendered
///
- public event EventHandler> DrawLine;
+ public event EventHandler>? DrawLine;
///
/// Adjusts the to ensure the given is visible. Has no
/// effect if already visible.
///
- public void EnsureVisible (T model)
+ public void EnsureVisible (T? model)
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
int idx = map.IndexOf (b => Equals (b.Model, model));
@@ -763,7 +668,7 @@ public void EnsureVisible (T model)
/// object).
///
/// The object to expand.
- public void Expand (T toExpand)
+ public void Expand (T? toExpand)
{
if (toExpand is null)
{
@@ -777,7 +682,7 @@ public void Expand (T toExpand)
/// Expands the supplied object and all child objects.
/// The object to expand.
- public void ExpandAll (T toExpand)
+ public void ExpandAll (T? toExpand)
{
if (toExpand is null)
{
@@ -795,7 +700,12 @@ public void ExpandAll (T toExpand)
///
public void ExpandAll ()
{
- foreach (KeyValuePair> item in roots)
+ if (Roots is null)
+ {
+ return;
+ }
+
+ foreach (KeyValuePair> item in Roots)
{
item.Value.ExpandAll ();
}
@@ -809,16 +719,16 @@ public void ExpandAll ()
/// is true
///
///
- public IEnumerable GetAllSelectedObjects ()
+ public IEnumerable GetAllSelectedObjects ()
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? 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)
+ if (MultiSelect && map is {})
{
- foreach (T m in map.Select (b => b.Model).Where (IsSelected))
+ foreach (T? m in map.Select (b => b.Model).Where (IsSelected))
{
yield return m;
}
@@ -838,13 +748,13 @@ public IEnumerable GetAllSelectedObjects ()
///
/// An object in the tree.
///
- public IEnumerable GetChildren (T o)
+ public T? [] GetChildren (T? o)
{
- Branch branch = ObjectToBranch (o);
+ Branch? branch = ObjectToBranch (o);
if (branch is null || !branch.IsExpanded)
{
- return new T [0];
+ return Array.Empty ();
}
return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
@@ -858,9 +768,9 @@ public IEnumerable GetChildren (T o)
///
public int GetContentWidth (bool visible)
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
- if (map.Count == 0)
+ if (map is null || map.Count == 0)
{
return 0;
}
@@ -879,10 +789,10 @@ public int GetContentWidth (bool visible)
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 (Driver!));
}
- return map.Max (b => b.GetWidth (Driver));
+ return map.Max (b => b.GetWidth (Driver!));
}
///
@@ -894,7 +804,7 @@ public int GetContentWidth (bool visible)
///
/// The row of the of the .
/// The object currently displayed on this row or null.
- public T GetObjectOnRow (int row) { return HitTest (row)?.Model; }
+ public T GetObjectOnRow (int row) { return HitTest (row)?.Model!; }
///
///
@@ -910,7 +820,7 @@ public int GetContentWidth (bool visible)
///
public int? GetObjectRow (T toFind)
{
- int idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind));
+ int idx = BuildLineMap ().IndexOf (o => o.Model!.Equals (toFind));
if (idx == -1)
{
@@ -926,7 +836,7 @@ public int GetContentWidth (bool visible)
///
/// An object in the tree.
///
- public T GetParent (T o) { return ObjectToBranch (o)?.Parent?.Model; }
+ public T? GetParent (T? o) { return ObjectToBranch (o)?.Parent?.Model; }
///
/// Returns the index of the object if it is currently exposed (it's parent(s) have been
@@ -936,13 +846,17 @@ public int GetContentWidth (bool visible)
/// 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)
+ public int GetScrollOffsetOf (T? o)
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
+ if (map is null)
+ {
+ return -1;
+ }
for (var i = 0; i < map.Count; i++)
{
- if (map.ElementAt (i).Model.Equals (o))
+ if (map!.ElementAt (i).Model!.Equals (o))
{
return i;
}
@@ -972,8 +886,8 @@ public void GoTo (T toSelect)
/// Changes the to the last object in the tree and scrolls so that it is visible.
public void GoToEnd ()
{
- IReadOnlyCollection> map = BuildLineMap ();
- ScrollOffsetVertical = Math.Max (0, map.Count - Viewport.Height + 1);
+ IReadOnlyCollection>? map = BuildLineMap ();
+ ScrollOffsetVertical = Math.Max (0, map!.Count - Viewport.Height + 1);
SelectedObject = map.LastOrDefault ()?.Model;
SetNeedsDraw ();
@@ -986,18 +900,18 @@ public void GoToEnd ()
public void GoToFirst ()
{
ScrollOffsetVertical = 0;
- SelectedObject = roots.Keys.FirstOrDefault ();
+ SelectedObject = Roots?.Keys.FirstOrDefault ();
SetNeedsDraw ();
}
/// Clears any cached results of the tree state.
- public void InvalidateLineMap () { cachedLineMap = null; }
+ public void InvalidateLineMap () { _cachedLineMap = null; }
/// 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; }
+ public bool IsExpanded (T? o) { return ObjectToBranch (o)?.IsExpanded ?? false; }
///
/// Returns true if the is either the or part of a
@@ -1005,249 +919,82 @@ public void GoToFirst ()
///
///
///
- public bool IsSelected (T model) { return Equals (SelectedObject, model) || (MultiSelect && multiSelectedRegions.Any (s => s.Contains (model))); }
-
- // BUGBUG: OnMouseEvent is internal. TreeView should not be overriding.
- ///
- protected override bool OnMouseEvent (MouseEventArgs me)
- {
- // If it is not an event we care about
- if (me is { IsSingleClicked: false, IsPressed: false, IsReleased: false, IsWheel: false }
- && !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked))
- {
- // do nothing
- return false;
- }
+ public bool IsSelected (T? model) { return Equals (SelectedObject, model) || (MultiSelect && _multiSelectedRegions.Any (s => s.Contains (model))); }
- if (!HasFocus && CanFocus)
- {
- SetFocus ();
- }
+ ///
+ /// Gets the that searches the collection as the user
+ /// types.
+ ///
+ public CollectionNavigator KeystrokeNavigator { get; } = new ();
- if (me.Flags == MouseFlags.WheeledDown)
- {
- ScrollDown ();
+ /// Maximum number of nodes that can be expanded in any given branch.
+ public int MaxDepth { get; set; } = 100;
- return true;
- }
+ /// Moves the selection down by the height of the control (1 page).
+ /// True if the navigation should add the covered nodes to the selected current selection.
+ ///
+ public void MovePageDown (bool expandSelection = false) { AdjustSelection (Viewport.Height, expandSelection); }
- if (me.Flags == MouseFlags.WheeledUp)
- {
- ScrollUp ();
+ /// Moves the selection up by the height of the control (1 page).
+ /// True if the navigation should add the covered nodes to the selected current selection.
+ ///
+ public void MovePageUp (bool expandSelection = false) { AdjustSelection (-Viewport.Height, expandSelection); }
- return true;
- }
+ /// True to allow multiple objects to be selected at once.
+ ///
+ public bool MultiSelect { get; set; } = true;
- if (me.Flags == MouseFlags.WheeledRight)
- {
- ScrollOffsetHorizontal++;
- SetNeedsDraw ();
+ ///
+ /// This event is raised when an object is activated e.g. by double clicking or pressing
+ /// .
+ ///
+ public event EventHandler>? ObjectActivated;
- return true;
- }
+ ///
+ /// Mouse event to trigger . Defaults to double click (
+ /// ). Set to null to disable this feature.
+ ///
+ ///
+ public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked;
- if (me.Flags == MouseFlags.WheeledLeft)
+ // TODO: Update to use Key instead of KeyCode
+ /// Key which when pressed triggers . Defaults to Enter.
+ public KeyCode ObjectActivationKey
+ {
+ get => _objectActivationKey;
+ set
{
- ScrollOffsetHorizontal--;
- SetNeedsDraw ();
-
- return true;
+ if (_objectActivationKey != value)
+ {
+ KeyBindings.ReplaceKey (ObjectActivationKey, value);
+ _objectActivationKey = value;
+ SetNeedsDraw ();
+ }
}
+ }
- if (me.Flags.HasFlag (MouseFlags.Button1Clicked))
+ /// The root objects in the tree, note that this collection is of root objects only.
+ public IEnumerable? Objects => Roots?.Keys;
+
+ /// Positions the cursor at the start of the selected objects line (if visible).
+ public override Point? PositionCursor ()
+ {
+ if (CanFocus && HasFocus && Visible && SelectedObject is { })
{
- // The line they clicked on a branch
- Branch clickedBranch = HitTest (me.Position.Y);
+ IReadOnlyCollection>? map = BuildLineMap ();
+ int idx = map!.IndexOf (b => b.Model!.Equals (SelectedObject));
- if (clickedBranch is null)
+ // if currently selected line is visible
+ if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Viewport.Height)
{
- return false;
+ Move (0, idx - ScrollOffsetVertical);
+
+ return MultiSelect ? new (0, idx - ScrollOffsetVertical) : null;
}
+ }
- bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.Position.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 ();
- }
-
- SetNeedsDraw ();
-
- 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
- Branch clickedBranch = HitTest (me.Position.Y);
-
- if (clickedBranch is null)
- {
- return false;
- }
-
- // Double click changes the selection to the clicked node as well as triggering
- // activation otherwise it feels wierd
- SelectedObject = clickedBranch.Model;
- SetNeedsDraw ();
-
- // trigger activation event
- OnObjectActivated (new ObjectActivatedEventArgs (this, clickedBranch.Model));
-
- // mouse event is handled.
- return true;
- }
-
- return false;
- }
-
- /// Moves the selection down by the height of the control (1 page).
- /// True if the navigation should add the covered nodes to the selected current selection.
- ///
- public void MovePageDown (bool expandSelection = false) { AdjustSelection (Viewport.Height, expandSelection); }
-
- /// Moves the selection up by the height of the control (1 page).
- /// True if the navigation should add the covered nodes to the selected current selection.
- ///
- public void MovePageUp (bool expandSelection = false) { AdjustSelection (-Viewport.Height, expandSelection); }
-
- ///
- /// This event is raised when an object is activated e.g. by double clicking or pressing
- /// .
- ///
- public event EventHandler> ObjectActivated;
-
- ///
- protected override bool OnDrawingContent ()
- {
- if (roots is null)
- {
- return true;
- }
-
- if (TreeBuilder is null)
- {
- Move (0, 0);
- Driver?.AddStr (NoBuilderError);
-
- return true;
- }
-
- IReadOnlyCollection> map = BuildLineMap ();
-
- for (var line = 0; line < Viewport.Height; line++)
- {
- int 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, Viewport.Width);
- }
- else
- {
- // Else clear the line to prevent stale symbols due to scrolling etc
- Move (0, line);
- SetAttribute (GetNormalColor ());
- Driver?.AddStr (new string (' ', Viewport.Width));
- }
- }
-
- return true;
- }
-
- ///
- protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View currentFocused, [CanBeNull] View newFocused)
- {
- if (newHasFocus)
- {
- // If there is no selected object and there are objects in the tree, select the first one
- if (SelectedObject is null && Objects.Any ())
- {
- SelectedObject = Objects.First ();
- }
- }
- }
-
- ///
- protected override bool OnKeyDown (Key key)
- {
- if (!Enabled)
- {
- return false;
- }
-
- // If not a keybinding, is the key a searchable key press?
- if (CollectionNavigatorBase.IsCompatibleKey (key) && AllowLetterBasedNavigation)
- {
- // If there has been a call to InvalidateMap since the last time
- // we need a new one to reflect the new exposed tree state
- IReadOnlyCollection> map = BuildLineMap ();
-
- // Find the current selected object within the tree
- int current = map.IndexOf (b => b.Model == SelectedObject);
- int? newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)key);
-
- if (newIndex is int && newIndex != -1)
- {
- SelectedObject = map.ElementAt ((int)newIndex).Model;
- EnsureVisible (selectedObject);
- SetNeedsDraw ();
-
- return true;
- }
- }
-
- return false;
- }
-
- /// Positions the cursor at the start of the selected objects line (if visible).
- public override Point? PositionCursor ()
- {
- if (CanFocus && HasFocus && Visible && SelectedObject is { })
- {
- IReadOnlyCollection> map = BuildLineMap ();
- int idx = map.IndexOf (b => b.Model.Equals (SelectedObject));
-
- // if currently selected line is visible
- if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Viewport.Height)
- {
- Move (0, idx - ScrollOffsetVertical);
-
- return MultiSelect ? new (0, idx - ScrollOffsetVertical) : null;
- }
- }
- return base.PositionCursor ();
- }
+ return null;
+ }
///
/// Rebuilds the tree structure for all exposed objects starting with the root objects. Call this method when you
@@ -1256,9 +1003,12 @@ protected override bool OnKeyDown (Key key)
///
public void RebuildTree ()
{
- foreach (Branch branch in roots.Values)
+ if (Roots is { })
{
- branch.Rebuild ();
+ foreach (Branch branch in Roots.Values)
+ {
+ branch.Rebuild ();
+ }
}
InvalidateLineMap ();
@@ -1275,9 +1025,9 @@ public void RebuildTree ()
/// 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)
+ public void RefreshObject (T? o, bool startAtTop = false)
{
- Branch branch = ObjectToBranch (o);
+ Branch? branch = ObjectToBranch (o);
if (branch is { })
{
@@ -1291,11 +1041,15 @@ public void RefreshObject (T o, bool startAtTop = false)
/// If is the currently then the selection is cleared
/// .
///
- public void Remove (T o)
+ public void Remove (T? o)
{
- if (roots.ContainsKey (o))
+ if (o is null)
+ {
+ return;
+ }
+ if (Roots is { } && Roots.ContainsKey (o))
{
- roots.Remove (o);
+ Roots.Remove (o);
InvalidateLineMap ();
SetNeedsDraw ();
@@ -1316,10 +1070,40 @@ public void ScrollDown ()
}
}
+ /// 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);
+ SetNeedsDraw ();
+ }
+ }
+
+ /// 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 an offset of 0. To see changes in the UI call
+ /// .
+ ///
+ public int ScrollOffsetVertical
+ {
+ get => _scrollOffsetVertical;
+ set
+ {
+ _scrollOffsetVertical = Math.Max (0, value);
+ SetNeedsDraw ();
+ }
+ }
+
/// Scrolls the view area up a single line without changing the current selection.
public void ScrollUp ()
{
- if (scrollOffsetVertical > 0)
+ if (_scrollOffsetVertical > 0)
{
ScrollOffsetVertical--;
SetNeedsDraw ();
@@ -1334,23 +1118,46 @@ public void SelectAll ()
return;
}
- multiSelectedRegions.Clear ();
+ _multiSelectedRegions.Clear ();
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
- if (map.Count == 0)
+ if (map is null || map.Count == 0)
{
return;
}
- multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map));
+ _multiSelectedRegions.Push (new (map.ElementAt (0), map.Count, map));
SetNeedsDraw ();
- OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject));
+ OnSelectionChanged (new (this!, SelectedObject, SelectedObject));
+ }
+
+ ///
+ /// 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
+ {
+ T? oldValue = _selectedObject;
+ _selectedObject = value;
+
+ if (!ReferenceEquals (oldValue, value))
+ {
+ OnSelectionChanged (new (this!, oldValue, value));
+ }
+ }
}
/// Called when the changes.
- public event EventHandler> SelectionChanged;
+ public event EventHandler>? SelectionChanged;
+
+ /// Determines how sub-branches of the tree are dynamically built at runtime as the user expands root nodes.
+ ///
+ public ITreeBuilder? TreeBuilder { get; set; }
///
/// Implementation of and . Performs operation and updates
@@ -1358,14 +1165,14 @@ public void SelectAll ()
///
///
///
- protected void CollapseImpl (T toCollapse, bool all)
+ protected void CollapseImpl (T? toCollapse, bool all)
{
if (toCollapse is null)
{
return;
}
- Branch branch = ObjectToBranch (toCollapse);
+ Branch? branch = ObjectToBranch (toCollapse);
// Nothing to collapse
if (branch is null)
@@ -1411,7 +1218,7 @@ protected virtual void CursorLeft (bool ctrl)
}
else
{
- T parent = GetParent (SelectedObject);
+ T? parent = GetParent (SelectedObject);
if (parent is { })
{
@@ -1430,13 +1237,233 @@ protected override void Dispose (bool disposing)
ColorGetter = null;
}
+ ///
+ protected override bool OnDrawingContent ()
+ {
+ if (Roots is null)
+ {
+ return true;
+ }
+
+ if (TreeBuilder is null)
+ {
+ Move (0, 0);
+ Driver?.AddStr (NoBuilderError);
+
+ return true;
+ }
+
+ IReadOnlyCollection>? map = BuildLineMap ();
+
+ if (map is null)
+ {
+ return true;
+ }
+
+ for (var line = 0; line < Viewport.Height; line++)
+ {
+ int 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, Viewport.Width);
+ }
+ else
+ {
+ // Else clear the line to prevent stale symbols due to scrolling etc
+ Move (0, line);
+ SetAttribute (GetNormalColor ());
+ Driver?.AddStr (new (' ', Viewport.Width));
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ protected override void OnHasFocusChanged (bool newHasFocus, View? currentFocused, View? newFocused)
+ {
+ if (newHasFocus)
+ {
+ // If there is no selected object and there are objects in the tree, select the first one
+ if (SelectedObject is null && Objects is { } && Objects.Any ())
+ {
+ SelectedObject = Objects.First ();
+ }
+ }
+ }
+
+ ///
+ protected override bool OnKeyDown (Key key)
+ {
+ if (!Enabled)
+ {
+ return false;
+ }
+
+ if (!CollectionNavigatorBase.IsCompatibleKey (key) || !AllowLetterBasedNavigation)
+ {
+ return false;
+ }
+
+ // If not a keybinding, is the key a searchable key press?
+
+ // If there has been a call to InvalidateMap since the last time
+ // we need a new one to reflect the new exposed tree state
+ IReadOnlyCollection>? map = BuildLineMap ();
+
+ if (map == null)
+ {
+ return false;
+ }
+
+ // Find the current selected object within the tree
+ int current = map.IndexOf (b => b.Model == SelectedObject);
+ int? newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)key);
+
+ if (newIndex is { } && newIndex != -1)
+ {
+ SelectedObject = map.ElementAt ((int)newIndex).Model;
+ EnsureVisible (_selectedObject);
+ SetNeedsDraw ();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // BUGBUG: OnMouseEvent is internal. TreeView should not be overriding.
+ ///
+ protected override bool OnMouseEvent (MouseEventArgs me)
+ {
+ // If it is not an event we care about
+ if (me is { IsSingleClicked: false, IsPressed: false, IsReleased: false, IsWheel: false }
+ && !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked))
+ {
+ // do nothing
+ return false;
+ }
+
+ if (!HasFocus && CanFocus)
+ {
+ SetFocus ();
+ }
+
+ if (me.Flags == MouseFlags.WheeledDown)
+ {
+ ScrollDown ();
+
+ return true;
+ }
+
+ if (me.Flags == MouseFlags.WheeledUp)
+ {
+ ScrollUp ();
+
+ return true;
+ }
+
+ if (me.Flags == MouseFlags.WheeledRight)
+ {
+ ScrollOffsetHorizontal++;
+ SetNeedsDraw ();
+
+ return true;
+ }
+
+ if (me.Flags == MouseFlags.WheeledLeft)
+ {
+ ScrollOffsetHorizontal--;
+ SetNeedsDraw ();
+
+ return true;
+ }
+
+ if (me.Flags.HasFlag (MouseFlags.Button1Clicked))
+ {
+ // The line they clicked on a branch
+ Branch? clickedBranch = HitTest (me.Position.Y);
+
+ if (clickedBranch is null)
+ {
+ return false;
+ }
+
+ bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver!, me.Position.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 ();
+ }
+
+ SetNeedsDraw ();
+
+ 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
+ Branch? clickedBranch = HitTest (me.Position.Y);
+
+ if (clickedBranch is null)
+ {
+ return false;
+ }
+
+ // Double click changes the selection to the clicked node as well as triggering
+ // activation otherwise it feels wierd
+ SelectedObject = clickedBranch.Model;
+ SetNeedsDraw ();
+
+ // trigger activation event
+ OnObjectActivated (new (this, clickedBranch.Model!));
+
+ // mouse event is handled.
+ return true;
+ }
+
+ return false;
+ }
+
/// Raises the event.
///
protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) { ObjectActivated?.Invoke (this, e); }
/// Raises the SelectionChanged event.
///
- protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) { SelectionChanged?.Invoke (this, e); }
+ protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) { SelectionChanged?.Invoke (this, e); }
///
/// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of
@@ -1447,50 +1474,65 @@ protected override void Dispose (bool disposing)
/// the next etc.
///
///
- internal IReadOnlyCollection> BuildLineMap ()
+ internal IReadOnlyCollection>? BuildLineMap ()
{
- if (cachedLineMap is { })
+ if (_cachedLineMap is { })
{
- return cachedLineMap;
+ return _cachedLineMap;
}
List> toReturn = new ();
- foreach (Branch root in roots.Values)
+ if (Roots is { })
{
- IEnumerable> toAdd = AddToLineMap (root, false, out bool isMatch);
-
- if (isMatch)
+ foreach (Branch root in Roots.Values)
{
- toReturn.AddRange (toAdd);
+ IEnumerable>? toAdd = AddToLineMap (root, false, out bool isMatch);
+
+ if (isMatch && toAdd is { })
+ {
+ toReturn.AddRange (toAdd);
+ }
}
}
- cachedLineMap = new ReadOnlyCollection> (toReturn);
+ _cachedLineMap = new ReadOnlyCollection> (toReturn);
// Update the collection used for search-typing
- KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray ();
+ KeystrokeNavigator.Collection = _cachedLineMap.Select (b => AspectGetter! (b.Model!)).ToArray ();
- return cachedLineMap;
+ return _cachedLineMap;
}
/// Raises the DrawLine event
///
internal void OnDrawLine (DrawTreeViewLineEventArgs e) { DrawLine?.Invoke (this, e); }
- private IEnumerable> AddToLineMap (Branch currentBranch, bool parentMatches, out bool match)
+ ///
+ /// 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 ();
+
+ private IEnumerable>? AddToLineMap (Branch? currentBranch, bool parentMatches, out bool match)
{
+ if (currentBranch is null)
+ {
+ match = false;
+ return Array.Empty> ();
+ }
+
bool weMatch = IsFilterMatch (currentBranch);
var anyChildMatches = false;
List> toReturn = new ();
List> children = new ();
- if (currentBranch.IsExpanded)
+ if (currentBranch is { IsExpanded: true, ChildBranches: { } })
{
foreach (Branch subBranch in currentBranch.ChildBranches.Values)
{
- foreach (Branch sub in AddToLineMap (subBranch, weMatch, out bool childMatch))
+ foreach (Branch sub in AddToLineMap (subBranch, weMatch, out bool childMatch)!)
{
if (childMatch)
{
@@ -1520,10 +1562,10 @@ private IEnumerable> AddToLineMap (Branch currentBranch, bool paren
///
private void AdjustSelectionToNext (Func, bool> predicate)
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
// empty map means we can't select anything anyway
- if (map.Count == 0)
+ if (map is null || map.Count == 0)
{
return;
}
@@ -1560,9 +1602,14 @@ private void AdjustSelectionToNext (Func, bool> predicate)
/// 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)
+ private Branch? HitTest (int y)
{
- IReadOnlyCollection> map = BuildLineMap ();
+ IReadOnlyCollection>? map = BuildLineMap ();
+
+ if (map is null)
+ {
+ return null;
+ }
int idx = y + ScrollOffsetVertical;
@@ -1576,7 +1623,7 @@ private Branch HitTest (int y)
return map.ElementAt (idx);
}
- private bool IsFilterMatch (Branch branch) { return Filter?.IsMatch (branch.Model) ?? true; }
+ private bool IsFilterMatch (Branch? branch) { return Filter?.IsMatch (branch?.Model) ?? true; }
///
/// Returns the corresponding in the tree for . This will not
@@ -1584,35 +1631,5 @@ private Branch HitTest (int y)
///
///
/// 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)); }
-}
-
-internal class TreeSelection where T : class
-{
- private readonly HashSet included = new ();
-
- /// Creates a new selection between two branches in the tree
- ///
- ///
- ///
- public TreeSelection (Branch from, int toIndex, IReadOnlyCollection> map)
- {
- Origin = from;
- included.Add (Origin.Model);
-
- int oldIdx = map.IndexOf (from);
-
- int lowIndex = Math.Min (oldIdx, toIndex);
- int highIndex = Math.Max (oldIdx, toIndex);
-
- // Select everything between the old and new indexes
- foreach (Branch alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex))
- {
- included.Add (alsoInclude.Model);
- }
- }
-
- public Branch Origin { get; }
- public bool Contains (T model) { return included.Contains (model); }
-
-}
+ private Branch? ObjectToBranch (T? toFind) { return BuildLineMap ()!.FirstOrDefault (o => o.Model is { } && o.Model.Equals (toFind)); }
+}
\ No newline at end of file
diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs
index f371a18f84..ccafcffa64 100644
--- a/UICatalog/Scenarios/TreeViewFileSystem.cs
+++ b/UICatalog/Scenarios/TreeViewFileSystem.cs
@@ -299,7 +299,7 @@ private void SetExpandableSymbols (Rune expand, Rune? collapse)
_miNoSymbols.Checked = expand.Value == default (int);
_treeViewFiles.Style.ExpandableSymbol = expand;
- _treeViewFiles.Style.CollapseableSymbol = collapse;
+ _treeViewFiles.Style.CollapsableSymbol = collapse;
_treeViewFiles.SetNeedsDraw ();
}