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(); } } }