-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathSearchableTreeView.cs
247 lines (222 loc) · 10.5 KB
/
SearchableTreeView.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
using System;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace SearchableControls
{
/// <summary>
/// An extension of the Framework's TreeView control that allows the user to search for text in the control
/// by pressing CTRL-F or using the context menu.
/// </summary>
/// <remarks>
/// <para>To use, simply build the SearchableControls library and add a reference it in your project. The
/// SearchableTreeView control should appear in the SearchableControls tab of the Visual Studio toolbox.
/// Drag this control to your forms in the way you would a standard TreeView.</para>
///
/// <para>You may wish to give your forms an Edit/Find menu item with a specified shortcut of Ctrl-F.
/// This should call the OpenFindDialog() function of the main searchable control, or in the case of
/// multiple searchable controls, the focused control. You could provide the same option from toolbars.</para>
///
/// <para>By default this form will just search the Text field of each TreeViewNode. A mechanism exists to
/// provide a delegate to perform an alternative searching procedure.</para>
///
/// <para>As you can see the class is derived directly from TreeView so can do everything that the standard
/// TreeView can do.</para>
///</remarks>
public partial class SearchableTreeView : TreeView, ISearchable
{
/// <summary>
/// Delegate of node searching function.
/// </summary>
/// <param name="node">The TreeNode to search</param>
/// <param name="regularExpression">The regular expression to use to match text</param>
/// <returns>'True' if the treeNode is deemed to have matched</returns>
public delegate bool NodeSearchDelegate(TreeNode node, Regex regularExpression);
/// <summary>
/// Node searching function
/// </summary>
/// <remarks>
/// This is set to a search of the Text property of the treeNode, but can be overridden by the
/// client to provide custom search facilities of whatever the node conceptually contains, typically
/// by using the node's Tag value to link it to an object.
/// </remarks>
[DesignerSerializationVisibility(0)]
public NodeSearchDelegate NodeSearcher
{
get { return nodeSearcher; }
set { nodeSearcher = value; }
}
private NodeSearchDelegate nodeSearcher;
/// <summary>
/// Construct a SearchableTreeView treeview control
/// </summary>
public SearchableTreeView()
{
InitializeComponent();
nodeSearcher = new NodeSearchDelegate(DefaultNodeSearch); // create default search delegate
// Currently there is no designer support for adding menu item event handlers
findToolStripMenuItem.Click += new EventHandler(findToolStripMenuItem_Click);
}
/// <summary>
/// Find Text from context menu
/// </summary>
void findToolStripMenuItem_Click(object sender, EventArgs e)
{
findDialog1.Show();
}
/// <summary>
/// Handle key events. Used to provide find functions
/// </summary>
/// <param name="sender">Standard system parameter</param>
/// <param name="e">Standard system parameter</param>
protected void SearchableTreeView_KeyDown(object sender, KeyEventArgs e)
{
// Control F pressed, for 'Find'?
if (e.KeyCode == Keys.F && e.Modifiers == Keys.Control)
{
findDialog1.Show();
e.SuppressKeyPress = true; // don't pass the event down
}
// F3 pressed, for search again?
else if (e.KeyCode == Keys.F3 && e.Modifiers == Keys.None)
{
findDialog1.FindNext();
e.SuppressKeyPress = true; // don't pass the event down
}
}
/// <summary>
/// Default function to search a node
/// </summary>
/// <param name="treeNode">The TreeNode to search</param>
/// <param name="regularExpression">The regular expression to use to match text</param>
/// <returns>'True' if the treeNode is deemed to have matched</returns>
private bool DefaultNodeSearch(TreeNode treeNode, Regex regularExpression)
{
return regularExpression.IsMatch(treeNode.Text);
}
/// <summary>
/// The various modes that the recursive scan can be in
/// </summary>
private enum TreeSearchState
{
NotStarted,
Started,
MatchMade,
HitEndNode
}
/// <summary>
/// A recursive 'sub search' command is used to search part of the tree
/// </summary>
/// <param name="regularExpression">The regular expression to use to match text</param>
/// <param name="treeNodeCollection">The collection of nodes to search down from</param>
/// <param name="startAfterNode">The node that searching actually begins from
/// (otherwise just walk the tree until this node is found)</param>
/// <param name="stopAtNode">A node to terminate the search at</param>
/// <param name="state">Sends and returns the state of the recursive search</param>
private void SubSearch(Regex regularExpression, TreeNodeCollection treeNodeCollection, TreeNode startAfterNode, TreeNode stopAtNode, ref SearchableTreeView.TreeSearchState state)
{
foreach (TreeNode treeNode in treeNodeCollection)
{
if (state == SearchableTreeView.TreeSearchState.Started) // Has the search started?
{
if (treeNode == stopAtNode)
{
state = SearchableTreeView.TreeSearchState.HitEndNode;
return;
}
if (nodeSearcher(treeNode, regularExpression))
{
// We need to show search results even when the FindDialog is active
// This means turning off HideSelection if it is set.
// This unfortunately causes a slight flicker. One way to avoid this is to turn off HideSelection
// in individual control instances.
if (HideSelection)
{
// Ensure that the property is restored when the FindDialog is deactivated
findDialog1.Deactivate += new EventHandler(RestoreHideSelection);
HideSelection = false;
}
SelectedNode = treeNode;
SelectedNode.EnsureVisible(); // Make sure the result node is visible
state = SearchableTreeView.TreeSearchState.MatchMade;
return;
}
}
if (startAfterNode == treeNode)
{
state = SearchableTreeView.TreeSearchState.Started;
}
// sub search child nodes
SubSearch(regularExpression, treeNode.Nodes, startAfterNode, stopAtNode, ref state);
if (state == SearchableTreeView.TreeSearchState.HitEndNode)
{
return;
}
if (state == SearchableTreeView.TreeSearchState.MatchMade)
{
return;
}
}
}
/// <summary>
/// A record of the first node of a search series
/// </summary>
private TreeNode originalSelectionStart;
private void findDialog1_SearchRequested(object sender, SearchEventArgs e)
{
if (e.FirstSearch)
{
// Store the selection start position on the first search so that when all searches are complete
// this fact can be reported to the user in the find dialog.
originalSelectionStart = SelectedNode;
}
// First part of search is between item after current selection (inclusive) and the end of the
// document (exclusive), or the original search position position (exclusive) if this is greater
// than the current selection position
TreeNode searchFromBelow = SelectedNode;
SearchableTreeView.TreeSearchState treeSearchState = SearchableTreeView.TreeSearchState.NotStarted;
// A SubSearch function is used to search part of the tree
SubSearch(e.SearchRegularExpression, Nodes, searchFromBelow, originalSelectionStart, ref treeSearchState);
if (treeSearchState == SearchableTreeView.TreeSearchState.MatchMade)
{
e.Successful = true;// We have a match
}
else if (treeSearchState != SearchableTreeView.TreeSearchState.HitEndNode)
{
// No match? We hit end of document
// Retry from the beginning if the original start position is before or equal to the current selection
e.RestartedFromDocumentTop = true; // The user is informed that the search started from the top
treeSearchState = SearchableTreeView.TreeSearchState.Started;
// Search first half of the document
SubSearch(e.SearchRegularExpression, Nodes, null, originalSelectionStart, ref treeSearchState);
if (treeSearchState == SearchableTreeView.TreeSearchState.MatchMade)
{
e.Successful = true; // We have a match
}
}
}
/// <summary>
/// Put this control's HideSelection property back to normal when the FindDialog is deactivated
/// </summary>
/// <remarks>
/// This unfortunately causes a slight flicker. One way to avoid this is to turn off HideSelection
/// in individual control instances.
/// </remarks>
void RestoreHideSelection(object sender, EventArgs e)
{
HideSelection = true;
}
#region ISearchable Members
/// <summary>
/// Return the FindDialog associated with the control
/// </summary>
public FindDialog FindDialog
{
get
{
return findDialog1;
}
}
#endregion
}
}