TreeViewMultiSelect.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Drawing;
  5. using System.Windows.Forms;
  6. namespace FastReport.Controls
  7. {
  8. /// <summary>
  9. /// TreeView control with multiselect support.
  10. /// </summary>
  11. /// <remarks>
  12. /// This control is for internal use only.
  13. /// </remarks>
  14. [ToolboxItem(false)]
  15. public class TreeViewMultiSelect : TreeView
  16. {
  17. private bool trackSelection;
  18. private Timer labelEditTimer;
  19. private List<TreeNode> selectedNodes;
  20. internal List<TreeNode> SelectedNodes
  21. {
  22. get => selectedNodes;
  23. set
  24. {
  25. UnpaintSelectedNodes();
  26. selectedNodes.Clear();
  27. selectedNodes.AddRange(value);
  28. PaintSelectedNodes();
  29. }
  30. }
  31. internal new TreeNode SelectedNode
  32. {
  33. get => SelectedNodes.Count == 0 ? null : SelectedNodes[0];
  34. set
  35. {
  36. // do not use TreeView.SelectedNode. Simulate selection using TreeNode.BackColor.
  37. // This way we have full control over selection and eliminate treeview's unexpected behaviour.
  38. // However we also need to implement such things as invoke label edit, keyboard navigation
  39. base.SelectedNode = null;
  40. UnpaintSelectedNodes();
  41. SelectedNodes.Clear();
  42. if (value != null)
  43. SelectedNodes.Add(value);
  44. PaintSelectedNodes();
  45. OnSelectionChanged();
  46. }
  47. }
  48. internal void TrackSelection()
  49. {
  50. trackSelection = true;
  51. base.SelectedNode = SelectedNode;
  52. base.SelectedNode = null;
  53. trackSelection = false;
  54. }
  55. internal event EventHandler SelectionChanged;
  56. internal event MouseEventHandler RightMouseButtonClicked;
  57. private void OnSelectionChanged() => SelectionChanged?.Invoke(this, EventArgs.Empty);
  58. private void OnRightMouseButtonClicked(MouseEventArgs e) => RightMouseButtonClicked?.Invoke(this, e);
  59. private List<TreeNode> GetAllNodes(bool visibleOnly = false)
  60. {
  61. var allNodes = new List<TreeNode>();
  62. GetAllNodes(Nodes, allNodes, visibleOnly);
  63. return allNodes;
  64. }
  65. private void GetAllNodes(TreeNodeCollection nodes, List<TreeNode> allNodes, bool visibleOnly)
  66. {
  67. foreach (TreeNode node in nodes)
  68. {
  69. allNodes.Add(node);
  70. if (node.IsExpanded || !visibleOnly)
  71. GetAllNodes(node.Nodes, allNodes, visibleOnly);
  72. }
  73. }
  74. private void PaintSelectedNodes()
  75. {
  76. Color backColor = SystemColors.Highlight;
  77. Color foreColor = SystemColors.HighlightText;
  78. foreach (TreeNode node in SelectedNodes)
  79. {
  80. node.BackColor = backColor;
  81. node.ForeColor = foreColor;
  82. }
  83. }
  84. private void UnpaintSelectedNodes()
  85. {
  86. foreach (TreeNode node in SelectedNodes)
  87. {
  88. node.BackColor = BackColor;
  89. node.ForeColor = ForeColor;
  90. }
  91. }
  92. private void EnsureItemSelected(TreeNode node)
  93. {
  94. if (node != null && !SelectedNodes.Contains(node))
  95. {
  96. SelectedNode = node;
  97. }
  98. }
  99. private void StartLabelEditTimer()
  100. {
  101. if (labelEditTimer == null)
  102. labelEditTimer = new Timer();
  103. labelEditTimer.Interval = SystemInformation.DoubleClickTime;
  104. labelEditTimer.Start();
  105. labelEditTimer.Tick += (s, e) =>
  106. {
  107. StopLabelEditTimer();
  108. SelectedNode?.BeginEdit();
  109. };
  110. }
  111. private void StopLabelEditTimer()
  112. {
  113. if (labelEditTimer != null)
  114. {
  115. labelEditTimer.Stop();
  116. labelEditTimer.Dispose();
  117. labelEditTimer = null;
  118. }
  119. }
  120. private void DoNodeClick(TreeNode node)
  121. {
  122. UnpaintSelectedNodes();
  123. if (ModifierKeys == Keys.None)
  124. {
  125. // regular click (w/o Ctrl or Shift): select clicked node
  126. SelectedNodes.Clear();
  127. SelectedNodes.Add(node);
  128. }
  129. else if (ModifierKeys == Keys.Control)
  130. {
  131. // click with Ctrl: toggle node selection
  132. if (SelectedNodes.Contains(node))
  133. SelectedNodes.Remove(node);
  134. else
  135. SelectedNodes.Add(node);
  136. // keep one node selected
  137. if (SelectedNodes.Count == 0)
  138. SelectedNodes.Add(node);
  139. }
  140. else if (ModifierKeys == Keys.Shift)
  141. {
  142. // click with Shift: select all nodes between the "start" node and clicked node
  143. if (SelectedNodes.Count == 0)
  144. {
  145. // nothing selected yet?
  146. SelectedNodes.Add(node);
  147. }
  148. else
  149. {
  150. // find the start and the end indexes
  151. var allNodes = GetAllNodes();
  152. // the start index is the index of the first selected node
  153. int startIndex = allNodes.IndexOf(SelectedNodes[0]);
  154. // the end index is the index of clicked node
  155. int endIndex = allNodes.IndexOf(node);
  156. SelectedNodes.Clear();
  157. if (startIndex <= endIndex)
  158. {
  159. // direct selection
  160. for (int i = startIndex; i <= endIndex; i++)
  161. SelectedNodes.Add(allNodes[i]);
  162. }
  163. else if (endIndex < startIndex)
  164. {
  165. // reverse selection
  166. for (int i = startIndex; i >= endIndex; i--)
  167. SelectedNodes.Add(allNodes[i]);
  168. }
  169. }
  170. }
  171. PaintSelectedNodes();
  172. }
  173. /// <inheritdoc/>
  174. protected override void OnBeforeSelect(TreeViewCancelEventArgs e)
  175. {
  176. if (trackSelection)
  177. return;
  178. // we don't use TreeView.SelectedNode. Prevent node selection
  179. e.Cancel = true;
  180. }
  181. /// <inheritdoc/>
  182. protected override void OnNodeMouseClick(TreeNodeMouseClickEventArgs e)
  183. {
  184. if (e.Button == MouseButtons.Left)
  185. {
  186. if (ModifierKeys == Keys.None && SelectedNodes.Count == 1 && SelectedNode == e.Node)
  187. {
  188. // clicked on a single, already selected node: doubleclick or label edit
  189. if (e.X >= e.Node.Bounds.X) // do we clicked on a node label?
  190. StartLabelEditTimer();
  191. }
  192. else
  193. {
  194. // handle multiselection
  195. DoNodeClick(e.Node);
  196. OnSelectionChanged();
  197. }
  198. }
  199. else if (e.Button == MouseButtons.Right)
  200. {
  201. // clicked not-yet-selected item: select it
  202. EnsureItemSelected(e.Node);
  203. OnRightMouseButtonClicked(e);
  204. }
  205. }
  206. /// <inheritdoc/>
  207. protected override void OnNodeMouseDoubleClick(TreeNodeMouseClickEventArgs e)
  208. {
  209. base.OnNodeMouseDoubleClick(e);
  210. StopLabelEditTimer();
  211. }
  212. /// <inheritdoc/>
  213. protected override void OnItemDrag(ItemDragEventArgs e)
  214. {
  215. // dragging not-yet-selected item: select it
  216. EnsureItemSelected(e.Item as TreeNode);
  217. base.OnItemDrag(e);
  218. }
  219. /// <inheritdoc/>
  220. protected override void OnKeyDown(KeyEventArgs e)
  221. {
  222. base.OnKeyDown(e);
  223. if (SelectedNode == null)
  224. return;
  225. var allNodes = GetAllNodes(true);
  226. int index = allNodes.IndexOf(SelectedNode);
  227. switch (e.KeyCode)
  228. {
  229. case Keys.Up:
  230. SelectedNode = index > 0 ? allNodes[index - 1] : allNodes[0];
  231. e.Handled = true;
  232. break;
  233. case Keys.Down:
  234. SelectedNode = index < allNodes.Count - 1 ? allNodes[index + 1] : SelectedNode;
  235. e.Handled = true;
  236. break;
  237. case Keys.Left:
  238. if (SelectedNode.Nodes.Count > 0 && SelectedNode.IsExpanded)
  239. SelectedNode.Collapse();
  240. else
  241. SelectedNode = SelectedNode.Parent ?? SelectedNode;
  242. e.Handled = true;
  243. break;
  244. case Keys.Right:
  245. if (SelectedNode.Nodes.Count > 0 && !SelectedNode.IsExpanded)
  246. SelectedNode.Expand();
  247. else if (SelectedNode.Nodes.Count > 0)
  248. SelectedNode = SelectedNode.Nodes[0];
  249. e.Handled = true;
  250. break;
  251. }
  252. }
  253. /// <inheritdoc/>
  254. protected override void Dispose(bool disposing)
  255. {
  256. base.Dispose(disposing);
  257. if (disposing)
  258. StopLabelEditTimer();
  259. }
  260. /// <summary>
  261. /// Creates a new instance of the TreeViewMultiSelect control.
  262. /// </summary>
  263. public TreeViewMultiSelect()
  264. {
  265. selectedNodes = new List<TreeNode>();
  266. }
  267. }
  268. }