mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 09:47:58 +01:00
Added RefreshObject and IsExpanded
RefreshObject notifies tree of changes to a model (e.g. it's children) and clears cached knowledge but persists the branch expansion state
This commit is contained in:
@@ -61,6 +61,22 @@ namespace Terminal.Gui {
|
||||
/// </summary>
|
||||
public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the state of the object <paramref name="o"/> in the tree. This will recompute children, string representation etc
|
||||
/// </summary>
|
||||
/// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
|
||||
/// <param name="o"></param>
|
||||
/// <param name="startAtTop">True to also refresh all ancestors of the objects branch (starting with the root). False to refresh only the passed node</param>
|
||||
public void RefreshObject (object o, bool startAtTop = false)
|
||||
{
|
||||
var branch = ObjectToBranch(o);
|
||||
if(branch != null) {
|
||||
branch.Refresh(startAtTop);
|
||||
SetNeedsDisplay();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The root objects in the tree, note that this collection is of root objects only
|
||||
@@ -364,6 +380,16 @@ namespace Terminal.Gui {
|
||||
SetNeedsDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given object <paramref name="o"/> is exposed in the tree and expanded otherwise false
|
||||
/// </summary>
|
||||
/// <param name="o"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsExpanded(object o)
|
||||
{
|
||||
return ObjectToBranch(o)?.IsExpanded ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapses the supplied object if it is currently expanded
|
||||
/// </summary>
|
||||
@@ -398,18 +424,22 @@ namespace Terminal.Gui {
|
||||
/// <summary>
|
||||
/// The users object that is being displayed by this branch of the tree
|
||||
/// </summary>
|
||||
public object Model {get;set;}
|
||||
public object Model {get;private set;}
|
||||
|
||||
/// <summary>
|
||||
/// The depth of the current branch. Depth of 0 indicates root level branches
|
||||
/// </summary>
|
||||
public int Depth {get;set;} = 0;
|
||||
public int Depth {get;private set;} = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
|
||||
/// </summary>
|
||||
public Dictionary<object,Branch> ChildBranches {get;set;}
|
||||
|
||||
/// <summary>
|
||||
/// The parent <see cref="Branch"/> or null if it is a root.
|
||||
/// </summary>
|
||||
public Branch Parent {get; private set;}
|
||||
|
||||
private TreeView tree;
|
||||
|
||||
@@ -426,6 +456,7 @@ namespace Terminal.Gui {
|
||||
|
||||
if(parentBranchIfAny != null) {
|
||||
Depth = parentBranchIfAny.Depth +1;
|
||||
Parent = parentBranchIfAny;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +469,9 @@ namespace Terminal.Gui {
|
||||
if (tree.ChildrenGetter == null)
|
||||
return;
|
||||
|
||||
this.ChildBranches = tree.ChildrenGetter(this.Model).ToDictionary(k=>k,val=>new Branch(tree,this,val));
|
||||
var children = tree.ChildrenGetter(this.Model) ?? new object[0];
|
||||
|
||||
this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch(tree,this,val));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -499,10 +532,50 @@ namespace Terminal.Gui {
|
||||
}
|
||||
}
|
||||
|
||||
internal void Collapse ()
|
||||
/// <summary>
|
||||
/// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
|
||||
/// </summary>
|
||||
public void Collapse ()
|
||||
{
|
||||
IsExpanded = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes cached knowledge in this branch e.g. what children an object has
|
||||
/// </summary>
|
||||
/// <param name="startAtTop">True to also refresh all <see cref="Parent"/> branches (starting with the root)</param>
|
||||
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.ChildrenGetter(this.Model) ?? new object[0];
|
||||
|
||||
// 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 toAdd in newChildren.Except(ChildBranches.Keys).ToArray())
|
||||
ChildBranches.Add(toAdd,new Branch(tree,this,toAdd));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -40,7 +40,51 @@ namespace UnitTests {
|
||||
return tree;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tests that <see cref="TreeView.Expand(object)"/> and <see cref="TreeView.IsExpanded(object)"/> are consistent
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsExpanded_TrueAfterExpand()
|
||||
{
|
||||
var tree = CreateTree(out Factory f, out _, out _);
|
||||
Assert.False(tree.IsExpanded(f));
|
||||
|
||||
tree.Expand(f);
|
||||
Assert.True(tree.IsExpanded(f));
|
||||
|
||||
tree.Collapse(f);
|
||||
Assert.False(tree.IsExpanded(f));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that <see cref="TreeView.IsExpanded(object)"/> and <see cref="TreeView.Expand(object)"/> behaves correctly when an object cannot be expanded (because it has no children)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsExpanded_FalseIfCannotExpand()
|
||||
{
|
||||
var tree = CreateTree(out Factory f, out Car c, out _);
|
||||
|
||||
// expose the car by expanding the factory
|
||||
tree.Expand(f);
|
||||
|
||||
// car is not expanded
|
||||
Assert.False(tree.IsExpanded(c));
|
||||
|
||||
//try to expand the car (should have no effect because cars have no children)
|
||||
tree.Expand(c);
|
||||
|
||||
Assert.False(tree.IsExpanded(c));
|
||||
|
||||
// should also be ignored
|
||||
tree.Collapse(c);
|
||||
|
||||
Assert.False(tree.IsExpanded(c));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests illegal ranges for <see cref="TreeView.ScrollOffset"/>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ScrollOffset_CannotBeNegative()
|
||||
{
|
||||
@@ -56,27 +100,72 @@ namespace UnitTests {
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests <see cref="TreeView.GetScrollOffsetOf(object)"/> for objects that are as yet undiscovered by the tree
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetScrollOffsetOf_MinusOneForUnRevealed()
|
||||
{
|
||||
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
|
||||
|
||||
// to start with the tree is collapsed and only knows about the root object
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
|
||||
|
||||
// reveal it by expanding the root object
|
||||
tree.Expand(f);
|
||||
|
||||
// tree now knows about children
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
|
||||
|
||||
// after collapsing the root node again
|
||||
tree.Collapse(f);
|
||||
|
||||
// tree no longer knows about the locations of these objects
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using <see cref="TreeView.RefreshObject(object, bool)"/>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RefreshObject_ChildRemoved()
|
||||
{
|
||||
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
|
||||
|
||||
//reveal it by expanding the root object
|
||||
tree.Expand(f);
|
||||
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
|
||||
|
||||
tree.Collapse(f);
|
||||
|
||||
// Factory now no longer makes Car c1 (only c2)
|
||||
f.Cars = new Car[]{c2};
|
||||
|
||||
// Tree does not know this yet
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
|
||||
|
||||
// If the user has selected the node c1
|
||||
tree.SelectedObject = c1;
|
||||
|
||||
// When we refresh the tree
|
||||
tree.RefreshObject(f);
|
||||
|
||||
// Now tree knows that factory has only one child node c2
|
||||
Assert.Equal(0,tree.GetScrollOffsetOf(f));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
|
||||
Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
|
||||
Assert.Equal(1,tree.GetScrollOffsetOf(c2));
|
||||
|
||||
// The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory)
|
||||
Assert.Equal(f,tree.SelectedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user