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