UltimateZoneBuilder/Source/Core/Controls/MultiSelectTreeview.cs
2017-01-15 00:35:40 +02:00

1718 lines
49 KiB
C#
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 didnt 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
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)
{
using(Pen p = new Pen(SelectionBackColor, 1)) g.DrawRectangle(p, rect);
}
}
else
{
if(tn.BackColor != SelectionBackColor)
{
using(Pen p = new Pen(BackColor, 1))
{
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
}