Source code: com/port80/eclipse/util/IncrementalFindTreeTarget.java
1 package com.port80.eclipse.util;
2
3 import java.text.MessageFormat;
4 import java.util.ArrayList;
5 import java.util.List;
6 import java.util.Stack;
7
8 import org.eclipse.jface.action.IStatusLineManager;
9 import org.eclipse.jface.viewers.AbstractTreeViewer;
10 import org.eclipse.jface.viewers.IBaseLabelProvider;
11 import org.eclipse.jface.viewers.IContentProvider;
12 import org.eclipse.jface.viewers.ILabelProvider;
13 import org.eclipse.jface.viewers.ITreeContentProvider;
14 import org.eclipse.jface.viewers.StructuredSelection;
15 import org.eclipse.jface.viewers.ViewerSorter;
16 import org.eclipse.swt.SWT;
17 import org.eclipse.swt.events.FocusEvent;
18 import org.eclipse.swt.events.FocusListener;
19 import org.eclipse.swt.events.KeyEvent;
20 import org.eclipse.swt.events.KeyListener;
21 import org.eclipse.swt.events.MouseEvent;
22 import org.eclipse.swt.events.MouseListener;
23 import org.eclipse.swt.widgets.Control;
24
25 /**
26 * An incremental find target for StructuredViewer with an ILabelProvider. Replace is always disabled.
27 *
28 * @since 2.0
29 * @author chrisl
30 */
31 public class IncrementalFindTreeTarget implements IIncrementalFindTreeTarget, KeyListener, MouseListener, FocusListener
32 //, ITextListener
33 {
34
35 ////////////////////////////////////////////////////////////////////////
36
37 private static final String NAME = "StructuredIncrementalFindTarget";
38 /** The string representing rendered tab */
39 private final static String TAB = UtilPlugin.getString("FindIncremental.render.tab"); //$NON-NLS-1$
40 private final static String PATTERN_NOTFOUND = UtilPlugin.getString("FindIncremental.pattern.not_found"); //$NON-NLS-1$
41 private final static String PATTERN_FOUND = UtilPlugin.getString("FindIncremental.pattern.found"); //$NON-NLS-1$
42 private final static String PROMPT = UtilPlugin.getString("FindIncremental.prompt"); //$NON-NLS-1$
43 /** A constant representing a find-next operation */
44 private static final int NEXT = 1;
45 /** A constant representing a find-previous operation */
46 private static final int PREVIOUS = 2;
47 /** A constant representing adding a character to the find pattern */
48 private static final int CHAR = 3;
49 /** A constant representing a wrap operation */
50 private static final Object WRAPPED = new Object();
51 private static final boolean DEBUG = false;
52
53 private static String fSavedString = "";
54
55 ////////////////////////////////////////////////////////////////////////
56
57 /** The text viewer to operate on */
58 private final AbstractTreeViewer fViewer;
59 /** The status line manager for output */
60 private final IStatusLineManager fStatusLine;
61 /** The find replace target to delegate find requests */
62 //private final IFindReplaceTarget fTarget;
63
64 /** The current find string */
65 private StringBuffer fFindString = new StringBuffer();
66 /** Index of first upper case char. in the find string. -1 if no upper case.*/
67 private int fCasePosition;
68 /** The position to start the find from */
69 private int fBasePosition;
70 /** The position of the last successful find */
71 private int fCurrentIndex;
72 /** A flag indicating if last find was successful */
73 private boolean fFound;
74 private boolean fForward;
75 /** A flag indicating listeners are installed. */
76 private boolean fInstalled;
77 private Object[] fElements;
78 private ILabelProvider fLabelProvider;
79 private ITreeContentProvider fContentProvider;
80 private KeyListener fPrevKeyListener;
81 /** Index stack to backtrace when taking back characters.*/
82 private Stack fStack;
83
84 ////////////////////////////////////////////////////////////////////////
85
86 /**
87 * Creates an instance of an incremental find target.
88 *
89 * @param viewer the text viewer to operate on
90 * @param manager the status line manager for output
91 */
92 public IncrementalFindTreeTarget(AbstractTreeViewer viewer, IStatusLineManager manager, KeyListener prev) {
93 fViewer = viewer;
94 fStatusLine = manager;
95 fPrevKeyListener = prev;
96 fStack = new Stack();
97 }
98
99 // IIncrementalFindTarget interface ////////////////////////////////////
100 //
101
102 /*
103 * @see IFindReplaceTarget#canPerformFind()
104 */
105 public boolean canPerformFind() {
106 return true;
107 }
108
109 public boolean isEditable() {
110 return false;
111 }
112
113 public StructuredSelection getSelection() {
114 return (StructuredSelection) fViewer.getSelection();
115 }
116
117 /** Get text representation of the currently selected item.
118 */
119 public String getSelectionText() {
120 StructuredSelection selection = getSelection();
121 if (selection == null)
122 return null;
123 if (selection.isEmpty())
124 return null;
125 return fLabelProvider.getText(selection.getFirstElement());
126 }
127
128 /*
129 */
130 public void setSelection(Object object) {
131 fViewer.setSelection(new StructuredSelection(object), true);
132 //fViewer.refresh();
133 }
134
135 /** Find item with the given string and select it.
136 *
137 * @return The index of the selected item or -1.
138 * @param start The index to start search from inclusively.
139 * @param findString The target string.
140 * @param forward
141 * @param caseSensitive
142 */
143 public int findAndSelect(int start, String findString, boolean forward, boolean caseSensitive) {
144 int index = -1;
145 if (!caseSensitive)
146 findString = findString.toLowerCase();
147 int step = forward ? 1 : -1;
148 if (!forward && start >= 0)
149 --start;
150 for (int i = start; i >= 0 && i < fElements.length; i += step) {
151 String s = fLabelProvider.getText(fElements[i]);
152 if (!caseSensitive)
153 s = s.toLowerCase();
154 if (s.indexOf(findString) != -1) {
155 index = i;
156 break;
157 }
158 }
159 if (index != -1)
160 setSelection(fElements[index]);
161 return index;
162 }
163
164 /*
165 * @see IFindReplaceTargetExtension#beginSession()
166 */
167 public void beginSession(boolean forward, String defaultString) {
168 if (DEBUG)
169 msgDebug(".beginSession()");
170 IContentProvider cp = fViewer.getContentProvider();
171 if (cp instanceof ITreeContentProvider)
172 fContentProvider = (ITreeContentProvider) cp;
173 else
174 return;
175 IBaseLabelProvider p = fViewer.getLabelProvider();
176 if (p instanceof ILabelProvider)
177 fLabelProvider = (ILabelProvider) p;
178 else
179 return;
180 install();
181 //
182 fForward = forward;
183 if (defaultString != null)
184 fSavedString = defaultString;
185 fElements =
186 getTreeElements(new Object[] { fViewer.getInput()}, fViewer.getSorter(), new ArrayList())
187 .toArray();
188 // getTreeElements(fContentProvider.getElements(null), fViewer.getSorter(), new ArrayList())
189 // .toArray();
190 if (fElements.length == 0) {
191 UtilPlugin.log(NAME + ".beginSession(): no element found.");
192 return;
193 }
194 fCasePosition = -1;
195 if (DEBUG)
196 msgDebug(".beginSession(): " + fElements.length + ", saved=" + fSavedString);
197 fBasePosition = 0;
198 StructuredSelection selection = (StructuredSelection) fViewer.getSelection();
199 if (selection != null) {
200 Object a = selection.getFirstElement();
201 if (a != null) {
202 for (int i = 0; i < fElements.length; ++i) {
203 if (a == fElements[i]) {
204 fBasePosition = i;
205 break;
206 }
207 }
208 }
209 }
210 fCurrentIndex = fBasePosition;
211 fFound = true;
212 statusMessage(PROMPT);
213 }
214
215 /*
216 * @see IFindReplaceTargetExtension#endSession()
217 */
218 public void endSession() {
219 if (DEBUG)
220 msgDebug(".endSession()");
221 if (fInstalled) {
222 if (fFindString.length() > 0)
223 fSavedString = fFindString.toString();
224 uninstall();
225 }
226 fFindString.setLength(0);
227 statusClear();
228 }
229
230 ////////////////////////////////////////////////////////////////////////
231
232 /**
233 * Installs this target. I.e. adds all required listeners.
234 */
235 private void install() {
236 if (fInstalled)
237 return;
238 Control control = fViewer.getControl();
239 if (fPrevKeyListener != null)
240 control.removeKeyListener(fPrevKeyListener);
241 control.addKeyListener(this);
242 control.addMouseListener(this);
243 control.addFocusListener(this);
244 fInstalled = true;
245 }
246
247 /**
248 * Uninstalls itself. I.e. removes all listeners installed in <code>install</code>.
249 */
250 private void uninstall() {
251 if (!fInstalled)
252 return;
253 Control control = fViewer.getControl();
254 //control.removeFocusListener(this);
255 control.removeMouseListener(this);
256 control.removeKeyListener(this);
257 if (fPrevKeyListener != null)
258 control.addKeyListener(fPrevKeyListener);
259 fInstalled = false;
260 }
261
262 ////////////////////////////////////////////////////////////////////////
263
264 /**
265 * Returns whether the find string can be found using the given options.
266 *
267 * @param forward the search direction
268 * @param position the start offset
269 * @param wrapSearch should the search wrap to start/end if end/start is reached
270 * @param takeBack is the find string shortend
271 * @return <code>true</code> if find string can be found using the options
272 */
273 private boolean performFindNext(boolean forward, int start, boolean wrapSearch, boolean takeBack) {
274 String string = fFindString.toString();
275 if (string.length() == 0)
276 string = fSavedString;
277 if (string.length() == 0) {
278 fCurrentIndex = start;
279 return false;
280 }
281 // If current index is not valid, start from start.
282 if (start < 0 || start >= fElements.length)
283 start = 0;
284 int index = findIndex(string, start, forward, fCasePosition != -1, wrapSearch, takeBack);
285 if (index != -1) {
286 fCurrentIndex = index;
287 fFound = true;
288 } else {
289 fFound = false;
290 }
291 if (DEBUG)
292 System.out.println(
293 NAME
294 + ".performFindNext(): forward="
295 + forward
296 + ", string="
297 + string
298 + ", found="
299 + fFound
300 + ", match="
301 + getSelectionText());
302 return fFound;
303 }
304
305 /**
306 * Retuns the offset at which the search string is found next using the given options
307 *
308 * @param findString the string to search for
309 * @param startPosition the start offset
310 * @param forward the search direction
311 * @param caseSensitive is the search case sensitive
312 * @param wrapSearch should the search wrap to start/end if end/start is reached
313 * @param takeBack is the find string shortend
314 * @return the offset of the next match or <code>-1</code>
315 */
316 private int findIndex(
317 String findString,
318 int startPosition,
319 boolean forward,
320 boolean caseSensitive,
321 boolean wrapSearch,
322 boolean takeBack) {
323
324 int index = findAndSelect(startPosition, findString, forward, caseSensitive);
325 if (index != -1)
326 return index;
327 // Not found, beep once
328 if (!takeBack)
329 beep();
330 if (!wrapSearch)
331 return -1;
332 // Before wrap search, stop once.
333 if (fFound)
334 return -1;
335 // Wrap search
336 startPosition = forward ? 0 : fElements.length - 1;
337 index = findAndSelect(startPosition, findString, forward, caseSensitive);
338 return index;
339 }
340
341 /**
342 * Updates the status line appropriate for the indicated success.
343 * @param found the success
344 */
345 private void updateStatus(boolean found) {
346 String string = fFindString.toString();
347 String prefix = ""; //$NON-NLS-1$
348 if (!found) {
349 statusError(MessageFormat.format(PATTERN_NOTFOUND, new Object[] { prefix, string }));
350 } else {
351 statusMessage(MessageFormat.format(PATTERN_FOUND, new Object[] { prefix, string }));
352 }
353 }
354
355 ////////////////////////////////////////////////////////////////////////
356
357 /**
358 * Perform increment find. All non-printable characters need to be escaped.
359 * TODO: escape non-printable characters.
360 *
361 * @see KeyListener#keyPressed(Event)
362 */
363 public void keyPressed(KeyEvent event) {
364 //NOTE:
365 // event.character=='' and event.keycode==0 for most keys and key combinations
366 // not named in SWT. eg. no keycode for keypad keys, window key ... etc!!!
367 if (DEBUG)
368 System.err.println(
369 NAME
370 + ".keyPressed(): state="
371 + event.stateMask
372 + ", char="
373 + event.character
374 + "keycode="
375 + event.keyCode);
376 if (event.stateMask == 0 && event.character == 0) {
377 switch (event.keyCode) {
378 case SWT.HOME :
379 fCurrentIndex = 0;
380 setSelection(fElements[0]);
381 break;
382 case SWT.END :
383 fCurrentIndex = fElements.length - 1;
384 setSelection(fElements[fCurrentIndex]);
385 break;
386 case SWT.ALT :
387 case SWT.ARROW_LEFT :
388 case SWT.ARROW_RIGHT :
389 case SWT.PAGE_DOWN :
390 case SWT.PAGE_UP :
391 case SWT.ARROW_DOWN :
392 case SWT.ARROW_UP :
393 default :
394 break;
395 }
396 updateStatus(fFound);
397 return;
398 }
399 // C-r,C-s are trapped. For now, use 'M-C-r' and 'M-C-s' instead.
400 if ((event.stateMask & SWT.ALT) != 0) {
401 // if (event.stateMask == (SWT.CTRL | SWT.ALT | SWT.SHIFT) && event.character == 0x73) {
402 if (event.keyCode == SWT.ARROW_DOWN) {
403 // 'C-s' forward
404 if (fFindString.length() == 0)
405 fFindString.append(fSavedString);
406 fForward = true;
407 performFindNext(true, fCurrentIndex + 1, true, false);
408 } else if (event.keyCode == SWT.ARROW_UP) {
409 fForward = false;
410 if (fFindString.length() == 0)
411 fFindString.append(fSavedString);
412 performFindNext(false, fCurrentIndex, true, false);
413 }
414 updateStatus(fFound);
415 return;
416 }
417 // Characters.
418 switch (event.character) {
419 case 0x1B :
420 case 0x0D :
421 // ESC, CR = quit
422 endSession();
423 break;
424 case 0x08 :
425 case 0x7F :
426 // backspace and delete
427 if (fFindString.length() > 0) {
428 popChar();
429 } else if (fFindString.length() == 0) {
430 setSelection(fElements[fBasePosition]);
431 } else
432 beep();
433 updateStatus(fFound);
434 break;
435 default :
436 if (event.stateMask == 0 || event.stateMask == SWT.SHIFT) {
437 pushChar(event.character);
438 performFindNext(fForward, fCurrentIndex, false, false);
439 }
440 updateStatus(fFound);
441 }
442 }
443
444 public void keyReleased(KeyEvent event) {
445 // if ((event.stateMask & SWT.CTRL) != 0) {
446 // // if (event.stateMask == (SWT.ALT | SWT.CTRL | SWT.SHIFT) && event.character == 0x72) {
447 // // 'C-r' and 'M-C-r' keyPressed() are trapped by eclipse.
448 // // For now, we use 'M-C-r' keyReleased() instead.
449 // if (event.keyCode == SWT.ARROW_UP) {
450 // fForward = false;
451 // if (fFindString.length() == 0)
452 // fFindString.append(fSavedString);
453 // performFindNext(false, fCurrentIndex, true, false);
454 // updateStatus(fFound);
455 // return;
456 // }
457 // }
458 }
459
460 /**
461 * Extends the incremental search by the given character.
462 * @param character the character to append to the searhc string
463 */
464 private void pushChar(char character) {
465 if (fCasePosition == -1
466 && Character.isUpperCase(character)
467 && Character.toLowerCase(character) != character)
468 fCasePosition = fFindString.length();
469 fFindString.append(character);
470 fStack.push(new Integer(fCurrentIndex));
471 }
472
473 /**
474 * Delete a character from the find string.
475 * @return Old length of find string.
476 */
477 private int popChar() {
478 int len = fFindString.length();
479 if (len > 0) {
480 fFindString.deleteCharAt(--len);
481 if (fCasePosition == len)
482 fCasePosition = -1;
483 }
484 // When findString is empty, go back to base position regardless.
485 if (len == 0) {
486 fCurrentIndex = fBasePosition;
487 fStack.clear();
488 } else if (fStack.empty())
489 return -1;
490 else
491 fCurrentIndex = ((Integer) fStack.pop()).intValue();
492 setSelection(fElements[fCurrentIndex]);
493 return fCurrentIndex;
494 }
495
496 private void beep() {
497 Control control = fViewer.getControl();
498 if (control != null && !control.isDisposed())
499 control.getDisplay().beep();
500 }
501
502 ////////////////////////////////////////////////////////////////////////
503
504 /*
505 * @see ITextListener#textChanged(TextEvent)
506 */
507 // public void textChanged(TextEvent event) {
508 // endSession();
509 // }
510
511 ////////////////////////////////////////////////////////////////////////
512
513 /*
514 * @see MouseListener#mouseDoubleClick(MouseEvent)
515 */
516 public void mouseDoubleClick(MouseEvent e) {
517 endSession();
518 }
519
520 /*
521 * @see MouseListener#mouseDown(MouseEvent)
522 */
523 public void mouseDown(MouseEvent e) {
524 endSession();
525 }
526
527 /*
528 * @see MouseListener#mouseUp(MouseEvent)
529 */
530 public void mouseUp(MouseEvent e) {
531 }
532
533 ////////////////////////////////////////////////////////////////////////
534
535 /*
536 * @see FocusListener#focusGained(FocusEvent)
537 */
538 public void focusGained(FocusEvent e) {
539 if (false)
540 msgDebug(".focusGrained()");
541 }
542
543 /*
544 * @see FocusListener#focusLost(FocusEvent)
545 */
546 public void focusLost(FocusEvent e) {
547 if (false)
548 msgDebug(".focusLost()");
549 //FIXME: 'M-C-r' caused advertise focus lost, so just ignore focus change for now.
550 //endSession();
551 }
552
553 ////////////////////////////////////////////////////////////////////////
554
555 /** Get an ordered list of elements in a tree.
556 */
557 private List getTreeElements(Object[] parents, ViewerSorter sorter, List ret) {
558 Object[] aa = parents;
559 if (sorter != null)
560 sorter.sort(fViewer, aa);
561 for (int i = 0; i < aa.length; ++i) {
562 ret.add(aa[i]);
563 getTreeElements(fContentProvider.getChildren(aa[i]), sorter, ret);
564 }
565 return ret;
566 }
567
568 ////////////////////////////////////////////////////////////////////////
569
570 /**
571 * Sets the given string as status message, clears the status error message.
572 * @param string the status message
573 */
574 private void statusMessage(String string) {
575 fStatusLine.setErrorMessage(""); //$NON-NLS-1$
576 fStatusLine.setMessage(escapeTabs(string));
577 }
578
579 /**
580 * Sets the status error message, clears the status message.
581 * @param string the status error message
582 */
583 private void statusError(String string) {
584 fStatusLine.setErrorMessage(escapeTabs(string));
585 fStatusLine.setMessage(""); //$NON-NLS-1$
586 }
587
588 /**
589 * Clears the status message and the status error message.
590 */
591 private void statusClear() {
592 fStatusLine.setErrorMessage(""); //$NON-NLS-1$
593 fStatusLine.setMessage(" "); //$NON-NLS-1$
594 }
595
596 /**
597 * Translates all tab characters into a proper status line presentation.
598 * @param string the string in which to translate the tabs
599 * @return the given string with all tab characters replace with a proper status line presentation
600 */
601 private String escapeTabs(String string) {
602 StringBuffer buffer = new StringBuffer();
603
604 int begin = 0;
605 int end = string.indexOf('\t', begin);
606
607 while (end >= 0) {
608 buffer.append(string.substring(begin, end));
609 buffer.append(TAB);
610 begin = end + 1;
611 end = string.indexOf('\t', begin);
612 }
613 buffer.append(string.substring(begin));
614
615 return buffer.toString();
616 }
617
618 private void msgDebug(String msg) {
619 System.err.println(NAME + msg);
620 }
621 }