using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace FastReport.Controls
{
///
/// TreeView control with multiselect support.
///
///
/// This control is for internal use only.
///
[ToolboxItem(false)]
public class TreeViewMultiSelect : TreeView
{
private bool trackSelection;
private Timer labelEditTimer;
private List selectedNodes;
internal List SelectedNodes
{
get => selectedNodes;
set
{
UnpaintSelectedNodes();
selectedNodes.Clear();
selectedNodes.AddRange(value);
PaintSelectedNodes();
}
}
internal new TreeNode SelectedNode
{
get => SelectedNodes.Count == 0 ? null : SelectedNodes[0];
set
{
// do not use TreeView.SelectedNode. Simulate selection using TreeNode.BackColor.
// This way we have full control over selection and eliminate treeview's unexpected behaviour.
// However we also need to implement such things as invoke label edit, keyboard navigation
base.SelectedNode = null;
UnpaintSelectedNodes();
SelectedNodes.Clear();
if (value != null)
SelectedNodes.Add(value);
PaintSelectedNodes();
OnSelectionChanged();
}
}
internal void TrackSelection()
{
trackSelection = true;
base.SelectedNode = SelectedNode;
base.SelectedNode = null;
trackSelection = false;
}
internal event EventHandler SelectionChanged;
internal event MouseEventHandler RightMouseButtonClicked;
private void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty);
private void OnRightMouseButtonClicked(MouseEventArgs e) => RightMouseButtonClicked?.Invoke(this, e);
private List GetAllNodes(bool visibleOnly = false)
{
var allNodes = new List();
GetAllNodes(Nodes, allNodes, visibleOnly);
return allNodes;
}
private void GetAllNodes(TreeNodeCollection nodes, List allNodes, bool visibleOnly)
{
foreach (TreeNode node in nodes)
{
allNodes.Add(node);
if (node.IsExpanded || !visibleOnly)
GetAllNodes(node.Nodes, allNodes, visibleOnly);
}
}
private void PaintSelectedNodes()
{
Color backColor = SystemColors.Highlight;
Color foreColor = SystemColors.HighlightText;
foreach (TreeNode node in SelectedNodes)
{
node.BackColor = backColor;
node.ForeColor = foreColor;
}
}
private void UnpaintSelectedNodes()
{
foreach (TreeNode node in SelectedNodes)
{
node.BackColor = BackColor;
node.ForeColor = ForeColor;
}
}
private void EnsureItemSelected(TreeNode node)
{
if (node != null && !SelectedNodes.Contains(node))
{
SelectedNode = node;
}
}
private void StartLabelEditTimer()
{
if (labelEditTimer == null)
labelEditTimer = new Timer();
labelEditTimer.Interval = SystemInformation.DoubleClickTime;
labelEditTimer.Start();
labelEditTimer.Tick += (s, e) =>
{
StopLabelEditTimer();
SelectedNode?.BeginEdit();
};
}
private void StopLabelEditTimer()
{
if (labelEditTimer != null)
{
labelEditTimer.Stop();
labelEditTimer.Dispose();
labelEditTimer = null;
}
}
private void DoNodeClick(TreeNode node)
{
UnpaintSelectedNodes();
if (ModifierKeys == Keys.None)
{
// regular click (w/o Ctrl or Shift): select clicked node
SelectedNodes.Clear();
SelectedNodes.Add(node);
}
else if (ModifierKeys == Keys.Control)
{
// click with Ctrl: toggle node selection
if (SelectedNodes.Contains(node))
SelectedNodes.Remove(node);
else
SelectedNodes.Add(node);
// keep one node selected
if (SelectedNodes.Count == 0)
SelectedNodes.Add(node);
}
else if (ModifierKeys == Keys.Shift)
{
// click with Shift: select all nodes between the "start" node and clicked node
if (SelectedNodes.Count == 0)
{
// nothing selected yet?
SelectedNodes.Add(node);
}
else
{
// find the start and the end indexes
var allNodes = GetAllNodes();
// the start index is the index of the first selected node
int startIndex = allNodes.IndexOf(SelectedNodes[0]);
// the end index is the index of clicked node
int endIndex = allNodes.IndexOf(node);
SelectedNodes.Clear();
if (startIndex <= endIndex)
{
// direct selection
for (int i = startIndex; i <= endIndex; i++)
SelectedNodes.Add(allNodes[i]);
}
else if (endIndex < startIndex)
{
// reverse selection
for (int i = startIndex; i >= endIndex; i--)
SelectedNodes.Add(allNodes[i]);
}
}
}
PaintSelectedNodes();
}
///
protected override void OnBeforeSelect(TreeViewCancelEventArgs e)
{
if (trackSelection)
return;
// we don't use TreeView.SelectedNode. Prevent node selection
e.Cancel = true;
}
///
protected override void OnNodeMouseClick(TreeNodeMouseClickEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (ModifierKeys == Keys.None && SelectedNodes.Count == 1 && SelectedNode == e.Node)
{
// clicked on a single, already selected node: doubleclick or label edit
if (e.X >= e.Node.Bounds.X) // do we clicked on a node label?
StartLabelEditTimer();
}
else
{
// handle multiselection
DoNodeClick(e.Node);
OnSelectionChanged();
}
}
else if (e.Button == MouseButtons.Right)
{
// clicked not-yet-selected item: select it
EnsureItemSelected(e.Node);
OnRightMouseButtonClicked(e);
}
}
///
protected override void OnNodeMouseDoubleClick(TreeNodeMouseClickEventArgs e)
{
base.OnNodeMouseDoubleClick(e);
StopLabelEditTimer();
}
///
protected override void OnItemDrag(ItemDragEventArgs e)
{
// dragging not-yet-selected item: select it
EnsureItemSelected(e.Item as TreeNode);
base.OnItemDrag(e);
}
///
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (SelectedNode == null)
return;
var allNodes = GetAllNodes(true);
int index = allNodes.IndexOf(SelectedNode);
switch (e.KeyCode)
{
case Keys.Up:
SelectedNode = index > 0 ? allNodes[index - 1] : allNodes[0];
e.Handled = true;
break;
case Keys.Down:
SelectedNode = index < allNodes.Count - 1 ? allNodes[index + 1] : SelectedNode;
e.Handled = true;
break;
case Keys.Left:
if (SelectedNode.Nodes.Count > 0 && SelectedNode.IsExpanded)
SelectedNode.Collapse();
else
SelectedNode = SelectedNode.Parent ?? SelectedNode;
e.Handled = true;
break;
case Keys.Right:
if (SelectedNode.Nodes.Count > 0 && !SelectedNode.IsExpanded)
SelectedNode.Expand();
else if (SelectedNode.Nodes.Count > 0)
SelectedNode = SelectedNode.Nodes[0];
e.Handled = true;
break;
}
}
///
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
StopLabelEditTimer();
}
///
/// Creates a new instance of the TreeViewMultiSelect control.
///
public TreeViewMultiSelect()
{
selectedNodes = new List();
}
}
}