001: /*
002: * @(#)TextComponentSearchable.java 10/11/2005
003: *
004: * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
005: */
006: package com.jidesoft.swing;
007:
008: import com.jidesoft.swing.event.SearchableEvent;
009:
010: import javax.swing.*;
011: import javax.swing.event.DocumentEvent;
012: import javax.swing.event.DocumentListener;
013: import javax.swing.text.*;
014: import java.awt.*;
015: import java.awt.event.ActionEvent;
016: import java.awt.event.KeyEvent;
017: import java.beans.PropertyChangeEvent;
018: import java.beans.PropertyChangeListener;
019: import java.util.HashMap;
020: import java.util.Iterator;
021:
022: /**
023: * <code>TextComponentSearchable</code> is an concrete implementation of {@link Searchable}
024: * that enables the search function in JTextComponent.
025: * <p>It's very simple to use it. Assuming you have a JTextComponent, all you need to do is to
026: * call
027: * <code><pre>
028: * JTextComponent textComponent = ....;
029: * TextComponentSearchable searchable = new TextComponentSearchable(textComponent);
030: * </pre></code>
031: * Now the JTextComponent will have the search function.
032: * <p/>
033: * There is very little customization you need to do to ListSearchable. The only thing you might
034: * need is when the element in the JTextComponent needs a special conversion to convert to string. If so, you can overide
035: * convertElementToString() to provide you own algorithm to do the conversion.
036: * <code><pre>
037: * JTextComponent textComponent = ....;
038: * TextComponentSearchable searchable = new ListSearchable(textComponent) {
039: * protected String convertElementToString(Object object) {
040: * ...
041: * }
042: * <p/>
043: * protected boolean isActivateKey(KeyEvent e) { // change to a different activation key
044: * return (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_F && (KeyEvent.CTRL_MASK & e.getModifiers()) != 0);
045: * }
046: * };
047: * </pre></code>
048: * <p/>
049: * Additional customization can be done on the base Searchable class such as background and foreground color, keystrokes,
050: * case sensitivity. TextComponentSearchable also has a special attribute called highlightColor. You can change it using {@link #setHighlightColor(java.awt.Color)}.
051: * <p/>
052: * Due to the special case of JTextComponent, the searching doesn't
053: * support wild card '*' or '?' as in other Searchables. The other difference is JTextComponent
054: * will keep the highlights after search popup hides. If you want to hide the highlights, just press
055: * ESC again (the first ESC will hide popup; the second ESC will hide all highlights if any).
056: */
057: public class TextComponentSearchable extends Searchable implements
058: DocumentListener, PropertyChangeListener {
059: private Highlighter.HighlightPainter _highlightPainter;
060: private final static Color DEFAULT_HIGHLIGHT_COLOR = new Color(204,
061: 204, 255);
062: private Color _highlightColor = null;
063: private int _selectedIndex = -1;
064: private HighlighCache _highlighCache;
065:
066: public TextComponentSearchable(JTextComponent textComponent) {
067: super (textComponent);
068: _highlighCache = new HighlighCache();
069: installHighlightsRemover();
070: setHighlightColor(DEFAULT_HIGHLIGHT_COLOR);
071: }
072:
073: /**
074: * Uninstalls the handler for ESC key to remove all highlights
075: */
076: public void uninstallHighlightsRemover() {
077: _component.unregisterKeyboardAction(KeyStroke.getKeyStroke(
078: KeyEvent.VK_ESCAPE, 0));
079: }
080:
081: /**
082: * Installs the handler for ESC key to remove all highlights
083: */
084: public void installHighlightsRemover() {
085: AbstractAction highlightRemover = new AbstractAction() {
086: public void actionPerformed(ActionEvent e) {
087: removeAllHighlights();
088: }
089: };
090: _component.registerKeyboardAction(highlightRemover, KeyStroke
091: .getKeyStroke(KeyEvent.VK_ESCAPE, 0),
092: JComponent.WHEN_FOCUSED);
093: }
094:
095: @Override
096: public void installListeners() {
097: super .installListeners();
098: if (_component instanceof JTextComponent) {
099: ((JTextComponent) _component).getDocument()
100: .addDocumentListener(this );
101: _component.addPropertyChangeListener("document", this );
102: }
103: }
104:
105: @Override
106: public void uninstallListeners() {
107: super .uninstallListeners();
108: if (_component instanceof JTextComponent) {
109: ((JTextComponent) _component).getDocument()
110: .removeDocumentListener(this );
111: _component.removePropertyChangeListener("document", this );
112: }
113: }
114:
115: @Override
116: protected void setSelectedIndex(int index, boolean incremental) {
117: if (_component instanceof JTextComponent) {
118: if (index == -1) {
119: removeAllHighlights();
120: _selectedIndex = -1;
121: return;
122: }
123:
124: if (!incremental) {
125: removeAllHighlights();
126: }
127:
128: String text = getSearchingText();
129: try {
130: addHighlight(index, text, incremental);
131: } catch (BadLocationException e) {
132: e.printStackTrace();
133: }
134: }
135: }
136:
137: /**
138: * Adds highlight to text component at specified index and text.
139: *
140: * @param index the index of the text to be highlighted
141: * @param text the text to be highlighted
142: * @param incremental if this is an incremental adding highlight
143: * @throws BadLocationException
144: */
145: protected void addHighlight(int index, String text,
146: boolean incremental) throws BadLocationException {
147: if (_component instanceof JTextComponent) {
148: JTextComponent textComponent = ((JTextComponent) _component);
149: Object obj = textComponent.getHighlighter().addHighlight(
150: index, index + text.length(), _highlightPainter);
151: _highlighCache.addHighlight(obj);
152: _selectedIndex = index;
153: if (!incremental) {
154: scrollTextVisible(textComponent, index, text.length());
155: }
156: }
157: }
158:
159: private void scrollTextVisible(JTextComponent textComponent,
160: int index, int length) {
161: // scroll highlight visible
162: if (index != -1) {
163: // Scroll the component if needed so that the composed text
164: // becomes visible.
165: try {
166: Rectangle begin = textComponent.modelToView(index);
167: if (begin == null) {
168: return;
169: }
170: Rectangle end = textComponent.modelToView(index
171: + length);
172: if (end == null) {
173: return;
174: }
175: Rectangle bounds = _component.getVisibleRect();
176: if (begin.x <= bounds.width) { // make sure if scroll back to the beginning as long as selected rect is visible
177: begin.width = end.x;
178: begin.x = 0;
179: } else {
180: begin.width = end.x - begin.x;
181: }
182: textComponent.scrollRectToVisible(begin);
183: } catch (BadLocationException ble) {
184: }
185: }
186: }
187:
188: /**
189: * Removes all highlights from the text component.
190: */
191: protected void removeAllHighlights() {
192: if (_component instanceof JTextComponent) {
193: Iterator itor = _highlighCache.getAllHighlights();
194: while (itor.hasNext()) {
195: Object o = itor.next();
196: ((JTextComponent) _component).getHighlighter()
197: .removeHighlight(o);
198: }
199: _highlighCache.removeAllHighlights();
200: }
201: }
202:
203: @Override
204: protected int getSelectedIndex() {
205: if (_component instanceof JTextComponent) {
206: return _selectedIndex;
207: }
208: return 0;
209: }
210:
211: @Override
212: protected Object getElementAt(int index) {
213: String text = getSearchingText();
214: if (text != null) {
215: if (_component instanceof JTextComponent) {
216: int endIndex = index + text.length();
217: int elementCount = getElementCount();
218: if (endIndex > elementCount) {
219: endIndex = getElementCount();
220: }
221: try {
222: return ((JTextComponent) _component).getDocument()
223: .getText(index, endIndex - index + 1);
224: } catch (BadLocationException e) {
225: return null;
226: }
227: }
228: }
229: return "";
230: }
231:
232: @Override
233: protected int getElementCount() {
234: if (_component instanceof JTextComponent) {
235: return ((JTextComponent) _component).getDocument()
236: .getLength();
237: }
238: return 0;
239: }
240:
241: /**
242: * Converts the element in JTextComponent to string. The returned value will be the
243: * <code>toString()</code> of whatever element that returned from <code>list.getModel().getElementAt(i)</code>.
244: *
245: * @param object
246: * @return the string representing the element in the JTextComponent.
247: */
248: @Override
249: protected String convertElementToString(Object object) {
250: if (object != null) {
251: return object.toString();
252: } else {
253: return "";
254: }
255: }
256:
257: public void propertyChange(PropertyChangeEvent evt) {
258: hidePopup();
259: _text = null;
260: if (evt.getOldValue() instanceof Document) {
261: ((Document) evt.getNewValue()).removeDocumentListener(this );
262: }
263: if (evt.getNewValue() instanceof Document) {
264: ((Document) evt.getNewValue()).addDocumentListener(this );
265: }
266: fireSearchableEvent(new SearchableEvent(this ,
267: SearchableEvent.SEARCHABLE_MODEL_CHANGE));
268: }
269:
270: public void insertUpdate(DocumentEvent e) {
271: hidePopup();
272: _text = null;
273: fireSearchableEvent(new SearchableEvent(this ,
274: SearchableEvent.SEARCHABLE_MODEL_CHANGE));
275: }
276:
277: public void removeUpdate(DocumentEvent e) {
278: hidePopup();
279: _text = null;
280: fireSearchableEvent(new SearchableEvent(this ,
281: SearchableEvent.SEARCHABLE_MODEL_CHANGE));
282: }
283:
284: public void changedUpdate(DocumentEvent e) {
285: hidePopup();
286: _text = null;
287: fireSearchableEvent(new SearchableEvent(this ,
288: SearchableEvent.SEARCHABLE_MODEL_CHANGE));
289: }
290:
291: @Override
292: protected boolean isActivateKey(KeyEvent e) {
293: if (_component instanceof JTextComponent
294: && ((JTextComponent) _component).isEditable()) {
295: return (e.getID() == KeyEvent.KEY_PRESSED
296: && e.getKeyCode() == KeyEvent.VK_F && (KeyEvent.CTRL_MASK & e
297: .getModifiers()) != 0);
298: } else {
299: return super .isActivateKey(e);
300: }
301: }
302:
303: /**
304: * Gets the highlight color.
305: *
306: * @return the highlight color.
307: */
308: public Color getHighlightColor() {
309: if (_highlightColor != null) {
310: return _highlightColor;
311: } else {
312: return DEFAULT_HIGHLIGHT_COLOR;
313: }
314: }
315:
316: /**
317: * Changes the highlight color.
318: *
319: * @param highlightColor
320: */
321: public void setHighlightColor(Color highlightColor) {
322: _highlightColor = highlightColor;
323: _highlightPainter = new DefaultHighlighter.DefaultHighlightPainter(
324: _highlightColor);
325: }
326:
327: @Override
328: public int findLast(String s) {
329: if (_component instanceof JTextComponent) {
330: String text = getDocumentText();
331: if (isCaseSensitive()) {
332: return text.lastIndexOf(s);
333: } else {
334: return text.toLowerCase().lastIndexOf(s.toLowerCase());
335: }
336: } else {
337: return super .findLast(s);
338: }
339: }
340:
341: private String _text = null;
342:
343: /**
344: * Gets the text from Document.
345: *
346: * @return the text of this JTextComponent. It used Document to get the text.
347: */
348: private String getDocumentText() {
349: if (_text == null) {
350: Document document = ((JTextComponent) _component)
351: .getDocument();
352: try {
353: String text = document.getText(0, document.getLength());
354: _text = text;
355: } catch (BadLocationException e) {
356: return "";
357: }
358: }
359: return _text;
360: }
361:
362: @Override
363: public int findFirst(String s) {
364: if (_component instanceof JTextComponent) {
365: String text = getDocumentText();
366: if (isCaseSensitive()) {
367: return text.indexOf(s);
368: } else {
369: return text.toLowerCase().indexOf(s.toLowerCase());
370: }
371: } else {
372: return super .findFirst(s);
373: }
374: }
375:
376: @Override
377: public int findFromCursor(String s) {
378: if (isReverseOrder()) {
379: return reverseFindFromCursor(s);
380: }
381:
382: if (_component instanceof JTextComponent) {
383: String text = getDocumentText();
384: if (!isCaseSensitive()) {
385: text = text.toLowerCase();
386: }
387: String str = isCaseSensitive() ? s : s.toLowerCase();
388: int selectedIndex = (getCursor() != -1 ? getCursor()
389: : getSelectedIndex());
390: if (selectedIndex < 0)
391: selectedIndex = 0;
392: int count = getElementCount();
393: if (count == 0)
394: return s.length() > 0 ? -1 : 0;
395:
396: // find from cursor
397: int found = text.indexOf(str, selectedIndex);
398:
399: // if not found, start over from the beginning
400: if (found == -1) {
401: found = text.indexOf(str, 0);
402: if (found >= selectedIndex) {
403: found = -1;
404: }
405: }
406:
407: return found;
408: } else {
409: return super .findFromCursor(s);
410: }
411: }
412:
413: @Override
414: public int reverseFindFromCursor(String s) {
415: if (!isReverseOrder()) {
416: return findFromCursor(s);
417: }
418:
419: if (_component instanceof JTextComponent) {
420: String text = getDocumentText();
421: if (!isCaseSensitive()) {
422: text = text.toLowerCase();
423: }
424: String str = isCaseSensitive() ? s : s.toLowerCase();
425: int selectedIndex = (getCursor() != -1 ? getCursor()
426: : getSelectedIndex());
427: if (selectedIndex < 0)
428: selectedIndex = 0;
429: int count = getElementCount();
430: if (count == 0)
431: return s.length() > 0 ? -1 : 0;
432:
433: // find from cursor
434: int found = text.lastIndexOf(str, selectedIndex);
435:
436: // if not found, start over from the end
437: if (found == -1) {
438: found = text.lastIndexOf(str, text.length() - 1);
439: if (found <= selectedIndex) {
440: found = -1;
441: }
442: }
443:
444: return found;
445: } else {
446: return super .findFromCursor(s);
447: }
448: }
449:
450: @Override
451: public int findNext(String s) {
452: if (_component instanceof JTextComponent) {
453: String text = getDocumentText();
454: if (!isCaseSensitive()) {
455: text = text.toLowerCase();
456: }
457: String str = isCaseSensitive() ? s : s.toLowerCase();
458: int selectedIndex = (getCursor() != -1 ? getCursor()
459: : getSelectedIndex());
460: if (selectedIndex < 0)
461: selectedIndex = 0;
462: int count = getElementCount();
463: if (count == 0)
464: return s.length() > 0 ? -1 : 0;
465:
466: // find from cursor
467: int found = text.indexOf(str, selectedIndex + 1);
468:
469: // if not found, start over from the beginning
470: if (found == -1 && isRepeats()) {
471: found = text.indexOf(str, 0);
472: if (found >= selectedIndex) {
473: found = -1;
474: }
475: }
476:
477: return found;
478: } else {
479: return super .findNext(s);
480: }
481: }
482:
483: @Override
484: public int findPrevious(String s) {
485: if (_component instanceof JTextComponent) {
486: String text = getDocumentText();
487: if (!isCaseSensitive()) {
488: text = text.toLowerCase();
489: }
490: String str = isCaseSensitive() ? s : s.toLowerCase();
491: int selectedIndex = (getCursor() != -1 ? getCursor()
492: : getSelectedIndex());
493: if (selectedIndex < 0)
494: selectedIndex = 0;
495: int count = getElementCount();
496: if (count == 0)
497: return s.length() > 0 ? -1 : 0;
498:
499: // find from cursor
500: int found = text.lastIndexOf(str, selectedIndex - 1);
501:
502: // if not found, start over from the beginning
503: if (found == -1 && isRepeats()) {
504: found = text.lastIndexOf(str, count - 1);
505: if (found <= selectedIndex) {
506: found = -1;
507: }
508: }
509:
510: return found;
511: } else {
512: return super .findPrevious(s);
513: }
514: }
515:
516: private class HighlighCache extends HashMap {
517: public void addHighlight(Object obj) {
518: put(obj, null);
519: }
520:
521: public void removeHighlight(Object obj) {
522: remove(obj);
523: }
524:
525: public Iterator getAllHighlights() {
526: return keySet().iterator();
527: }
528:
529: public void removeAllHighlights() {
530: clear();
531: }
532: }
533: }
|