UltimateZoneBuilder/Source/Core/Controls/MultiSelectTreeview.cs

1719 lines
49 KiB
C#
Raw Normal View History

#region ================== Copyright
// *****************************************************************************
//
// Copyright 2004, Coder's Lab
// All rights reserved. The software and associated documentation
// supplied hereunder are the proprietary information of Coder's Lab
// and are supplied subject to licence terms.
//
//
// You can use this control freely in your projects, but let me know if you
// are using it so I can add you to a list of references.
//
// Email: ludwig.stuyck@coders-lab.be
// Home page: http://www.coders-lab.be
//
// History
// 18/07/2004
// - Control creation
// 24/07/2004
// - Implemented rubberband selection; also combination keys work:
// ctrl, shift, ctrl+shift
// 25/08/2004
// - Rubberband selection temporary removed due to scrolling problems.
// - Renamed TreeViewSelectionMode property to SelectionMode.
// - Renamed SelectionModes enumeration to TreeViewSelectionMode.
// - Added MultiSelectSameParent selection mode.
// - Added keyboard functionality.
// - Enhanced selection drawing.
// - Added SelectionBackColor property.
// 02/09/2004
// - When shift/ctrl was pressed, treeview scrolled to last selected
// node. Fixed.
// - Moved TreeViewSelectionMode outside the TreeView class.
// - BeforeSelect was fired multiple times, AfterSelect was never
// fired. Fixed.
// - Collapsing/Expanding node changed selection. This does not happen
// anymore, except if a node that has selected descendants is
// collapsed; then all descendants are unselected and the collapsed
// node becomes selected.
// - If in the BeforeSelect event, e.Cancel is set to true, then node
// will not be selected
// - SHIFT selection sometimes didn<64>t behave correctly. Fixed.
// 04/09/2004
// - SelectedNodes is no longer an array of tree nodes, but a
// SelectedNodesCollection
// - In the AfterSelect event, the SelectedNodes contained two tree
// nodes; the old one and the new one. Fixed.
// 05/09/2004
// - Added Home, End, PgUp and PgDwn keys functionality
// 08/10/2004
// - SelectedNodeCollection renamed to NodeCollection
// - Fixes by GKM
//
// 18/8/2005
// - Added events BeforeDeselect and AfterDeselect
// 09/5/2007
// - Added an InvokeRequired check to Flashnode()
// 16/5/2007
// - Gave the document a consistant format
// - Created a new event 'SelectionsChanged'
//
// *****************************************************************************
#endregion
#region ================== Namespaces
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
#endregion
2016-04-27 09:13:07 +00:00
namespace CodeImp.DoomBuilder.Controls
{
#region TreeViewSelectionMode enumeration
/// <summary>
/// Selection mode for the treeview.
/// </summary>
/// <remarks>
/// The Selection mode determines how treeview nodes can be selected.
/// </remarks>
public enum TreeViewSelectionMode
{
/// <summary>
/// Only one node can be selected at a time.
/// </summary>
SingleSelect,
/// <summary>
/// Multiple nodes can be selected at the same time without restriction.
/// </summary>
MultiSelect,
/// <summary>
/// Multiple nodes that belong to the same root branch can be selected at the same time.
/// </summary>
MultiSelectSameRootBranch,
/// <summary>
/// Multiple nodes that belong to the same level can be selected at the same time.
/// </summary>
MultiSelectSameLevel,
/// <summary>
/// Multiple nodes that belong to the same level and same root branch can be selected at the same time.
/// </summary>
MultiSelectSameLevelAndRootBranch,
/// <summary>
/// Only nodes that belong to the same direct parent can be selected at the same time.
/// </summary>
MultiSelectSameParent
}
#endregion
#region Delegates
/// <summary>
/// Delegate used for tree node events.
/// </summary>
public delegate void TreeNodeEventHandler(TreeNode tn);
#endregion
/// <summary>
/// The TreeView control is a regular treeview with multi-selection capability.
/// </summary>
[ToolboxItem(true)]
public class MultiSelectTreeview : BufferedTreeView
{
public event TreeViewEventHandler AfterDeselect;
public event TreeViewEventHandler BeforeDeselect;
public event EventHandler SelectionsChanged;
protected void OnAfterDeselect(TreeNode tn)
{
if(AfterDeselect != null)
{
AfterDeselect(this, new TreeViewEventArgs(tn));
}
}
protected void OnBeforeDeselect(TreeNode tn)
{
if(BeforeDeselect != null)
{
BeforeDeselect(this, new TreeViewEventArgs(tn));
}
}
protected void OnSelectionsChanged()
{
if(blnSelectionChanged)
if(SelectionsChanged != null)
{
SelectionsChanged(this, new EventArgs());
}
}
#region Private variables
/// <summary>
/// Required designer variable.
/// </summary>
private Container components;
/// <summary>
/// Used to make sure that SelectedNode can only be used from within this class.
/// </summary>
private bool blnInternalCall;
/// <summary>
/// Hashtable that contains all selected nodes.
/// </summary>
private Hashtable htblSelectedNodes = new Hashtable();
/// <summary>
/// Track whether the total SelectedNodes changed across multiple operations
/// for SelectionsChanged event
/// </summary>
private bool blnSelectionChanged;
/// <summary>
/// Hashtable to preserve Node's original colors (colors can be set on the TreeView, or individual nodes)
/// (GKM)
/// </summary>
private Hashtable htblSelectedNodesOrigColors = new Hashtable();
/// <summary>
/// Keeps track of node that has to be pu in edit mode.
/// </summary>
private TreeNode tnNodeToStartEditOn;
/// <summary>
/// Remembers whether mouse click on a node was single or double click.
/// </summary>
private bool blnWasDoubleClick;
/// <summary>
/// Keeps track of most recent selected node.
/// </summary>
private TreeNode tnMostRecentSelectedNode;
/// <summary>
/// Keeps track of the selection mirror point; this is the last selected node without SHIFT key pressed.
/// It is used as the mirror node during SHIFT selection.
/// </summary>
private TreeNode tnSelectionMirrorPoint;
/// <summary>
/// Keeps track of the number of mouse clicks.
/// </summary>
private int intMouseClicks;
/// <summary>
/// Selection mode.
/// </summary>
private TreeViewSelectionMode selectionMode = TreeViewSelectionMode.SingleSelect;
/// <summary>
/// Backcolor for selected nodes.
/// </summary>
private Color selectionBackColor = SystemColors.Highlight;
/// <summary>
/// Keeps track whether a node click has been handled by the mouse down event. This is almost always the
/// case, except when a selected node has been clicked again. Then, it will not be handled in the mouse
/// down event because we might want to drag the node and if that's the case, node should not go in edit
/// mode.
/// </summary>
private bool blnNodeProcessedOnMouseDown;
/// <summary>
/// Holds node that needs to be flashed.
/// </summary>
private TreeNode tnToFlash;
/// <summary>
/// Keeps track of the first selected node when selection has begun with the keyboard.
/// </summary>
private TreeNode tnKeysStartNode;
#endregion
#region SelectedNode, SelectionMode, SelectionBackColor, SelectedNodes + events
/// <summary>
/// This property is for internal use only. Use SelectedNodes instead.
/// </summary>
public new TreeNode SelectedNode
{
get
{
if(!blnInternalCall)
{
throw new NotSupportedException("Use SelectedNodes instead of SelectedNode.");
}
else
{
return base.SelectedNode;
}
}
set
{
if(!blnInternalCall)
{
throw new NotSupportedException("Use SelectedNodes instead of SelectedNode.");
}
else
{
base.SelectedNode = value;
}
}
}
/// <summary>
/// Gets/sets selection mode.
/// </summary>
public TreeViewSelectionMode SelectionMode
{
get
{
return selectionMode;
}
set
{
selectionMode = value;
}
}
/// <summary>
/// Gets/sets backcolor for selected nodes.
/// </summary>
public Color SelectionBackColor
{
get
{
return selectionBackColor;
}
set
{
selectionBackColor = value;
}
}
/// <summary>
/// Gets selected nodes.
/// </summary>
public NodesCollection SelectedNodes
{
get
{
// Create a SelectedNodesCollection to return, and add event handlers to catch actions on it
NodesCollection selectedNodesCollection = new NodesCollection();
foreach(TreeNode tn in htblSelectedNodes.Values)
{
selectedNodesCollection.Add(tn);
}
selectedNodesCollection.TreeNodeAdded += new TreeNodeEventHandler(SelectedNodes_TreeNodeAdded);
selectedNodesCollection.TreeNodeInserted += new TreeNodeEventHandler(SelectedNodes_TreeNodeInserted);
selectedNodesCollection.TreeNodeRemoved += new TreeNodeEventHandler(SelectedNodes_TreeNodeRemoved);
selectedNodesCollection.SelectedNodesCleared += new EventHandler(SelectedNodes_SelectedNodesCleared);
return selectedNodesCollection;
}
}
/// <summary>
/// Occurs when a tree node is added to the SelectedNodes collection.
/// </summary>
/// <param name="tn">Tree node that was added.</param>
private void SelectedNodes_TreeNodeAdded(TreeNode tn)
{
blnSelectionChanged = false;
SelectNode(tn, true, TreeViewAction.Unknown);
//ProcessNodeRange(null, tn, new MouseEventArgs(MouseButtons.Left, 1, Cursor.Position.X, Cursor.Position.Y, 0), Keys.None, TreeViewAction.ByKeyboard, false);
OnSelectionsChanged();
}
/// <summary>
/// Occurs when a tree node is inserted to the SelectedNodes collection.
/// </summary>
/// <param name="tn">tree node that was inserted.</param>
private void SelectedNodes_TreeNodeInserted(TreeNode tn)
{
blnSelectionChanged = false;
SelectNode(tn, true, TreeViewAction.Unknown);
OnSelectionsChanged();
}
/// <summary>
/// Occurs when a tree node is removed from the SelectedNodes collection.
/// </summary>
/// <param name="tn">Tree node that was removed.</param>
private void SelectedNodes_TreeNodeRemoved(TreeNode tn)
{
blnSelectionChanged = false;
SelectNode(tn, false, TreeViewAction.Unknown);
OnSelectionsChanged();
}
/// <summary>
/// Occurs when the SelectedNodes collection was cleared.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SelectedNodes_SelectedNodesCleared(object sender, EventArgs e)
{
blnSelectionChanged = false;
UnselectAllNodes(TreeViewAction.Unknown);
OnSelectionsChanged();
}
#endregion
#region Node selection methods
/// <summary>
/// Unselects all selected nodes.
/// </summary>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectAllNodes(TreeViewAction tva)
{
UnselectAllNodesExceptNode(null, tva);
}
/// <summary>
/// Unselects all selected nodes that don't belong to the specified level.
/// </summary>
/// <param name="level">Node level.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectAllNodesNotBelongingToLevel(int level, TreeViewAction tva)
{
// First, build list of nodes that need to be unselected
List<TreeNode> arrNodesToDeselect = new List<TreeNode>(); //mxd
foreach(TreeNode selectedTreeNode in htblSelectedNodes.Values)
{
if(GetNodeLevel(selectedTreeNode) != level)
{
arrNodesToDeselect.Add(selectedTreeNode);
}
}
// Do the actual unselect
foreach(TreeNode tnToDeselect in arrNodesToDeselect)
{
SelectNode(tnToDeselect, false, tva);
}
}
/// <summary>
/// Unselects all selected nodes that don't belong directly to the specified parent.
/// </summary>
/// <param name="parent">Parent node.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectAllNodesNotBelongingDirectlyToParent(TreeNode parent, TreeViewAction tva)
{
// First, build list of nodes that need to be unselected
List<TreeNode> arrNodesToDeselect = new List<TreeNode>(); //mxd
foreach(TreeNode selectedTreeNode in htblSelectedNodes.Values)
{
if(selectedTreeNode.Parent != parent)
{
arrNodesToDeselect.Add(selectedTreeNode);
}
}
// Do the actual unselect
foreach(TreeNode tnToDeselect in arrNodesToDeselect)
{
SelectNode(tnToDeselect, false, tva);
}
}
/// <summary>
/// Unselects all selected nodes that don't belong directly or indirectly to the specified parent.
/// </summary>
/// <param name="parent">Parent node.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectAllNodesNotBelongingToParent(TreeNode parent, TreeViewAction tva)
{
// First, build list of nodes that need to be unselected
List<TreeNode> arrNodesToDeselect = new List<TreeNode>(); //mxd
foreach(TreeNode selectedTreeNode in htblSelectedNodes.Values)
{
if(!IsChildOf(selectedTreeNode, parent))
{
arrNodesToDeselect.Add(selectedTreeNode);
}
}
// Do the actual unselect
foreach(TreeNode tnToDeselect in arrNodesToDeselect)
{
SelectNode(tnToDeselect, false, tva);
}
}
/// <summary>
/// Unselects all selected nodes, except for the specified node which should not be touched.
/// </summary>
/// <param name="nodeKeepSelected">Node not to touch.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectAllNodesExceptNode(TreeNode nodeKeepSelected, TreeViewAction tva)
{
// First, build list of nodes that need to be unselected
List<TreeNode> arrNodesToDeselect = new List<TreeNode>(); //mxd
foreach(TreeNode selectedTreeNode in htblSelectedNodes.Values)
{
if(nodeKeepSelected == null)
{
arrNodesToDeselect.Add(selectedTreeNode);
}
else if((nodeKeepSelected != null) && (selectedTreeNode != nodeKeepSelected))
{
arrNodesToDeselect.Add(selectedTreeNode);
}
}
// Do the actual unselect
foreach(TreeNode tnToDeselect in arrNodesToDeselect)
{
SelectNode(tnToDeselect, false, tva);
}
}
/// <summary>
/// occurs when a node is about to be selected.
/// </summary>
/// <param name="e">TreeViewCancelEventArgs.</param>
protected override void OnBeforeSelect(TreeViewCancelEventArgs e)
{
// We don't want the base TreeView to handle the selection, because it can only handle single selection.
// Instead, we'll handle the selection ourselves by keeping track of the selected nodes and drawing the
// selection ourselves.
e.Cancel = true;
}
/// <summary>
/// Determines whether the specified node is selected or not.
/// </summary>
/// <param name="tn">Node to check.</param>
/// <returns>True if specified node is selected, false if not.</returns>
private bool IsNodeSelected(TreeNode tn)
{
if(tn != null)
return htblSelectedNodes.ContainsKey(tn.GetHashCode());
return false;
}
private void PreserveNodeColors(TreeNode tn)
{
if(tn == null) return;
if(htblSelectedNodesOrigColors.ContainsKey(tn.GetHashCode()))
{
// Color[] color = (Color[])htblSelectedNodesOrigColors[tn.GetHashCode()];
// color[0]=tn.BackColor;
// color[1]=tn.ForeColor;
}
else
{
htblSelectedNodesOrigColors.Add(tn.GetHashCode(), new[] { tn.BackColor, tn.ForeColor });
}
}
/// <summary>
/// (Un)selects the specified node.
/// </summary>
/// <param name="tn">Node to (un)select.</param>
/// <param name="select">True to select node, false to unselect node.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
/// <returns>True if node was selected, false if not.</returns>
private bool SelectNode(TreeNode tn, bool select, TreeViewAction tva)
{
bool blnSelected = false;
if(tn == null)
return false;
if(select)
{
// Only try to select node if it was not already selected
if(!IsNodeSelected(tn))
{
// Check if node selection is cancelled
TreeViewCancelEventArgs tvcea = new TreeViewCancelEventArgs(tn, false, tva);
base.OnBeforeSelect(tvcea);
if(tvcea.Cancel)
{
// This node selection was cancelled!
return false;
}
PreserveNodeColors(tn);
tn.BackColor = SelectionBackColor; // GKM moved from above
tn.ForeColor = BackColor; // GKM moved from above
htblSelectedNodes.Add(tn.GetHashCode(), tn);
blnSelected = true;
blnSelectionChanged = true;
base.OnAfterSelect(new TreeViewEventArgs(tn, tva));
}
tnMostRecentSelectedNode = tn;
}
else
{
// Only unselect node if it was selected
if(IsNodeSelected(tn))
{
OnBeforeDeselect(tn);
Color[] originalColors = (Color[])this.htblSelectedNodesOrigColors[tn.GetHashCode()];
if(originalColors != null)
{
htblSelectedNodes.Remove(tn.GetHashCode());
blnSelectionChanged = true;
htblSelectedNodesOrigColors.Remove(tn.GetHashCode());
// GKM - Restore original node colors
tn.BackColor = originalColors[0]; // GKM - was BackColor;
tn.ForeColor = originalColors[1]; // GKM - was ForeColor;
}
OnAfterDeselect(tn);
}
}
return blnSelected;
}
/// <summary>
/// Selects nodes within the specified range.
/// </summary>
/// <param name="startNode">Start node.</param>
/// <param name="endNode">End Node.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void SelectNodesInsideRange(TreeNode startNode, TreeNode endNode, TreeViewAction tva)
{
// Calculate start node and end node
TreeNode firstNode, lastNode;
if(startNode.Bounds.Y < endNode.Bounds.Y)
{
firstNode = startNode;
lastNode = endNode;
}
else
{
firstNode = endNode;
lastNode = startNode;
}
// Select each node in range
SelectNode(firstNode, true, tva);
TreeNode tnTemp = firstNode;
while(tnTemp != lastNode)
{
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
SelectNode(tnTemp, true, tva);
}
}
SelectNode(lastNode, true, tva);
}
/// <summary>
/// Unselects nodes outside the specified range.
/// </summary>
/// <param name="startNode">Start node.</param>
/// <param name="endNode">End node.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectNodesOutsideRange(TreeNode startNode, TreeNode endNode, TreeViewAction tva)
{
// Calculate start node and end node
TreeNode firstNode, lastNode;
if(startNode.Bounds.Y < endNode.Bounds.Y)
{
firstNode = startNode;
lastNode = endNode;
}
else
{
firstNode = endNode;
lastNode = startNode;
}
// Unselect each node outside range
TreeNode tnTemp = firstNode;
while(tnTemp != null)
{
tnTemp = tnTemp.PrevVisibleNode;
if(tnTemp != null)
{
SelectNode(tnTemp, false, tva);
}
}
tnTemp = lastNode;
while(tnTemp != null)
{
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
SelectNode(tnTemp, false, tva);
}
}
}
/// <summary>
/// Recursively unselect node.
/// </summary>
/// <param name="tn">Node to recursively unselect.</param>
/// <param name="tva">Specifies the action that caused the selection change.</param>
private void UnselectNodesRecursively(TreeNode tn, TreeViewAction tva)
{
SelectNode(tn, false, tva);
foreach(TreeNode child in tn.Nodes)
{
UnselectNodesRecursively(child, tva);
}
}
#endregion
#region Helper methods
/// <summary>
/// Determines whether a mouse click was inside the node bounds or outside the node bounds..
/// </summary>
/// <param name="tn">TreeNode to check.</param>
/// <param name="e">MouseEventArgs.</param>
/// <returns>True is mouse was clicked inside the node bounds, false if it was clicked ouside the node bounds.</returns>
private static bool IsClickOnNode(TreeNode tn, MouseEventArgs e)
{
if(tn == null) return false;
// GKM
// Determine the rightmost position we'll process clicks (so that the click has to be on the node's bounds,
// like the .NET treeview
int rightMostX = tn.Bounds.X + tn.Bounds.Width;
return (tn != null && e.X < rightMostX); // GKM
}
/// <summary>
/// Gets level of specified node.
/// </summary>
/// <param name="node">Node.</param>
/// <returns>Level of node.</returns>
public int GetNodeLevel(TreeNode node)
{
int level = 0;
while((node = node.Parent) != null)
level++;
return level;
}
/// <summary>
/// Determines whether the specified node is a child (indirect or direct) of the specified parent.
/// </summary>
/// <param name="child">Node to check.</param>
/// <param name="parent">Parent node.</param>
/// <returns>True if specified node is a direct or indirect child of parent node, false if not.</returns>
private static bool IsChildOf(TreeNode child, TreeNode parent)
{
bool blnChild = false;
TreeNode tnTemp = child;
while(tnTemp != null)
{
if(tnTemp == parent)
{
blnChild = true;
break;
}
else
{
tnTemp = tnTemp.Parent;
}
}
return blnChild;
}
/// <summary>
/// Gets root parent of specified node.
/// </summary>
/// <param name="child">Node.</param>
/// <returns>Root parent of specified node.</returns>
public TreeNode GetRootParent(TreeNode child)
{
TreeNode tnParent = child;
while(tnParent.Parent != null)
{
tnParent = tnParent.Parent;
}
return tnParent;
}
/// <summary>
/// Gets number of visible nodes.
/// </summary>
/// <returns>Number of visible nodes.</returns>
private int GetNumberOfVisibleNodes()
{
int intCounter = 0;
TreeNode tnTemp = this.Nodes[0];
while(tnTemp != null)
{
if(tnTemp.IsVisible)
{
intCounter++;
}
tnTemp = tnTemp.NextVisibleNode;
}
return intCounter;
}
/// <summary>
/// Gets last visible node.
/// </summary>
/// <returns>Last visible node.</returns>
private TreeNode GetLastVisibleNode()
{
TreeNode tnTemp = this.Nodes[0];
while(tnTemp.NextVisibleNode != null)
{
tnTemp = tnTemp.NextVisibleNode;
}
return tnTemp;
}
/// <summary>
/// Gets next tree node(s), starting from the specified node and direction.
/// </summary>
/// <param name="start">Node to start from.</param>
/// <param name="down">True to go down, false to go up.</param>
/// <param name="intNumber">Number of nodes to go down or up.</param>
/// <returns>Next node.</returns>
private static TreeNode GetNextTreeNode(TreeNode start, bool down, int intNumber)
{
int intCounter = 0;
TreeNode tnTemp = start;
while(intCounter < intNumber)
{
if(down)
{
if(tnTemp.NextVisibleNode != null)
tnTemp = tnTemp.NextVisibleNode;
else
break;
}
else
{
if(tnTemp.PrevVisibleNode != null)
tnTemp = tnTemp.PrevVisibleNode;
else
break;
}
intCounter++;
}
return tnTemp;
}
/// <summary>
/// makes focus rectangle visible or hides it.
/// </summary>
/// <param name="tn">Node to make focus rectangle (in)visible for.</param>
/// <param name="visible">True to make focus rectangle visible, false to hide it.</param>
private void SetFocusToNode(TreeNode tn, bool visible)
{
Graphics g = this.CreateGraphics();
Rectangle rect = new Rectangle(tn.Bounds.X, tn.Bounds.Y, tn.Bounds.Width, tn.Bounds.Height);
if(visible)
{
this.Invalidate(rect, false);
Update();
if(tn.BackColor != SelectionBackColor)
{
2015-05-27 10:34:25 +00:00
using(Pen p = new Pen(SelectionBackColor, 1)) g.DrawRectangle(p, rect);
}
}
else
{
if(tn.BackColor != SelectionBackColor)
{
using(Pen p = new Pen(BackColor, 1))
2015-05-27 10:34:25 +00:00
{
g.DrawRectangle(p, tnMostRecentSelectedNode.Bounds.X, tnMostRecentSelectedNode.Bounds.Y, tnMostRecentSelectedNode.Bounds.Width, tnMostRecentSelectedNode.Bounds.Height);
}
}
this.Invalidate(rect, false);
Update();
}
}
#endregion
#region Dispose
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if(disposing)
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
#endregion
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
DoubleBuffered = true; //mxd
SetStyle(ControlStyles.OptimizedDoubleBuffer, true); //mxd
}
#endregion
#region OnMouseUp, OnMouseDown
/// <summary>
/// Occurs when mouse button is up after a click.
/// </summary>
/// <param name="e"></param>
protected override void OnMouseUp(MouseEventArgs e)
{
#if DEBUG
try
{
#endif
if(!this.blnNodeProcessedOnMouseDown)
{
TreeNode tn = this.GetNodeAt(e.X, e.Y);
// Mouse click has not been handled by the mouse down event, so do it here. This is the case when
// a selected node was clicked again; in that case we handle that click here because in case the
// user is dragging the node, we should not put it in edit mode.
if(IsClickOnNode(tn, e))
{
this.ProcessNodeRange(this.tnMostRecentSelectedNode, tn, e, Control.ModifierKeys, TreeViewAction.ByMouse, true);
}
}
this.blnNodeProcessedOnMouseDown = false;
base.OnMouseUp(e);
#if DEBUG
}
catch(Exception ex)
{
// GKM - Untrapped exceptions were killing me for debugging purposes.
// It probably shouldn't be here permanently, but it was causing real trouble for me.
MessageBox.Show(this, ex.ToString());
}
#endif
}
private bool IsPlusMinusClicked(TreeNode tn, MouseEventArgs e)
{
int intNodeLevel = GetNodeLevel(tn);
bool blnPlusMinusClicked = e.X < 20 + (intNodeLevel * 20);
return blnPlusMinusClicked;
}
/// <summary>
/// Occurs when mouse is down.
/// </summary>
/// <param name="e"></param>
protected override void OnMouseDown(MouseEventArgs e)
{
tnKeysStartNode = null;
// Store number of mouse clicks in OnMouseDown event, because here we also get e.Clicks = 2 when an item was doubleclicked
// in OnMouseUp we seem to get always e.Clicks = 1, also when item is doubleclicked
intMouseClicks = e.Clicks;
TreeNode tn = this.GetNodeAt(e.X, e.Y);
if(tn == null) return;
// Preserve colors here, because if you do it later then it will already have selected colors
// Don't know why...!
PreserveNodeColors(tn);
// If +/- was clicked, we should not process the node.
if(!IsPlusMinusClicked(tn, e))
{
// If mouse down on a node that is already selected, then we should process this node in the mouse up event, because we
// might want to drag it and it should not be put in edit mode.
// Also, only process node if click was in node's bounds.
if((tn != null) && (IsClickOnNode(tn, e)) && (!IsNodeSelected(tn)))
{
// Flash node. In case the node selection is cancelled by the user, this gives the effect that it
// was selected and unselected again.
tnToFlash = tn;
System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(FlashNode));
t.Start();
blnNodeProcessedOnMouseDown = true;
ProcessNodeRange(tnMostRecentSelectedNode, tn, e, Control.ModifierKeys, TreeViewAction.ByMouse, true);
}
}
base.OnMouseDown(e);
}
#endregion
#region FlashNode, StartEdit
/// <summary>
/// Flashes node.
/// </summary>
private void FlashNode()
{
if(this.InvokeRequired)
{
this.Invoke(new MethodInvoker(FlashNode));
return;
}
TreeNode tn = tnToFlash;
// Only flash node is it's not yet selected
if(!IsNodeSelected(tn))
{
tn.BackColor = SelectionBackColor;
tn.ForeColor = this.BackColor;
this.Invalidate();
this.Refresh();
Application.DoEvents();
System.Threading.Thread.Sleep(200);
}
// If node is not selected yet, restore default colors to end flashing
if(!IsNodeSelected(tn))
{
tn.BackColor = BackColor;
tn.ForeColor = this.ForeColor;
}
}
/// <summary>
/// Starts edit on a node.
/// </summary>
private void StartEdit()
{
System.Threading.Thread.Sleep(200);
if(!blnWasDoubleClick)
{
blnInternalCall = true;
SelectedNode = tnNodeToStartEditOn;
blnInternalCall = false;
tnNodeToStartEditOn.BeginEdit();
}
else
{
blnWasDoubleClick = false;
}
}
#endregion
#region ProcessNodeRange
/// <summary>
/// Processes a node range.
/// </summary>
/// <param name="startNode">Start node of range.</param>
/// <param name="endNode">End node of range.</param>
/// <param name="e">MouseEventArgs.</param>
/// <param name="keys">Keys.</param>
/// <param name="tva">TreeViewAction.</param>
/// <param name="allowStartEdit">True if node can go to edit mode, false if not.</param>
private void ProcessNodeRange(TreeNode startNode, TreeNode endNode, MouseEventArgs e, Keys keys, TreeViewAction tva, bool allowStartEdit)
{
blnSelectionChanged = false; // prepare for OnSelectionsChanged
if(e.Button == MouseButtons.Left)
{
blnWasDoubleClick = (intMouseClicks == 2);
TreeNode tnTemp;
int intNodeLevelStart;
if(((keys & Keys.Control) == 0) && ((keys & Keys.Shift) == 0))
{
// CTRL and SHIFT not held down
tnSelectionMirrorPoint = endNode;
int intNumberOfSelectedNodes = SelectedNodes.Count;
// If it was a double click, select node and suspend further processing
if(blnWasDoubleClick)
{
base.OnMouseDown(e);
return;
}
if(!IsPlusMinusClicked(endNode, e))
{
bool blnNodeWasSelected = IsNodeSelected(endNode);
UnselectAllNodesExceptNode(endNode, tva);
SelectNode(endNode, true, tva);
if((blnNodeWasSelected) && (LabelEdit) && (allowStartEdit) && (!blnWasDoubleClick) && (intNumberOfSelectedNodes <= 1))
{
// Node should be put in edit mode
tnNodeToStartEditOn = endNode;
System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(StartEdit));
t.Start();
}
}
}
else if(((keys & Keys.Control) != 0) && ((keys & Keys.Shift) == 0))
{
// CTRL held down
tnSelectionMirrorPoint = null;
if(!IsNodeSelected(endNode))
{
switch(selectionMode)
{
case TreeViewSelectionMode.SingleSelect:
UnselectAllNodesExceptNode(endNode, tva);
break;
case TreeViewSelectionMode.MultiSelectSameRootBranch:
TreeNode tnAbsoluteParent2 = GetRootParent(endNode);
UnselectAllNodesNotBelongingToParent(tnAbsoluteParent2, tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevel:
UnselectAllNodesNotBelongingToLevel(GetNodeLevel(endNode), tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevelAndRootBranch:
TreeNode tnAbsoluteParent = GetRootParent(endNode);
UnselectAllNodesNotBelongingToParent(tnAbsoluteParent, tva);
UnselectAllNodesNotBelongingToLevel(GetNodeLevel(endNode), tva);
break;
case TreeViewSelectionMode.MultiSelectSameParent:
TreeNode tnParent = endNode.Parent;
UnselectAllNodesNotBelongingDirectlyToParent(tnParent, tva);
break;
}
SelectNode(endNode, true, tva);
}
else
{
SelectNode(endNode, false, tva);
}
}
else if(((keys & Keys.Control) == 0) && ((keys & Keys.Shift) != 0))
{
// SHIFT pressed
if(tnSelectionMirrorPoint == null)
{
tnSelectionMirrorPoint = startNode;
}
switch(selectionMode)
{
case TreeViewSelectionMode.SingleSelect:
UnselectAllNodesExceptNode(endNode, tva);
SelectNode(endNode, true, tva);
break;
case TreeViewSelectionMode.MultiSelectSameRootBranch:
TreeNode tnAbsoluteParentStartNode = GetRootParent(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
TreeNode tnAbsoluteParent = GetRootParent(tnTemp);
if(tnAbsoluteParent == tnAbsoluteParentStartNode)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToParent(tnAbsoluteParentStartNode, tva);
UnselectNodesOutsideRange(tnSelectionMirrorPoint, endNode, tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevel:
intNodeLevelStart = GetNodeLevel(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
int intNodeLevel = GetNodeLevel(tnTemp);
if(intNodeLevel == intNodeLevelStart)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToLevel(intNodeLevelStart, tva);
UnselectNodesOutsideRange(tnSelectionMirrorPoint, endNode, tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevelAndRootBranch:
TreeNode tnAbsoluteParentStart = GetRootParent(startNode);
intNodeLevelStart = GetNodeLevel(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
int intNodeLevel = GetNodeLevel(tnTemp);
TreeNode tnAbsoluteParent = GetRootParent(tnTemp);
if((intNodeLevel == intNodeLevelStart) && (tnAbsoluteParent == tnAbsoluteParentStart))
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToParent(tnAbsoluteParentStart, tva);
UnselectAllNodesNotBelongingToLevel(intNodeLevelStart, tva);
UnselectNodesOutsideRange(tnSelectionMirrorPoint, endNode, tva);
break;
case TreeViewSelectionMode.MultiSelect:
SelectNodesInsideRange(tnSelectionMirrorPoint, endNode, tva);
UnselectNodesOutsideRange(tnSelectionMirrorPoint, endNode, tva);
break;
case TreeViewSelectionMode.MultiSelectSameParent:
TreeNode tnParentStartNode = startNode.Parent;
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
TreeNode tnParent = tnTemp.Parent;
if(tnParent == tnParentStartNode)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingDirectlyToParent(tnParentStartNode, tva);
UnselectNodesOutsideRange(tnSelectionMirrorPoint, endNode, tva);
break;
}
}
else if(((keys & Keys.Control) != 0) && ((keys & Keys.Shift) != 0))
{
// SHIFT AND CTRL pressed
switch(selectionMode)
{
case TreeViewSelectionMode.SingleSelect:
UnselectAllNodesExceptNode(endNode, tva);
SelectNode(endNode, true, tva);
break;
case TreeViewSelectionMode.MultiSelectSameRootBranch:
TreeNode tnAbsoluteParentStartNode = GetRootParent(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
TreeNode tnAbsoluteParent = GetRootParent(tnTemp);
if(tnAbsoluteParent == tnAbsoluteParentStartNode)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToParent(tnAbsoluteParentStartNode, tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevel:
intNodeLevelStart = GetNodeLevel(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
int intNodeLevel = GetNodeLevel(tnTemp);
if(intNodeLevel == intNodeLevelStart)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToLevel(intNodeLevelStart, tva);
break;
case TreeViewSelectionMode.MultiSelectSameLevelAndRootBranch:
TreeNode tnAbsoluteParentStart = GetRootParent(startNode);
intNodeLevelStart = GetNodeLevel(startNode);
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
int intNodeLevel = GetNodeLevel(tnTemp);
TreeNode tnAbsoluteParent = GetRootParent(tnTemp);
if((intNodeLevel == intNodeLevelStart) && (tnAbsoluteParent == tnAbsoluteParentStart))
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingToParent(tnAbsoluteParentStart, tva);
UnselectAllNodesNotBelongingToLevel(intNodeLevelStart, tva);
break;
case TreeViewSelectionMode.MultiSelect:
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
SelectNode(tnTemp, true, tva);
}
}
break;
case TreeViewSelectionMode.MultiSelectSameParent:
TreeNode tnParentStartNode = startNode.Parent;
tnTemp = startNode;
// Check each visible node from startNode to endNode and select it if needed
while((tnTemp != null) && (tnTemp != endNode))
{
if(startNode.Bounds.Y > endNode.Bounds.Y)
tnTemp = tnTemp.PrevVisibleNode;
else
tnTemp = tnTemp.NextVisibleNode;
if(tnTemp != null)
{
TreeNode tnParent = tnTemp.Parent;
if(tnParent == tnParentStartNode)
{
SelectNode(tnTemp, true, tva);
}
}
}
UnselectAllNodesNotBelongingDirectlyToParent(tnParentStartNode, tva);
break;
}
}
}
else if(e.Button == MouseButtons.Right)
{
// if right mouse button clicked, clear selection and select right-clicked node
if(!IsNodeSelected(endNode))
{
UnselectAllNodes(tva);
SelectNode(endNode, true, tva);
}
}
OnSelectionsChanged();
}
#endregion
#region OnBeforeLabelEdit
/// <summary>
/// Occurs before node goes into edit mode.
/// </summary>
/// <param name="e"></param>
protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e)
{
blnSelectionChanged = false; // prepare for OnSelectionsChanged
// Make sure that it's the only selected node
SelectNode(e.Node, true, TreeViewAction.ByMouse);
UnselectAllNodesExceptNode(e.Node, TreeViewAction.ByMouse);
OnSelectionsChanged();
base.OnBeforeLabelEdit(e);
}
#endregion
#region OnKeyDown
/// <summary>
/// occurs when a key is down.
/// </summary>
/// <param name="e"></param>
protected override void OnKeyDown(KeyEventArgs e)
{
Keys kMod = Keys.None;
switch(e.Modifiers)
{
case Keys.Shift:
case Keys.Control:
case Keys.Control | Keys.Shift:
kMod = Keys.Shift;
if(tnKeysStartNode == null)
tnKeysStartNode = tnMostRecentSelectedNode;
break;
default:
tnKeysStartNode = null;
break;
}
int intNumber = 0;
TreeNode tnNewlySelectedNodeWithKeys = null;
switch(e.KeyCode)
{
case Keys.Down:
tnNewlySelectedNodeWithKeys = tnMostRecentSelectedNode.NextVisibleNode;
break;
case Keys.Up:
tnNewlySelectedNodeWithKeys = tnMostRecentSelectedNode.PrevVisibleNode;
break;
case Keys.Left:
if(tnMostRecentSelectedNode.IsExpanded)
tnMostRecentSelectedNode.Collapse();
else
tnNewlySelectedNodeWithKeys = tnMostRecentSelectedNode.Parent;
break;
case Keys.Right:
if(!tnMostRecentSelectedNode.IsExpanded)
tnMostRecentSelectedNode.Expand();
else
if(tnMostRecentSelectedNode.Nodes != null)
tnNewlySelectedNodeWithKeys = tnMostRecentSelectedNode.Nodes[0];
break;
case Keys.Home:
tnNewlySelectedNodeWithKeys = this.Nodes[0];
break;
case Keys.End:
tnNewlySelectedNodeWithKeys = GetLastVisibleNode();
break;
case Keys.PageDown:
intNumber = GetNumberOfVisibleNodes();
tnNewlySelectedNodeWithKeys = GetNextTreeNode(tnMostRecentSelectedNode, true, intNumber);
break;
case Keys.PageUp:
intNumber = GetNumberOfVisibleNodes();
tnNewlySelectedNodeWithKeys = GetNextTreeNode(tnMostRecentSelectedNode, false, intNumber);
break;
default:
base.OnKeyDown(e); // GKM
return;
}
if((tnNewlySelectedNodeWithKeys != null))
{
SetFocusToNode(tnMostRecentSelectedNode, false);
ProcessNodeRange(tnKeysStartNode, tnNewlySelectedNodeWithKeys, new MouseEventArgs(MouseButtons.Left, 1, Cursor.Position.X, Cursor.Position.Y, 0), kMod, TreeViewAction.ByKeyboard, false);
tnMostRecentSelectedNode = tnNewlySelectedNodeWithKeys;
SetFocusToNode(tnMostRecentSelectedNode, true);
}
// Ensure visibility
if(tnMostRecentSelectedNode != null)
{
TreeNode tnToMakeVisible = null;
switch(e.KeyCode)
{
case Keys.Down:
case Keys.Right:
tnToMakeVisible = GetNextTreeNode(tnMostRecentSelectedNode, true, 5);
break;
case Keys.Up:
case Keys.Left:
tnToMakeVisible = GetNextTreeNode(tnMostRecentSelectedNode, false, 5);
break;
case Keys.Home:
case Keys.End:
tnToMakeVisible = tnMostRecentSelectedNode;
break;
case Keys.PageDown:
tnToMakeVisible = GetNextTreeNode(tnMostRecentSelectedNode, true, intNumber - 2);
break;
case Keys.PageUp:
tnToMakeVisible = GetNextTreeNode(tnMostRecentSelectedNode, false, intNumber - 2);
break;
}
if(tnToMakeVisible != null)
tnToMakeVisible.EnsureVisible();
}
base.OnKeyDown(e);
}
#endregion
#region OnAfterCollapse
/// <summary>
/// Occurs after a node is collapsed.
/// </summary>
/// <param name="e"></param>
protected override void OnAfterCollapse(TreeViewEventArgs e)
{
blnSelectionChanged = false;
// All child nodes should be deselected
bool blnChildSelected = false;
foreach(TreeNode tn in e.Node.Nodes)
{
if(IsNodeSelected(tn))
{
blnChildSelected = true;
}
UnselectNodesRecursively(tn, TreeViewAction.Collapse);
}
if(blnChildSelected)
{
SelectNode(e.Node, true, TreeViewAction.Collapse);
}
OnSelectionsChanged();
base.OnAfterCollapse(e);
}
#endregion
#region OnItemDrag
/// <summary>
/// Occurs when an item is being dragged.
/// </summary>
/// <param name="e"></param>
protected override void OnItemDrag(ItemDragEventArgs e)
{
e = new ItemDragEventArgs(MouseButtons.Left, this.SelectedNodes);
base.OnItemDrag(e);
}
#endregion
}
#region SelectedNodesCollection
/// <summary>
/// Collection of selected nodes.
/// </summary>
public class NodesCollection : CollectionBase
{
#region Events
/// <summary>
/// Event fired when a tree node has been added to the collection.
/// </summary>
internal event TreeNodeEventHandler TreeNodeAdded;
/// <summary>
/// Event fired when a tree node has been removed to the collection.
/// </summary>
internal event TreeNodeEventHandler TreeNodeRemoved;
/// <summary>
/// Event fired when a tree node has been inserted to the collection.
/// </summary>
internal event TreeNodeEventHandler TreeNodeInserted;
/// <summary>
/// Event fired the collection has been cleared.
/// </summary>
internal event EventHandler SelectedNodesCleared;
#endregion
#region CollectionBase members
/// <summary>
/// Gets tree node at specified index.
/// </summary>
public TreeNode this[int index]
{
get { return ((TreeNode)List[index]); }
}
/// <summary>
/// Adds a tree node to the collection.
/// </summary>
/// <param name="treeNode">Tree node to add.</param>
/// <returns>The position into which the new element was inserted.</returns>
public int Add(TreeNode treeNode)
{
if(TreeNodeAdded != null)
TreeNodeAdded(treeNode);
return List.Add(treeNode);
}
/// <summary>
/// Inserts a tree node at specified index.
/// </summary>
/// <param name="index">The position into which the new element has to be inserted.</param>
/// <param name="treeNode">Tree node to insert.</param>
public void Insert(int index, TreeNode treeNode)
{
if(TreeNodeInserted != null)
TreeNodeInserted(treeNode);
List.Add(treeNode);
}
/// <summary>
/// Removed a tree node from the collection.
/// </summary>
/// <param name="treeNode">Tree node to remove.</param>
public void Remove(TreeNode treeNode)
{
if(TreeNodeRemoved != null)
TreeNodeRemoved(treeNode);
List.Remove(treeNode);
}
/// <summary>
/// Determines whether treenode belongs to the collection.
/// </summary>
/// <param name="treeNode">Tree node to check.</param>
/// <returns>True if tree node belongs to the collection, false if not.</returns>
public bool Contains(TreeNode treeNode)
{
return List.Contains(treeNode);
}
/// <summary>
/// Gets index of tree node in the collection.
/// </summary>
/// <param name="treeNode">Tree node to get index of.</param>
/// <returns>Index of tree node in the collection.</returns>
public int IndexOf(TreeNode treeNode)
{
return List.IndexOf(treeNode);
}
#endregion
#region OnClear
/// <summary>
/// Occurs when collection is being cleared.
/// </summary>
protected override void OnClear()
{
if(SelectedNodesCleared != null)
SelectedNodesCleared(this, EventArgs.Empty);
base.OnClear();
}
#endregion
}
#endregion
}