2129 lines
85 KiB
C#
2129 lines
85 KiB
C#
/*
|
|
* TreeListView - A listview that can show a tree of objects in a column
|
|
*
|
|
* Author: Phillip Piper
|
|
* Date: 23/09/2008 11:15 AM
|
|
*
|
|
* Change log:
|
|
* 2014-10-08 JPP - Fixed an issue where pre-expanded branches would not initially expand properly
|
|
* 2014-09-29 JPP - Fixed issue where RefreshObject() on a root object could cause exceptions
|
|
* - Fixed issue where CollapseAll() while filtering could cause exception
|
|
* 2014-03-09 JPP - Fixed issue where removing a branches only child and then calling RefreshObject()
|
|
* could throw an exception.
|
|
* v2.7
|
|
* 2014-02-23 JPP - Added Reveal() method to show a deeply nested models.
|
|
* 2014-02-05 JPP - Fix issue where refreshing a non-root item would collapse all expanded children of that item
|
|
* 2014-02-01 JPP - ClearObjects() now actually, you know, clears objects :)
|
|
* - Corrected issue where Expanded event was being raised twice.
|
|
* - RebuildChildren() no longer checks if CanExpand is true before rebuilding.
|
|
* 2014-01-16 JPP - Corrected an off-by-1 error in hit detection, which meant that clicking in the last 16 pixels
|
|
* of an items label was being ignored.
|
|
* 2013-11-20 JPP - Moved event triggers into Collapse() and Expand() so that the events are always triggered.
|
|
* - CheckedObjects now includes objects that are in a branch that is currently collapsed
|
|
* - CollapseAll() and ExpandAll() now trigger cancellable events
|
|
* 2013-09-29 JPP - Added TreeFactory to allow the underlying Tree to be replaced by another implementation.
|
|
* 2013-09-23 JPP - Fixed long standing issue where RefreshObject() would not work on root objects
|
|
* which overrode Equals()/GetHashCode().
|
|
* 2013-02-23 JPP - Added HierarchicalCheckboxes. When this is true, the checkedness of a parent
|
|
* is an synopsis of the checkedness of its children. When all children are checked,
|
|
* the parent is checked. When all children are unchecked, the parent is unchecked.
|
|
* If some children are checked and some are not, the parent is indeterminate.
|
|
* v2.6
|
|
* 2012-10-25 JPP - Circumvent annoying issue in ListView control where changing
|
|
* selection would leave artifacts on the control.
|
|
* 2012-08-10 JPP - Don't trigger selection changed events during expands
|
|
*
|
|
* v2.5.1
|
|
* 2012-04-30 JPP - Fixed issue where CheckedObjects would return model objects that had been filtered out.
|
|
* - Allow any column to render the tree, not just column 0 (still not sure about this one)
|
|
* v2.5.0
|
|
* 2011-04-20 JPP - Added ExpandedObjects property and RebuildAll() method.
|
|
* 2011-04-09 JPP - Added Expanding, Collapsing, Expanded and Collapsed events.
|
|
* The ..ing events are cancellable. These are only fired in response
|
|
* to user actions.
|
|
* v2.4.1
|
|
* 2010-06-15 JPP - Fixed issue in Tree.RemoveObjects() which resulted in removed objects
|
|
* being reported as still existing.
|
|
* v2.3
|
|
* 2009-09-01 JPP - Fixed off-by-one error that was messing up hit detection
|
|
* 2009-08-27 JPP - Fixed issue when dragging a node from one place to another in the tree
|
|
* v2.2.1
|
|
* 2009-07-14 JPP - Clicks to the left of the expander in tree cells are now ignored.
|
|
* v2.2
|
|
* 2009-05-12 JPP - Added tree traverse operations: GetParent and GetChildren.
|
|
* - Added DiscardAllState() to completely reset the TreeListView.
|
|
* 2009-05-10 JPP - Removed all unsafe code
|
|
* 2009-05-09 JPP - Fixed issue where any command (Expand/Collapse/Refresh) on a model
|
|
* object that was once visible but that is currently in a collapsed branch
|
|
* would cause the control to crash.
|
|
* 2009-05-07 JPP - Fixed issue where RefreshObjects() would fail when none of the given
|
|
* objects were present/visible.
|
|
* 2009-04-20 JPP - Fixed issue where calling Expand() on an already expanded branch confused
|
|
* the display of the children (SF#2499313)
|
|
* 2009-03-06 JPP - Calculate edit rectangle on column 0 more accurately
|
|
* v2.1
|
|
* 2009-02-24 JPP - All commands now work when the list is empty (SF #2631054)
|
|
* - TreeListViews can now be printed with ListViewPrinter
|
|
* 2009-01-27 JPP - Changed to use new Renderer and HitTest scheme
|
|
* 2009-01-22 JPP - Added RevealAfterExpand property. If this is true (the default),
|
|
* after expanding a branch, the control scrolls to reveal as much of the
|
|
* expanded branch as possible.
|
|
* 2009-01-13 JPP - Changed TreeRenderer to work with visual styles are disabled
|
|
* v2.0.1
|
|
* 2009-01-07 JPP - Made all public and protected methods virtual
|
|
* - Changed some classes from 'internal' to 'protected' so that they
|
|
* can be accessed by subclasses of TreeListView.
|
|
* 2008-12-22 JPP - Added UseWaitCursorWhenExpanding property
|
|
* - Made TreeRenderer public so that it can be subclassed
|
|
* - Added LinePen property to TreeRenderer to allow the connection drawing
|
|
* pen to be changed
|
|
* - Fixed some rendering issues where the text highlight rect was miscalculated
|
|
* - Fixed connection line problem when there is only a single root
|
|
* v2.0
|
|
* 2008-12-10 JPP - Expand/collapse with mouse now works when there is no SmallImageList.
|
|
* 2008-12-01 JPP - Search-by-typing now works.
|
|
* 2008-11-26 JPP - Corrected calculation of expand/collapse icon (SF#2338819)
|
|
* - Fixed ugliness with dotted lines in renderer (SF#2332889)
|
|
* - Fixed problem with custom selection colors (SF#2338805)
|
|
* 2008-11-19 JPP - Expand/collapse now preserve the selection -- more or less :)
|
|
* - Overrode RefreshObjects() to rebuild the given objects and their children
|
|
* 2008-11-05 JPP - Added ExpandAll() and CollapseAll() commands
|
|
* - CanExpand is no longer cached
|
|
* - Renamed InitialBranches to RootModels since it deals with model objects
|
|
* 2008-09-23 JPP Initial version
|
|
*
|
|
* TO DO:
|
|
*
|
|
* Copyright (C) 2006-2014 Phillip Piper
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Windows.Forms;
|
|
|
|
namespace BrightIdeasSoftware
|
|
{
|
|
/// <summary>
|
|
/// A TreeListView combines an expandable tree structure with list view columns.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>To support tree operations, two delegates must be provided:</para>
|
|
/// <list type="table">
|
|
/// <item>
|
|
/// <term>
|
|
/// CanExpandGetter
|
|
/// </term>
|
|
/// <description>
|
|
/// This delegate must accept a model object and return a boolean indicating
|
|
/// if that model should be expandable.
|
|
/// </description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// ChildrenGetter
|
|
/// </term>
|
|
/// <description>
|
|
/// This delegate must accept a model object and return an IEnumerable of model
|
|
/// objects that will be displayed as children of the parent model. This delegate will only be called
|
|
/// for a model object if the CanExpandGetter has already returned true for that model.
|
|
/// </description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// ParentGetter
|
|
/// </term>
|
|
/// <description>
|
|
/// This delegate must accept a model object and return the parent model.
|
|
/// This delegate will only be called when HierarchicalCheckboxes is true OR when Reveal() is called.
|
|
/// </description>
|
|
/// </item>
|
|
/// </list>
|
|
/// <para>
|
|
/// The top level branches of the tree are set via the Roots property. SetObjects(), AddObjects()
|
|
/// and RemoveObjects() are interpreted as operations on this collection of roots.
|
|
/// </para>
|
|
/// <para>
|
|
/// To add new children to an existing branch, make changes to your model objects and then
|
|
/// call RefreshObject() on the parent.
|
|
/// </para>
|
|
/// <para>The tree must be a directed acyclic graph -- no cycles are allowed. Put more mundanely,
|
|
/// each model object must appear only once in the tree. If the same model object appears in two
|
|
/// places in the tree, the control will become confused.</para>
|
|
/// </remarks>
|
|
public partial class TreeListView : VirtualObjectListView
|
|
{
|
|
/// <summary>
|
|
/// Make a default TreeListView
|
|
/// </summary>
|
|
public TreeListView() {
|
|
this.OwnerDraw = true;
|
|
this.View = View.Details;
|
|
this.CheckedObjectsMustStillExistInList = false;
|
|
|
|
// ReSharper disable DoNotCallOverridableMethodsInConstructor
|
|
this.RegenerateTree();
|
|
this.TreeColumnRenderer = new TreeRenderer();
|
|
// ReSharper restore DoNotCallOverridableMethodsInConstructor
|
|
|
|
// This improves hit detection even if we don't have any state image
|
|
this.SmallImageList = new ImageList();
|
|
// this.StateImageList.ImageSize = new Size(6, 6);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Properties
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to decide if a model object can be expanded.
|
|
/// </summary>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public virtual CanExpandGetterDelegate CanExpandGetter {
|
|
get { return this.TreeModel.CanExpandGetter; }
|
|
set { this.TreeModel.CanExpandGetter = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether or not this listview is capable of showing groups
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public override bool CanShowGroups {
|
|
get {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to fetch the children of a model object
|
|
/// </summary>
|
|
/// <remarks>This delegate will only be called if the CanExpand delegate has
|
|
/// returned true for the model object.</remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public virtual ChildrenGetterDelegate ChildrenGetter {
|
|
get { return this.TreeModel.ChildrenGetter; }
|
|
set { this.TreeModel.ChildrenGetter = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to fetch the parent of a model object
|
|
/// </summary>
|
|
/// <returns>The parent of the given model, or null if the model doesn't exist or
|
|
/// if the model is a root</returns>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public ParentGetterDelegate ParentGetter {
|
|
get { return parentGetter; }
|
|
set { parentGetter = value; }
|
|
}
|
|
private ParentGetterDelegate parentGetter;
|
|
|
|
/// <summary>
|
|
/// Get or set the collection of model objects that are checked.
|
|
/// When setting this property, any row whose model object isn't
|
|
/// in the given collection will be unchecked. Setting to null is
|
|
/// equivalent to unchecking all.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This property returns a simple collection. Changes made to the returned
|
|
/// collection do NOT affect the list. This is different to the behaviour of
|
|
/// CheckedIndicies collection.
|
|
/// </para>
|
|
/// <para>
|
|
/// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects.
|
|
/// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus
|
|
/// the number of objects to be checked.
|
|
/// </para>
|
|
/// <para>
|
|
/// If the ListView is not currently showing CheckBoxes, this property does nothing. It does
|
|
/// not remember any check box settings made.
|
|
/// </para>
|
|
/// </remarks>
|
|
public override IList CheckedObjects {
|
|
get {
|
|
return base.CheckedObjects;
|
|
}
|
|
set {
|
|
ArrayList objectsToRecalculate = new ArrayList(this.CheckedObjects);
|
|
if (value != null)
|
|
objectsToRecalculate.AddRange(value);
|
|
|
|
base.CheckedObjects = value;
|
|
|
|
if (this.HierarchicalCheckboxes)
|
|
RecalculateHierarchicalCheckBoxGraph(objectsToRecalculate);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the model objects that are expanded.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>This can be used to expand model objects before they are seen.</para>
|
|
/// <para>
|
|
/// Setting this does *not* force the control to rebuild
|
|
/// its display. You need to call RebuildAll(true).
|
|
/// </para>
|
|
/// </remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public IEnumerable ExpandedObjects {
|
|
get {
|
|
return this.TreeModel.mapObjectToExpanded.Keys;
|
|
}
|
|
set {
|
|
this.TreeModel.mapObjectToExpanded.Clear();
|
|
if (value != null) {
|
|
foreach (object x in value)
|
|
this.TreeModel.SetModelExpanded(x, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the filter that is applied to our whole list of objects.
|
|
/// TreeListViews do not currently support whole list filters.
|
|
/// </summary>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public override IListFilter ListFilter {
|
|
get { return null; }
|
|
set {
|
|
System.Diagnostics.Debug.Assert(value == null, "TreeListView do not support ListFilters");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether this tree list view will display hierarchical checkboxes.
|
|
/// Hierarchical checkboxes is when a parent's "checkedness" is calculated from
|
|
/// the "checkedness" of its children. If all children are checked, the parent
|
|
/// will be checked. If all children are unchecked, the parent will also be unchecked.
|
|
/// If some children are checked and others are not, the parent will be indeterminate.
|
|
/// </summary>
|
|
[Category("ObjectListView"),
|
|
Description("Show hierarchical checkboxes be enabled?"),
|
|
DefaultValue(false)]
|
|
public virtual bool HierarchicalCheckboxes {
|
|
get { return this.hierarchicalCheckboxes; }
|
|
set {
|
|
if (this.hierarchicalCheckboxes == value)
|
|
return;
|
|
|
|
this.hierarchicalCheckboxes = value;
|
|
this.CheckBoxes = value;
|
|
if (value)
|
|
this.TriStateCheckBoxes = false;
|
|
}
|
|
}
|
|
private bool hierarchicalCheckboxes;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the collection of root objects of the tree
|
|
/// </summary>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public override IEnumerable Objects {
|
|
get { return this.Roots; }
|
|
set { this.Roots = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the collection of objects that will be considered when creating clusters
|
|
/// (which are used to generate Excel-like column filters)
|
|
/// </summary>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public override IEnumerable ObjectsForClustering {
|
|
get {
|
|
for (int i = 0; i < this.TreeModel.GetObjectCount(); i++)
|
|
yield return this.TreeModel.GetNthObject(i);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// After expanding a branch, should the TreeListView attempts to show as much of the
|
|
/// revealed descendents as possible.
|
|
/// </summary>
|
|
[Category("ObjectListView"),
|
|
Description("Should the parent of an expand subtree be scrolled to the top revealing the children?"),
|
|
DefaultValue(true)]
|
|
public bool RevealAfterExpand {
|
|
get { return revealAfterExpand; }
|
|
set { revealAfterExpand = value; }
|
|
}
|
|
private bool revealAfterExpand = true;
|
|
|
|
/// <summary>
|
|
/// The model objects that form the top level branches of the tree.
|
|
/// </summary>
|
|
/// <remarks>Setting this does <b>NOT</b> reset the state of the control.
|
|
/// In particular, it does not collapse branches.</remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public virtual IEnumerable Roots {
|
|
get { return this.TreeModel.RootObjects; }
|
|
set {
|
|
this.TreeColumnRenderer = this.TreeColumnRenderer;
|
|
this.TreeModel.RootObjects = value ?? new ArrayList();
|
|
this.UpdateVirtualListSize();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make sure that at least one column is displaying a tree.
|
|
/// If no columns is showing the tree, make column 0 do it.
|
|
/// </summary>
|
|
protected virtual void EnsureTreeRendererPresent(TreeRenderer renderer) {
|
|
if (this.Columns.Count == 0)
|
|
return;
|
|
|
|
foreach (OLVColumn col in this.Columns) {
|
|
if (col.Renderer is TreeRenderer) {
|
|
col.Renderer = renderer;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No column held a tree renderer, so give column 0 one
|
|
OLVColumn columnZero = this.GetColumn(0);
|
|
columnZero.Renderer = renderer;
|
|
columnZero.WordWrap = columnZero.WordWrap;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the renderer that will be used to draw the tree structure.
|
|
/// Setting this to null resets the renderer to default.
|
|
/// </summary>
|
|
/// <remarks>If a column is currently rendering the tree, the renderer
|
|
/// for that column will be replaced. If no column is rendering the tree,
|
|
/// column 0 will be given this renderer.</remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public virtual TreeRenderer TreeColumnRenderer {
|
|
get { return treeRenderer ?? (treeRenderer = new TreeRenderer()); }
|
|
set {
|
|
treeRenderer = value ?? new TreeRenderer();
|
|
EnsureTreeRendererPresent(treeRenderer);
|
|
}
|
|
}
|
|
private TreeRenderer treeRenderer;
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to create the underlying Tree structure
|
|
/// that the TreeListView uses to manage the information about the tree.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>The factory must not return null. </para>
|
|
/// <para>
|
|
/// Most users of TreeListView will never have to use this delegate.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public TreeFactoryDelegate TreeFactory {
|
|
get { return treeFactory; }
|
|
set { treeFactory = value; }
|
|
}
|
|
private TreeFactoryDelegate treeFactory;
|
|
|
|
/// <summary>
|
|
/// Should a wait cursor be shown when a branch is being expanded?
|
|
/// </summary>
|
|
/// <remarks>When this is true, the wait cursor will be shown whilst the children of the
|
|
/// branch are being fetched. If the children of the branch have already been cached,
|
|
/// the cursor will not change.</remarks>
|
|
[Category("ObjectListView"),
|
|
Description("Should a wait cursor be shown when a branch is being expanded?"),
|
|
DefaultValue(true)]
|
|
public virtual bool UseWaitCursorWhenExpanding {
|
|
get { return useWaitCursorWhenExpanding; }
|
|
set { useWaitCursorWhenExpanding = value; }
|
|
}
|
|
private bool useWaitCursorWhenExpanding = true;
|
|
|
|
/// <summary>
|
|
/// Gets the model that is used to manage the tree structure
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Don't mess with this property unless you really know what you are doing.
|
|
/// If you don't already know what it's for, you don't need it.</remarks>
|
|
[Browsable(false),
|
|
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
|
|
public Tree TreeModel {
|
|
get { return this.treeModel; }
|
|
protected set { this.treeModel = value; }
|
|
}
|
|
private Tree treeModel;
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Accessing
|
|
|
|
/// <summary>
|
|
/// Return true if the branch at the given model is expanded
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns></returns>
|
|
public virtual bool IsExpanded(Object model) {
|
|
Branch br = this.TreeModel.GetBranch(model);
|
|
return (br != null && br.IsExpanded);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Commands
|
|
|
|
/// <summary>
|
|
/// Collapse the subtree underneath the given model
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
public virtual void Collapse(Object model) {
|
|
if (this.GetItemCount() == 0)
|
|
return;
|
|
|
|
OLVListItem item = this.ModelToItem(model);
|
|
TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(model, item);
|
|
this.OnCollapsing(args);
|
|
if (args.Canceled)
|
|
return;
|
|
|
|
IList selection = this.SelectedObjects;
|
|
int index = this.TreeModel.Collapse(model);
|
|
if (index >= 0) {
|
|
this.UpdateVirtualListSize();
|
|
this.SelectedObjects = selection;
|
|
if (index < this.GetItemCount())
|
|
this.RedrawItems(index, this.GetItemCount() - 1, true);
|
|
this.OnCollapsed(new TreeBranchCollapsedEventArgs(model, item));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse all subtrees within this control
|
|
/// </summary>
|
|
public virtual void CollapseAll() {
|
|
if (this.GetItemCount() == 0)
|
|
return;
|
|
|
|
TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(null, null);
|
|
this.OnCollapsing(args);
|
|
if (args.Canceled)
|
|
return;
|
|
|
|
IList selection = this.SelectedObjects;
|
|
int index = this.TreeModel.CollapseAll();
|
|
if (index >= 0) {
|
|
this.UpdateVirtualListSize();
|
|
this.SelectedObjects = selection;
|
|
if (index < this.GetItemCount())
|
|
this.RedrawItems(index, this.GetItemCount() - 1, true);
|
|
this.OnCollapsed(new TreeBranchCollapsedEventArgs(null, null));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all items from this list
|
|
/// </summary>
|
|
/// <remark>This method can safely be called from background threads.</remark>
|
|
public override void ClearObjects() {
|
|
if (this.InvokeRequired)
|
|
this.Invoke(new MethodInvoker(this.ClearObjects));
|
|
else {
|
|
this.Roots = null;
|
|
this.DiscardAllState();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse all roots and forget everything we know about all models
|
|
/// </summary>
|
|
public virtual void DiscardAllState() {
|
|
this.CheckStateMap.Clear();
|
|
this.RebuildAll(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand the subtree underneath the given model object
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
public virtual void Expand(Object model) {
|
|
if (this.GetItemCount() == 0)
|
|
return;
|
|
|
|
// Give the world a chance to cancel the expansion
|
|
OLVListItem item = this.ModelToItem(model);
|
|
TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(model, item);
|
|
this.OnExpanding(args);
|
|
if (args.Canceled)
|
|
return;
|
|
|
|
// Remember the selection so we can put it back later
|
|
IList selection = this.SelectedObjects;
|
|
|
|
// Expand the model first
|
|
int index = this.TreeModel.Expand(model);
|
|
if (index < 0)
|
|
return;
|
|
|
|
// Update the size of the list and restore the selection
|
|
this.UpdateVirtualListSize();
|
|
using (this.SuspendSelectionEventsDuring())
|
|
this.SelectedObjects = selection;
|
|
|
|
// Redraw the items that were changed by the expand operation
|
|
this.RedrawItems(index, this.GetItemCount() - 1, true);
|
|
|
|
this.OnExpanded(new TreeBranchExpandedEventArgs(model, item));
|
|
|
|
if (this.RevealAfterExpand && index > 0) {
|
|
// TODO: This should be a separate method
|
|
this.BeginUpdate();
|
|
try {
|
|
int countPerPage = NativeMethods.GetCountPerPage(this);
|
|
int descedentCount = this.TreeModel.GetVisibleDescendentCount(model);
|
|
// If all of the descendents can be shown in the window, make sure that last one is visible.
|
|
// If all the descendents can't fit into the window, move the model to the top of the window
|
|
// (which will show as many of the descendents as possible)
|
|
if (descedentCount < countPerPage) {
|
|
this.EnsureVisible(index + descedentCount);
|
|
} else {
|
|
this.TopItemIndex = index;
|
|
}
|
|
}
|
|
finally {
|
|
this.EndUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand all the branches within this tree recursively.
|
|
/// </summary>
|
|
/// <remarks>Be careful: this method could take a long time for large trees.</remarks>
|
|
public virtual void ExpandAll() {
|
|
if (this.GetItemCount() == 0)
|
|
return;
|
|
|
|
// Give the world a chance to cancel the expansion
|
|
TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(null, null);
|
|
this.OnExpanding(args);
|
|
if (args.Canceled)
|
|
return;
|
|
|
|
IList selection = this.SelectedObjects;
|
|
int index = this.TreeModel.ExpandAll();
|
|
if (index < 0)
|
|
return;
|
|
|
|
this.UpdateVirtualListSize();
|
|
using (this.SuspendSelectionEventsDuring())
|
|
this.SelectedObjects = selection;
|
|
this.RedrawItems(index, this.GetItemCount() - 1, true);
|
|
this.OnExpanded(new TreeBranchExpandedEventArgs(null, null));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completely rebuild the tree structure
|
|
/// </summary>
|
|
/// <param name="preserveState">If true, the control will try to preserve selection and expansion</param>
|
|
public virtual void RebuildAll(bool preserveState) {
|
|
int previousTopItemIndex = preserveState ? this.TopItemIndex : -1;
|
|
|
|
this.RebuildAll(
|
|
preserveState ? this.SelectedObjects : null,
|
|
preserveState ? this.ExpandedObjects : null,
|
|
preserveState ? this.CheckedObjects : null);
|
|
|
|
if (preserveState)
|
|
this.TopItemIndex = previousTopItemIndex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completely rebuild the tree structure
|
|
/// </summary>
|
|
/// <param name="selected">If not null, this list of objects will be selected after the tree is rebuilt</param>
|
|
/// <param name="expanded">If not null, this collection of objects will be expanded after the tree is rebuilt</param>
|
|
/// <param name="checkedObjects">If not null, this collection of objects will be checked after the tree is rebuilt</param>
|
|
protected virtual void RebuildAll(IList selected, IEnumerable expanded, IList checkedObjects) {
|
|
// Remember the bits of info we don't want to forget (anyone ever see Memento?)
|
|
IEnumerable roots = this.Roots;
|
|
CanExpandGetterDelegate canExpand = this.CanExpandGetter;
|
|
ChildrenGetterDelegate childrenGetter = this.ChildrenGetter;
|
|
|
|
try {
|
|
this.BeginUpdate();
|
|
|
|
// Give ourselves a new data structure
|
|
this.RegenerateTree();
|
|
|
|
// Put back the bits we didn't want to forget
|
|
this.CanExpandGetter = canExpand;
|
|
this.ChildrenGetter = childrenGetter;
|
|
if (expanded != null)
|
|
this.ExpandedObjects = expanded;
|
|
this.Roots = roots;
|
|
if (selected != null)
|
|
this.SelectedObjects = selected;
|
|
if (checkedObjects != null)
|
|
this.CheckedObjects = checkedObjects;
|
|
}
|
|
finally {
|
|
this.EndUpdate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unroll all the ancestors of the given model and make sure it is then visible.
|
|
/// </summary>
|
|
/// <remarks>This works best when a ParentGetter is installed.</remarks>
|
|
/// <param name="modelToReveal">The object to be revealed</param>
|
|
/// <param name="selectAfterReveal">If true, the model will be selected and focused after being revealed</param>
|
|
/// <returns>True if the object was found and revealed. False if it was not found.</returns>
|
|
public virtual void Reveal(object modelToReveal, bool selectAfterReveal) {
|
|
// Collect all the ancestors of the model
|
|
ArrayList ancestors = new ArrayList();
|
|
foreach (object ancestor in this.GetAncestors(modelToReveal))
|
|
ancestors.Add(ancestor);
|
|
|
|
// Arrange them from root down to the model's immediate parent
|
|
ancestors.Reverse();
|
|
try {
|
|
this.BeginUpdate();
|
|
foreach (object ancestor in ancestors)
|
|
this.Expand(ancestor);
|
|
this.EnsureModelVisible(modelToReveal);
|
|
if (selectAfterReveal)
|
|
this.SelectObject(modelToReveal, true);
|
|
}
|
|
finally {
|
|
this.EndUpdate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the rows that are showing the given objects
|
|
/// </summary>
|
|
public override void RefreshObjects(IList modelObjects) {
|
|
if (this.InvokeRequired) {
|
|
this.Invoke((MethodInvoker) delegate { this.RefreshObjects(modelObjects); });
|
|
return;
|
|
}
|
|
// There is no point in refreshing anything if the list is empty
|
|
if (this.GetItemCount() == 0)
|
|
return;
|
|
|
|
// Remember the selection so we can put it back later
|
|
IList selection = this.SelectedObjects;
|
|
|
|
// We actually need to refresh the parents.
|
|
// Refreshes on root objects have to be handled differently
|
|
ArrayList updatedRoots = new ArrayList();
|
|
Hashtable modelsAndParents = new Hashtable();
|
|
foreach (Object model in modelObjects) {
|
|
if (model == null)
|
|
continue;
|
|
modelsAndParents[model] = true;
|
|
object parent = GetParent(model);
|
|
if (parent == null) {
|
|
updatedRoots.Add(model);
|
|
} else {
|
|
modelsAndParents[parent] = true;
|
|
}
|
|
}
|
|
|
|
// Update any changed roots
|
|
if (updatedRoots.Count > 0) {
|
|
ArrayList newRoots = ObjectListView.EnumerableToArray(this.Roots, false);
|
|
bool changed = false;
|
|
foreach (Object model in updatedRoots) {
|
|
int index = newRoots.IndexOf(model);
|
|
if (index >= 0 && !ReferenceEquals(newRoots[index], model)) {
|
|
newRoots[index] = model;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed)
|
|
this.Roots = newRoots;
|
|
}
|
|
|
|
// Refresh each object, remembering where the first update occurred
|
|
int firstChange = Int32.MaxValue;
|
|
foreach (Object model in modelsAndParents.Keys) {
|
|
if (model != null) {
|
|
int index = this.TreeModel.RebuildChildren(model);
|
|
if (index >= 0)
|
|
firstChange = Math.Min(firstChange, index);
|
|
}
|
|
}
|
|
|
|
// If we didn't refresh any objects, don't do anything else
|
|
if (firstChange >= this.GetItemCount())
|
|
return;
|
|
|
|
this.ClearCachedInfo();
|
|
this.UpdateVirtualListSize();
|
|
this.SelectedObjects = selection;
|
|
|
|
// Redraw everything from the first update to the end of the list
|
|
this.RedrawItems(firstChange, this.GetItemCount() - 1, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change the check state of the given object to be the given state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the given model object isn't in the list, we still try to remember
|
|
/// its state, in case it is referenced in the future.</remarks>
|
|
/// <param name="modelObject"></param>
|
|
/// <param name="state"></param>
|
|
/// <returns>True if the checkedness of the model changed</returns>
|
|
protected override bool SetObjectCheckedness(object modelObject, CheckState state) {
|
|
// If the checkedness of the given model changes AND this tree has
|
|
// hierarchical checkboxes, then we need to update the checkedness of
|
|
// its children, and recalculate the checkedness of the parent (recursively)
|
|
if (!base.SetObjectCheckedness(modelObject, state))
|
|
return false;
|
|
|
|
if (!this.HierarchicalCheckboxes)
|
|
return true;
|
|
|
|
// Give each child the same checkedness as the model
|
|
|
|
CheckState? checkedness = this.GetCheckState(modelObject);
|
|
if (!checkedness.HasValue || checkedness.Value == CheckState.Indeterminate)
|
|
return true;
|
|
|
|
foreach (object child in this.GetChildrenWithoutExpanding(modelObject)) {
|
|
this.SetObjectCheckedness(child, checkedness.Value);
|
|
}
|
|
|
|
ArrayList args = new ArrayList();
|
|
args.Add(modelObject);
|
|
this.RecalculateHierarchicalCheckBoxGraph(args);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
private IEnumerable GetChildrenWithoutExpanding(Object model) {
|
|
Branch br = this.TreeModel.GetBranch(model);
|
|
if (br == null || !br.CanExpand)
|
|
return new ArrayList();
|
|
|
|
return br.Children;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggle the expanded state of the branch at the given model object
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
public virtual void ToggleExpansion(Object model) {
|
|
if (this.IsExpanded(model))
|
|
this.Collapse(model);
|
|
else
|
|
this.Expand(model);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Commands - Tree traversal
|
|
|
|
/// <summary>
|
|
/// Return whether or not the given model can expand.
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <remarks>The given model must have already been seen in the tree</remarks>
|
|
public virtual bool CanExpand(Object model) {
|
|
Branch br = this.TreeModel.GetBranch(model);
|
|
return (br != null && br.CanExpand);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the model object that is the parent of the given model object.
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns></returns>
|
|
/// <remarks>The given model must have already been seen in the tree.</remarks>
|
|
public virtual Object GetParent(Object model) {
|
|
Branch br = this.TreeModel.GetBranch(model);
|
|
return br == null || br.ParentBranch == null ? null : br.ParentBranch.Model;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the collection of model objects that are the children of the
|
|
/// given model as they exist in the tree at the moment.
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This method returns the collection of children as the tree knows them. If the given
|
|
/// model has never been presented to the user (e.g. it belongs to a parent that has
|
|
/// never been expanded), then this method will return an empty collection.</para>
|
|
/// <para>
|
|
/// Because of this, if you want to traverse the whole tree, this is not the method to use.
|
|
/// It's better to traverse the your data model directly.
|
|
/// </para>
|
|
/// <para>
|
|
/// If the given model has not already been seen in the tree or
|
|
/// if it is not expandable, an empty collection will be returned.
|
|
/// </para>
|
|
/// </remarks>
|
|
public virtual IEnumerable GetChildren(Object model) {
|
|
Branch br = this.TreeModel.GetBranch(model);
|
|
if (br == null || !br.CanExpand)
|
|
return new ArrayList();
|
|
|
|
br.FetchChildren();
|
|
|
|
return br.Children;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Delegates
|
|
|
|
/// <summary>
|
|
/// Delegates of this type are use to decide if the given model object can be expanded
|
|
/// </summary>
|
|
/// <param name="model">The model under consideration</param>
|
|
/// <returns>Can the given model be expanded?</returns>
|
|
public delegate bool CanExpandGetterDelegate(Object model);
|
|
|
|
/// <summary>
|
|
/// Delegates of this type are used to fetch the children of the given model object
|
|
/// </summary>
|
|
/// <param name="model">The parent whose children should be fetched</param>
|
|
/// <returns>An enumerable over the children</returns>
|
|
public delegate IEnumerable ChildrenGetterDelegate(Object model);
|
|
|
|
/// <summary>
|
|
/// Delegates of this type are used to fetch the parent of the given model object.
|
|
/// </summary>
|
|
/// <param name="model">The child whose parent should be fetched</param>
|
|
/// <returns>The parent of the child or null if the child is a root</returns>
|
|
public delegate Object ParentGetterDelegate(Object model);
|
|
|
|
/// <summary>
|
|
/// Delegates of this type are used to create a new underlying Tree structure.
|
|
/// </summary>
|
|
/// <param name="view">The view for which the Tree is being created</param>
|
|
/// <returns>A subclass of Tree</returns>
|
|
public delegate Tree TreeFactoryDelegate(TreeListView view);
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
#region Implementation
|
|
|
|
/// <summary>
|
|
/// Handle a left button down event
|
|
/// </summary>
|
|
/// <param name="hti"></param>
|
|
/// <returns></returns>
|
|
protected override bool ProcessLButtonDown(OlvListViewHitTestInfo hti) {
|
|
// Did they click in the expander?
|
|
if (hti.HitTestLocation == HitTestLocation.ExpandButton) {
|
|
this.PossibleFinishCellEditing();
|
|
this.ToggleExpansion(hti.RowObject);
|
|
return true;
|
|
}
|
|
|
|
return base.ProcessLButtonDown(hti);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a OLVListItem for given row index
|
|
/// </summary>
|
|
/// <param name="itemIndex">The index of the row that is needed</param>
|
|
/// <returns>An OLVListItem</returns>
|
|
/// <remarks>This differs from the base method by also setting up the IndentCount property.</remarks>
|
|
public override OLVListItem MakeListViewItem(int itemIndex) {
|
|
OLVListItem olvItem = base.MakeListViewItem(itemIndex);
|
|
Branch br = this.TreeModel.GetBranch(olvItem.RowObject);
|
|
if (br != null)
|
|
olvItem.IndentCount = br.Level;
|
|
return olvItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reinitialize the Tree structure
|
|
/// </summary>
|
|
protected virtual void RegenerateTree() {
|
|
this.TreeModel = this.TreeFactory == null ? new Tree(this) : this.TreeFactory(this);
|
|
Trace.Assert(this.TreeModel != null);
|
|
this.VirtualListDataSource = this.TreeModel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recalculate the state of the checkboxes of all the items in the given list
|
|
/// and their ancestors.
|
|
/// </summary>
|
|
/// <remarks>This only makes sense when HierarchicalCheckboxes is true.</remarks>
|
|
/// <param name="toCheck"></param>
|
|
protected virtual void RecalculateHierarchicalCheckBoxGraph(IList toCheck) {
|
|
if (toCheck == null || toCheck.Count == 0)
|
|
return;
|
|
|
|
// Avoid recursive calculations
|
|
if (isRecalculatingHierarchicalCheckBox)
|
|
return;
|
|
|
|
try {
|
|
isRecalculatingHierarchicalCheckBox = true;
|
|
foreach (object ancestor in CalculateDistinctAncestors(toCheck))
|
|
this.RecalculateSingleHierarchicalCheckBox(ancestor);
|
|
}
|
|
finally {
|
|
isRecalculatingHierarchicalCheckBox = false;
|
|
}
|
|
|
|
}
|
|
private bool isRecalculatingHierarchicalCheckBox;
|
|
|
|
/// <summary>
|
|
/// Recalculate the hierarchy state of the given item and its ancestors
|
|
/// </summary>
|
|
/// <remarks>This only makes sense when HierarchicalCheckboxes is true.</remarks>
|
|
/// <param name="modelObject"></param>
|
|
protected virtual void RecalculateSingleHierarchicalCheckBox(object modelObject) {
|
|
|
|
if (modelObject == null)
|
|
return;
|
|
|
|
// Only branches have calculated check states. Leaf node checkedness is not calculated
|
|
if (!this.CanExpand(modelObject))
|
|
return;
|
|
|
|
// Set the checkedness of the given model based on the state of its children.
|
|
CheckState? aggregate = null;
|
|
foreach (object child in this.GetChildren(modelObject)) {
|
|
CheckState? checkedness = this.GetCheckState(child);
|
|
if (!checkedness.HasValue)
|
|
continue;
|
|
|
|
if (aggregate.HasValue) {
|
|
if (aggregate.Value != checkedness.Value) {
|
|
aggregate = CheckState.Indeterminate;
|
|
break;
|
|
}
|
|
} else
|
|
aggregate = checkedness;
|
|
}
|
|
|
|
base.SetObjectCheckedness(modelObject, aggregate ?? CheckState.Indeterminate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Yield the unique ancestors of the given collection of objects.
|
|
/// The order of the ancestors is guaranteed to be deeper objects first.
|
|
/// Roots will always be last.
|
|
/// </summary>
|
|
/// <param name="toCheck"></param>
|
|
/// <returns>Unique ancestors of the given objects</returns>
|
|
protected virtual IEnumerable CalculateDistinctAncestors(IList toCheck) {
|
|
|
|
if (toCheck.Count == 1) {
|
|
foreach (object ancestor in this.GetAncestors(toCheck[0])) {
|
|
yield return ancestor;
|
|
}
|
|
} else {
|
|
// WARNING - Clever code
|
|
|
|
// Example: Calculate ancestors of A, B, X and Y
|
|
// A and B are children of P, child of GP, child of Root
|
|
// X and Y are children of Q, child of GP, child of Root
|
|
|
|
// Build a list of all ancestors of all objects we need to check
|
|
ArrayList allAncestors = new ArrayList();
|
|
foreach (object child in toCheck) {
|
|
foreach (object ancestor in this.GetAncestors(child)) {
|
|
allAncestors.Add(ancestor);
|
|
}
|
|
}
|
|
|
|
// allAncestors = { P, GP, Root, P, GP, Root, Q, GP, Root, Q, GP, Root }
|
|
|
|
ArrayList uniqueAncestors = new ArrayList();
|
|
Dictionary<object, bool> alreadySeen = new Dictionary<object, bool>();
|
|
allAncestors.Reverse();
|
|
foreach (object ancestor in allAncestors) {
|
|
if (!alreadySeen.ContainsKey(ancestor)) {
|
|
alreadySeen[ancestor] = true;
|
|
uniqueAncestors.Add(ancestor);
|
|
}
|
|
}
|
|
|
|
// uniqueAncestors = { Root, GP, Q, P }
|
|
|
|
uniqueAncestors.Reverse();
|
|
foreach (object x in uniqueAncestors)
|
|
yield return x;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return all the ancestors of the given model
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This uses ParentGetter if possible.
|
|
/// </para>
|
|
/// <para>If the given model is a root OR if the model doesn't exist, the collection will be empty</para>
|
|
/// </remarks>
|
|
/// <param name="model">The model whose ancestors should be calculated</param>
|
|
/// <returns>Return a collection of ancestors of the given model.</returns>
|
|
protected virtual IEnumerable GetAncestors(object model) {
|
|
ParentGetterDelegate parentGetterDelegate = this.ParentGetter ?? this.GetParent;
|
|
|
|
object parent = parentGetterDelegate(model);
|
|
while (parent != null) {
|
|
yield return parent;
|
|
parent = parentGetterDelegate(parent);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
#region Event handlers
|
|
|
|
/// <summary>
|
|
/// The application is idle and a SelectionChanged event has been scheduled
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
protected override void HandleApplicationIdle(object sender, EventArgs e) {
|
|
base.HandleApplicationIdle(sender, e);
|
|
|
|
// There is an annoying redraw issue on ListViews that use indentation and
|
|
// that have full row select enabled. When the selection reduces to a subset
|
|
// of previously selected rows, or when the selection is extended using
|
|
// shift-pageup/down, then the space occupied by the indentation is not
|
|
// invalidated, and hence remains highlighted.
|
|
// Ideally we'd want to know exactly which rows were selected or deselected
|
|
// and then invalidate just the indentation region of those rows,
|
|
// but that's too much work. So just redraw the control.
|
|
// Actually... the selection issues show just slightly for non-full row select
|
|
// controls as well. So, always redraw the control after the selection
|
|
// changes.
|
|
this.Invalidate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decide if the given key event should be handled as a normal key input to the control?
|
|
/// </summary>
|
|
/// <param name="keyData"></param>
|
|
/// <returns></returns>
|
|
protected override bool IsInputKey(Keys keyData) {
|
|
// We want to handle Left and Right keys within the control
|
|
Keys key = keyData & Keys.KeyCode;
|
|
if (key == Keys.Left || key == Keys.Right)
|
|
return true;
|
|
|
|
return base.IsInputKey(keyData);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle the keyboard input to mimic a TreeView.
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
/// <returns>Was the key press handled?</returns>
|
|
protected override void OnKeyDown(KeyEventArgs e) {
|
|
OLVListItem focused = this.FocusedItem as OLVListItem;
|
|
if (focused == null) {
|
|
base.OnKeyDown(e);
|
|
return;
|
|
}
|
|
|
|
Object modelObject = focused.RowObject;
|
|
Branch br = this.TreeModel.GetBranch(modelObject);
|
|
|
|
switch (e.KeyCode) {
|
|
case Keys.Left:
|
|
// If the branch is expanded, collapse it. If it's collapsed,
|
|
// select the parent of the branch.
|
|
if (br.IsExpanded)
|
|
this.Collapse(modelObject);
|
|
else {
|
|
if (br.ParentBranch != null && br.ParentBranch.Model != null)
|
|
this.SelectObject(br.ParentBranch.Model, true);
|
|
}
|
|
e.Handled = true;
|
|
break;
|
|
|
|
case Keys.Right:
|
|
// If the branch is expanded, select the first child.
|
|
// If it isn't expanded and can be, expand it.
|
|
if (br.IsExpanded) {
|
|
List<Branch> filtered = br.FilteredChildBranches;
|
|
if (filtered.Count > 0)
|
|
this.SelectObject(filtered[0].Model, true);
|
|
} else {
|
|
if (br.CanExpand)
|
|
this.Expand(modelObject);
|
|
}
|
|
e.Handled = true;
|
|
break;
|
|
}
|
|
|
|
base.OnKeyDown(e);
|
|
}
|
|
|
|
#endregion
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Support classes
|
|
|
|
/// <summary>
|
|
/// A Tree object represents a tree structure data model that supports both
|
|
/// tree and flat list operations as well as fast access to branches.
|
|
/// </summary>
|
|
/// <remarks>If you create a subclass of Tree, you must install it in the TreeListView
|
|
/// via the TreeFactory delegate.</remarks>
|
|
public class Tree : IVirtualListDataSource, IFilterableDataSource
|
|
{
|
|
/// <summary>
|
|
/// Create a Tree
|
|
/// </summary>
|
|
/// <param name="treeView"></param>
|
|
public Tree(TreeListView treeView) {
|
|
this.treeView = treeView;
|
|
this.trunk = new Branch(null, this, null);
|
|
this.trunk.IsExpanded = true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Properties
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to decide if a model object can be expanded.
|
|
/// </summary>
|
|
public CanExpandGetterDelegate CanExpandGetter {
|
|
get { return canExpandGetter; }
|
|
set { canExpandGetter = value; }
|
|
}
|
|
private CanExpandGetterDelegate canExpandGetter;
|
|
|
|
/// <summary>
|
|
/// This is the delegate that will be used to fetch the children of a model object
|
|
/// </summary>
|
|
/// <remarks>This delegate will only be called if the CanExpand delegate has
|
|
/// returned true for the model object.</remarks>
|
|
public ChildrenGetterDelegate ChildrenGetter {
|
|
get { return childrenGetter; }
|
|
set { childrenGetter = value; }
|
|
}
|
|
private ChildrenGetterDelegate childrenGetter;
|
|
|
|
|
|
/// <summary>
|
|
/// Get or return the top level model objects in the tree
|
|
/// </summary>
|
|
public IEnumerable RootObjects {
|
|
get { return this.trunk.Children; }
|
|
set {
|
|
this.trunk.Children = value;
|
|
foreach (Branch br in this.trunk.ChildBranches)
|
|
br.RefreshChildren();
|
|
this.RebuildList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// What tree view is this Tree the model for?
|
|
/// </summary>
|
|
public TreeListView TreeView {
|
|
get { return this.treeView; }
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Commands
|
|
|
|
/// <summary>
|
|
/// Collapse the subtree underneath the given model
|
|
/// </summary>
|
|
/// <param name="model">The model to be collapsed. If the model isn't in the tree,
|
|
/// or if it is already collapsed, the command does nothing.</param>
|
|
/// <returns>The index of the model in flat list version of the tree</returns>
|
|
public virtual int Collapse(Object model) {
|
|
Branch br = this.GetBranch(model);
|
|
if (br == null || !br.IsExpanded)
|
|
return -1;
|
|
|
|
// Remember that the branch is collapsed, even if it's currently not visible
|
|
if (!br.Visible) {
|
|
br.Collapse();
|
|
return -1;
|
|
}
|
|
|
|
int count = br.NumberVisibleDescendents;
|
|
br.Collapse();
|
|
|
|
// Remove the visible descendents from after the branch itself
|
|
int index = this.GetObjectIndex(model);
|
|
this.objectList.RemoveRange(index + 1, count);
|
|
this.RebuildObjectMap(index + 1);
|
|
return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse all branches in this tree
|
|
/// </summary>
|
|
/// <returns>Nothing useful</returns>
|
|
public virtual int CollapseAll() {
|
|
this.trunk.CollapseAll();
|
|
this.RebuildList();
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand the subtree underneath the given model object
|
|
/// </summary>
|
|
/// <param name="model">The model to be expanded.</param>
|
|
/// <returns>The index of the model in flat list version of the tree</returns>
|
|
/// <remarks>
|
|
/// If the model isn't in the tree,
|
|
/// if it cannot be expanded or if it is already expanded, the command does nothing.
|
|
/// </remarks>
|
|
public virtual int Expand(Object model) {
|
|
Branch br = this.GetBranch(model);
|
|
if (br == null || !br.CanExpand || br.IsExpanded)
|
|
return -1;
|
|
|
|
// Remember that the branch is expanded, even if it's currently not visible
|
|
br.Expand();
|
|
if (!br.Visible)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
int index = this.GetObjectIndex(model);
|
|
this.InsertChildren(br, index + 1);
|
|
return index;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand all branches in this tree
|
|
/// </summary>
|
|
/// <returns>Return the index of the first branch that was expanded</returns>
|
|
public virtual int ExpandAll() {
|
|
this.trunk.ExpandAll();
|
|
this.Sort(this.lastSortColumn, this.lastSortOrder);
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the Branch object that represents the given model in the tree
|
|
/// </summary>
|
|
/// <param name="model">The model whose branches is to be returned</param>
|
|
/// <returns>The branch that represents the given model, or null if the model
|
|
/// isn't in the tree.</returns>
|
|
public virtual Branch GetBranch(object model) {
|
|
if (model == null)
|
|
return null;
|
|
|
|
Branch br;
|
|
this.mapObjectToBranch.TryGetValue(model, out br);
|
|
return br;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the number of visible descendents that are below the given model.
|
|
/// </summary>
|
|
/// <param name="model">The model whose descendent count is to be returned</param>
|
|
/// <returns>The number of visible descendents. 0 if the model doesn't exist or is collapsed</returns>
|
|
public virtual int GetVisibleDescendentCount(object model)
|
|
{
|
|
Branch br = this.GetBranch(model);
|
|
return br == null || !br.IsExpanded ? 0 : br.NumberVisibleDescendents;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebuild the children of the given model, refreshing any cached information held about the given object
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns>The index of the model in flat list version of the tree</returns>
|
|
public virtual int RebuildChildren(Object model) {
|
|
Branch br = this.GetBranch(model);
|
|
if (br == null || !br.Visible)
|
|
return -1;
|
|
|
|
int count = br.NumberVisibleDescendents;
|
|
|
|
// Remove the visible descendents from after the branch itself
|
|
int index = this.GetObjectIndex(model);
|
|
if (count > 0)
|
|
this.objectList.RemoveRange(index + 1, count);
|
|
|
|
// Refresh our knowledge of our children (do this even if CanExpand is false, because
|
|
// the branch have already collected some children and that information could be stale)
|
|
br.RefreshChildren();
|
|
|
|
// Insert the refreshed children if the branch can expand and is expanded
|
|
if (br.CanExpand && br.IsExpanded)
|
|
this.InsertChildren(br, index + 1);
|
|
return index;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Implementation
|
|
|
|
/// <summary>
|
|
/// Is the given model expanded?
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns></returns>
|
|
internal bool IsModelExpanded(object model) {
|
|
// Special case: model == null is the container for the roots. This is always expanded
|
|
if (model == null)
|
|
return true;
|
|
bool isExpanded;
|
|
this.mapObjectToExpanded.TryGetValue(model, out isExpanded);
|
|
return isExpanded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remember whether or not the given model was expanded
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <param name="isExpanded"></param>
|
|
internal void SetModelExpanded(object model, bool isExpanded) {
|
|
if (model == null) return;
|
|
|
|
if (isExpanded)
|
|
this.mapObjectToExpanded[model] = true;
|
|
else
|
|
this.mapObjectToExpanded.Remove(model);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Insert the children of the given branch into the given position
|
|
/// </summary>
|
|
/// <param name="br">The branch whose children should be inserted</param>
|
|
/// <param name="index">The index where the children should be inserted</param>
|
|
protected virtual void InsertChildren(Branch br, int index) {
|
|
// Expand the branch
|
|
br.Expand();
|
|
br.Sort(this.GetBranchComparer());
|
|
|
|
// Insert the branch's visible descendents after the branch itself
|
|
this.objectList.InsertRange(index, br.Flatten());
|
|
this.RebuildObjectMap(index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebuild our flat internal list of objects.
|
|
/// </summary>
|
|
protected virtual void RebuildList() {
|
|
this.objectList = ArrayList.Adapter(this.trunk.Flatten());
|
|
List<Branch> filtered = this.trunk.FilteredChildBranches;
|
|
if (filtered.Count > 0) {
|
|
filtered[0].IsFirstBranch = true;
|
|
filtered[0].IsOnlyBranch = (filtered.Count == 1);
|
|
}
|
|
this.RebuildObjectMap(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebuild our reverse index that maps an object to its location
|
|
/// in the filteredObjectList array.
|
|
/// </summary>
|
|
/// <param name="startIndex"></param>
|
|
protected virtual void RebuildObjectMap(int startIndex) {
|
|
if (startIndex == 0)
|
|
this.mapObjectToIndex.Clear();
|
|
for (int i = startIndex; i < this.objectList.Count; i++)
|
|
this.mapObjectToIndex[this.objectList[i]] = i;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new branch within this tree
|
|
/// </summary>
|
|
/// <param name="parent"></param>
|
|
/// <param name="model"></param>
|
|
/// <returns></returns>
|
|
internal Branch MakeBranch(Branch parent, object model) {
|
|
Branch br = new Branch(parent, this, model);
|
|
|
|
// Remember that the given branch is part of this tree.
|
|
this.mapObjectToBranch[model] = br;
|
|
return br;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
|
|
#region IVirtualListDataSource Members
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="n"></param>
|
|
/// <returns></returns>
|
|
public virtual object GetNthObject(int n) {
|
|
return this.objectList[n];
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public virtual int GetObjectCount() {
|
|
return this.trunk.NumberVisibleDescendents;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns></returns>
|
|
public virtual int GetObjectIndex(object model)
|
|
{
|
|
int index;
|
|
if (model != null && this.mapObjectToIndex.TryGetValue(model, out index))
|
|
return index;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="first"></param>
|
|
/// <param name="last"></param>
|
|
public virtual void PrepareCache(int first, int last) {
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
/// <param name="first"></param>
|
|
/// <param name="last"></param>
|
|
/// <param name="column"></param>
|
|
/// <returns></returns>
|
|
public virtual int SearchText(string value, int first, int last, OLVColumn column) {
|
|
return AbstractVirtualListDataSource.DefaultSearchText(value, first, last, column, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sort the tree on the given column and in the given order
|
|
/// </summary>
|
|
/// <param name="column"></param>
|
|
/// <param name="order"></param>
|
|
public virtual void Sort(OLVColumn column, SortOrder order) {
|
|
this.lastSortColumn = column;
|
|
this.lastSortOrder = order;
|
|
|
|
// TODO: Need to raise an AboutToSortEvent here
|
|
|
|
// Sorting is going to change the order of the branches so clear
|
|
// the "first branch" flag
|
|
foreach (Branch b in this.trunk.ChildBranches)
|
|
b.IsFirstBranch = false;
|
|
|
|
this.trunk.Sort(this.GetBranchComparer());
|
|
this.RebuildList();
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected virtual BranchComparer GetBranchComparer() {
|
|
if (this.lastSortColumn == null)
|
|
return null;
|
|
|
|
return new BranchComparer(new ModelObjectComparer(
|
|
this.lastSortColumn,
|
|
this.lastSortOrder,
|
|
this.treeView.SecondarySortColumn ?? this.treeView.GetColumn(0),
|
|
this.treeView.SecondarySortColumn == null ? this.lastSortOrder : this.treeView.SecondarySortOrder));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add the given collection of objects to the roots of this tree
|
|
/// </summary>
|
|
/// <param name="modelObjects"></param>
|
|
public virtual void AddObjects(ICollection modelObjects) {
|
|
ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, true);
|
|
foreach (Object x in modelObjects)
|
|
newRoots.Add(x);
|
|
this.SetObjects(newRoots);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all of the given objects from the roots of the tree.
|
|
/// Any objects that is not already in the roots collection is ignored.
|
|
/// </summary>
|
|
/// <param name="modelObjects"></param>
|
|
public virtual void RemoveObjects(ICollection modelObjects) {
|
|
ArrayList newRoots = new ArrayList();
|
|
foreach (Object x in this.treeView.Roots)
|
|
newRoots.Add(x);
|
|
foreach (Object x in modelObjects) {
|
|
newRoots.Remove(x);
|
|
this.mapObjectToIndex.Remove(x);
|
|
}
|
|
this.SetObjects(newRoots);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the roots of this tree to be the given collection
|
|
/// </summary>
|
|
/// <param name="collection"></param>
|
|
public virtual void SetObjects(IEnumerable collection) {
|
|
// We interpret a SetObjects() call as setting the roots of the tree
|
|
this.treeView.Roots = collection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update/replace the nth object with the given object
|
|
/// </summary>
|
|
/// <param name="index"></param>
|
|
/// <param name="modelObject"></param>
|
|
public void UpdateObject(int index, object modelObject) {
|
|
ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, false);
|
|
if (index < newRoots.Count)
|
|
newRoots[index] = modelObject;
|
|
SetObjects(newRoots);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IFilterableDataSource Members
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="mFilter"></param>
|
|
/// <param name="lFilter"></param>
|
|
public void ApplyFilters(IModelFilter mFilter, IListFilter lFilter) {
|
|
this.modelFilter = mFilter;
|
|
this.listFilter = lFilter;
|
|
this.RebuildList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is this list currently being filtered?
|
|
/// </summary>
|
|
internal bool IsFiltering {
|
|
get {
|
|
return this.treeView.UseFiltering && (this.modelFilter != null || this.listFilter != null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should the given model be included in this control?
|
|
/// </summary>
|
|
/// <param name="model">The model to consider</param>
|
|
/// <returns>True if it will be included</returns>
|
|
internal bool IncludeModel(object model) {
|
|
if (!this.treeView.UseFiltering)
|
|
return true;
|
|
|
|
if (this.modelFilter == null)
|
|
return true;
|
|
|
|
return this.modelFilter.Filter(model);
|
|
}
|
|
|
|
#endregion
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Private instance variables
|
|
|
|
private OLVColumn lastSortColumn;
|
|
private SortOrder lastSortOrder;
|
|
private readonly Dictionary<Object, Branch> mapObjectToBranch = new Dictionary<object, Branch>();
|
|
// ReSharper disable once InconsistentNaming
|
|
internal Dictionary<Object, bool> mapObjectToExpanded = new Dictionary<object, bool>();
|
|
private readonly Dictionary<Object, int> mapObjectToIndex = new Dictionary<object, int>();
|
|
private ArrayList objectList = new ArrayList();
|
|
private readonly TreeListView treeView;
|
|
private readonly Branch trunk;
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
// ReSharper disable once InconsistentNaming
|
|
protected IModelFilter modelFilter;
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
// ReSharper disable once InconsistentNaming
|
|
protected IListFilter listFilter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A Branch represents a sub-tree within a tree
|
|
/// </summary>
|
|
public class Branch
|
|
{
|
|
/// <summary>
|
|
/// Indicators for branches
|
|
/// </summary>
|
|
[Flags]
|
|
public enum BranchFlags
|
|
{
|
|
/// <summary>
|
|
/// FirstBranch of tree
|
|
/// </summary>
|
|
FirstBranch = 1,
|
|
|
|
/// <summary>
|
|
/// LastChild of parent
|
|
/// </summary>
|
|
LastChild = 2,
|
|
|
|
/// <summary>
|
|
/// OnlyBranch of tree
|
|
/// </summary>
|
|
OnlyBranch = 4
|
|
}
|
|
|
|
#region Life and death
|
|
|
|
/// <summary>
|
|
/// Create a Branch
|
|
/// </summary>
|
|
/// <param name="parent"></param>
|
|
/// <param name="tree"></param>
|
|
/// <param name="model"></param>
|
|
public Branch(Branch parent, Tree tree, Object model) {
|
|
this.ParentBranch = parent;
|
|
this.Tree = tree;
|
|
this.Model = model;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public properties
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Properties
|
|
|
|
/// <summary>
|
|
/// Get the ancestor branches of this branch, with the 'oldest' ancestor first.
|
|
/// </summary>
|
|
public virtual IList<Branch> Ancestors {
|
|
get {
|
|
List<Branch> ancestors = new List<Branch>();
|
|
if (this.ParentBranch != null)
|
|
this.ParentBranch.PushAncestors(ancestors);
|
|
return ancestors;
|
|
}
|
|
}
|
|
|
|
private void PushAncestors(IList<Branch> list) {
|
|
// This is designed to ignore the trunk (which has no parent)
|
|
if (this.ParentBranch != null) {
|
|
this.ParentBranch.PushAncestors(list);
|
|
list.Add(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can this branch be expanded?
|
|
/// </summary>
|
|
public virtual bool CanExpand {
|
|
get {
|
|
if (this.Tree.CanExpandGetter == null || this.Model == null)
|
|
return false;
|
|
|
|
return this.Tree.CanExpandGetter(this.Model);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets our children
|
|
/// </summary>
|
|
public List<Branch> ChildBranches {
|
|
get { return this.childBranches; }
|
|
set { this.childBranches = value; }
|
|
}
|
|
private List<Branch> childBranches = new List<Branch>();
|
|
|
|
/// <summary>
|
|
/// Get/set the model objects that are beneath this branch
|
|
/// </summary>
|
|
public virtual IEnumerable Children {
|
|
get {
|
|
ArrayList children = new ArrayList();
|
|
foreach (Branch x in this.ChildBranches)
|
|
children.Add(x.Model);
|
|
return children;
|
|
}
|
|
set {
|
|
this.ChildBranches.Clear();
|
|
|
|
TreeListView treeListView = this.Tree.TreeView;
|
|
CheckState? checkedness = null;
|
|
if (treeListView != null && treeListView.HierarchicalCheckboxes)
|
|
checkedness = treeListView.GetCheckState(this.Model);
|
|
foreach (Object x in value) {
|
|
this.AddChild(x);
|
|
|
|
// If the tree view is showing hierarchical checkboxes, then
|
|
// when a child object is first added, it has the same checkedness as this branch
|
|
if (checkedness.HasValue && checkedness.Value == CheckState.Checked)
|
|
treeListView.SetObjectCheckedness(x, checkedness.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddChild(object childModel) {
|
|
Branch br = this.Tree.GetBranch(childModel);
|
|
if (br == null)
|
|
br = this.Tree.MakeBranch(this, childModel);
|
|
else {
|
|
br.ParentBranch = this;
|
|
br.Model = childModel;
|
|
br.ClearCachedInfo();
|
|
}
|
|
this.ChildBranches.Add(br);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list of all the branches that survive filtering
|
|
/// </summary>
|
|
public List<Branch> FilteredChildBranches {
|
|
get {
|
|
if (!this.IsExpanded)
|
|
return new List<Branch>();
|
|
|
|
if (!this.Tree.IsFiltering)
|
|
return this.ChildBranches;
|
|
|
|
List<Branch> filtered = new List<Branch>();
|
|
foreach (Branch b in this.ChildBranches) {
|
|
if (this.Tree.IncludeModel(b.Model))
|
|
filtered.Add(b);
|
|
else {
|
|
// Also include this branch if it has any filtered branches (yes, its recursive)
|
|
if (b.FilteredChildBranches.Count > 0)
|
|
filtered.Add(b);
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or set whether this branch is expanded
|
|
/// </summary>
|
|
public bool IsExpanded {
|
|
get { return this.Tree.IsModelExpanded(this.Model); }
|
|
set { this.Tree.SetModelExpanded(this.Model, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if this branch is the first branch of the entire tree
|
|
/// </summary>
|
|
public virtual bool IsFirstBranch {
|
|
get {
|
|
return ((this.flags & Branch.BranchFlags.FirstBranch) != 0);
|
|
}
|
|
set {
|
|
if (value)
|
|
this.flags |= Branch.BranchFlags.FirstBranch;
|
|
else
|
|
this.flags &= ~Branch.BranchFlags.FirstBranch;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if this branch is the last child of its parent
|
|
/// </summary>
|
|
public virtual bool IsLastChild {
|
|
get {
|
|
return ((this.flags & Branch.BranchFlags.LastChild) != 0);
|
|
}
|
|
set {
|
|
if (value)
|
|
this.flags |= Branch.BranchFlags.LastChild;
|
|
else
|
|
this.flags &= ~Branch.BranchFlags.LastChild;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if this branch is the only top level branch
|
|
/// </summary>
|
|
public virtual bool IsOnlyBranch {
|
|
get {
|
|
return ((this.flags & Branch.BranchFlags.OnlyBranch) != 0);
|
|
}
|
|
set {
|
|
if (value)
|
|
this.flags |= Branch.BranchFlags.OnlyBranch;
|
|
else
|
|
this.flags &= ~Branch.BranchFlags.OnlyBranch;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the depth level of this branch
|
|
/// </summary>
|
|
public int Level {
|
|
get {
|
|
if (this.ParentBranch == null)
|
|
return 0;
|
|
|
|
return this.ParentBranch.Level + 1;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets which model is represented by this branch
|
|
/// </summary>
|
|
public Object Model {
|
|
get { return model; }
|
|
set { model = value; }
|
|
}
|
|
private Object model;
|
|
|
|
/// <summary>
|
|
/// Return the number of descendents of this branch that are currently visible
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public virtual int NumberVisibleDescendents {
|
|
get {
|
|
if (!this.IsExpanded)
|
|
return 0;
|
|
|
|
List<Branch> filtered = this.FilteredChildBranches;
|
|
int count = filtered.Count;
|
|
foreach (Branch br in filtered)
|
|
count += br.NumberVisibleDescendents;
|
|
return count;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets our parent branch
|
|
/// </summary>
|
|
public Branch ParentBranch {
|
|
get { return parentBranch; }
|
|
set { parentBranch = value; }
|
|
}
|
|
private Branch parentBranch;
|
|
|
|
/// <summary>
|
|
/// Gets or sets our overall tree
|
|
/// </summary>
|
|
public Tree Tree {
|
|
get { return tree; }
|
|
set { tree = value; }
|
|
}
|
|
private Tree tree;
|
|
|
|
/// <summary>
|
|
/// Is this branch currently visible? A branch is visible
|
|
/// if it has no parent (i.e. it's a root), or its parent
|
|
/// is visible and expanded.
|
|
/// </summary>
|
|
public virtual bool Visible {
|
|
get {
|
|
if (this.ParentBranch == null)
|
|
return true;
|
|
|
|
return this.ParentBranch.IsExpanded && this.ParentBranch.Visible;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Commands
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Commands
|
|
|
|
/// <summary>
|
|
/// Clear any cached information that this branch is holding
|
|
/// </summary>
|
|
public virtual void ClearCachedInfo() {
|
|
this.Children = new ArrayList();
|
|
this.alreadyHasChildren = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse this branch
|
|
/// </summary>
|
|
public virtual void Collapse() {
|
|
this.IsExpanded = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand this branch
|
|
/// </summary>
|
|
public virtual void Expand() {
|
|
if (this.CanExpand) {
|
|
this.IsExpanded = true;
|
|
this.FetchChildren();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand this branch recursively
|
|
/// </summary>
|
|
public virtual void ExpandAll() {
|
|
this.Expand();
|
|
foreach (Branch br in this.ChildBranches) {
|
|
if (br.CanExpand)
|
|
br.ExpandAll();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse all branches in this tree
|
|
/// </summary>
|
|
/// <returns>Nothing useful</returns>
|
|
public virtual void CollapseAll()
|
|
{
|
|
this.Collapse();
|
|
foreach (Branch br in this.ChildBranches) {
|
|
if (br.IsExpanded)
|
|
br.CollapseAll();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetch the children of this branch.
|
|
/// </summary>
|
|
/// <remarks>This should only be called when CanExpand is true.</remarks>
|
|
public virtual void FetchChildren() {
|
|
if (this.alreadyHasChildren)
|
|
return;
|
|
|
|
this.alreadyHasChildren = true;
|
|
|
|
if (this.Tree.ChildrenGetter == null)
|
|
return;
|
|
|
|
Cursor previous = Cursor.Current;
|
|
try {
|
|
if (this.Tree.TreeView.UseWaitCursorWhenExpanding)
|
|
Cursor.Current = Cursors.WaitCursor;
|
|
this.Children = this.Tree.ChildrenGetter(this.Model);
|
|
}
|
|
finally {
|
|
Cursor.Current = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapse the visible descendents of this branch into list of model objects
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public virtual IList Flatten() {
|
|
ArrayList flatList = new ArrayList();
|
|
if (this.IsExpanded)
|
|
this.FlattenOnto(flatList);
|
|
return flatList;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flatten this branch's visible descendents onto the given list.
|
|
/// </summary>
|
|
/// <param name="flatList"></param>
|
|
/// <remarks>The branch itself is <b>not</b> included in the list.</remarks>
|
|
public virtual void FlattenOnto(IList flatList) {
|
|
Branch lastBranch = null;
|
|
foreach (Branch br in this.FilteredChildBranches) {
|
|
lastBranch = br;
|
|
br.IsLastChild = false;
|
|
flatList.Add(br.Model);
|
|
if (br.IsExpanded) {
|
|
br.FetchChildren(); // make sure we have the branches children
|
|
br.FlattenOnto(flatList);
|
|
}
|
|
}
|
|
if (lastBranch != null)
|
|
lastBranch.IsLastChild = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force a refresh of all children recursively
|
|
/// </summary>
|
|
public virtual void RefreshChildren() {
|
|
|
|
// Forget any previous children. We always do this so that if
|
|
// IsExpanded or CanExpand have changed, we aren't left with stale information.
|
|
this.ClearCachedInfo();
|
|
|
|
if (!this.IsExpanded || !this.CanExpand)
|
|
return;
|
|
|
|
this.FetchChildren();
|
|
foreach (Branch br in this.ChildBranches)
|
|
br.RefreshChildren();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sort the sub-branches and their descendents so they are ordered according
|
|
/// to the given comparer.
|
|
/// </summary>
|
|
/// <param name="comparer">The comparer that orders the branches</param>
|
|
public virtual void Sort(BranchComparer comparer) {
|
|
if (this.ChildBranches.Count == 0)
|
|
return;
|
|
|
|
if (comparer != null)
|
|
this.ChildBranches.Sort(comparer);
|
|
|
|
foreach (Branch br in this.ChildBranches)
|
|
br.Sort(comparer);
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// Private instance variables
|
|
|
|
private bool alreadyHasChildren;
|
|
private BranchFlags flags;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class sorts branches according to how their respective model objects are sorted
|
|
/// </summary>
|
|
public class BranchComparer : IComparer<Branch>
|
|
{
|
|
/// <summary>
|
|
/// Create a BranchComparer
|
|
/// </summary>
|
|
/// <param name="actualComparer"></param>
|
|
public BranchComparer(IComparer actualComparer) {
|
|
this.actualComparer = actualComparer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Order the two branches
|
|
/// </summary>
|
|
/// <param name="x"></param>
|
|
/// <param name="y"></param>
|
|
/// <returns></returns>
|
|
public int Compare(Branch x, Branch y) {
|
|
return this.actualComparer.Compare(x.Model, y.Model);
|
|
}
|
|
|
|
private readonly IComparer actualComparer;
|
|
}
|
|
|
|
}
|
|
}
|