001: /*
002: * @(#)AbstractIntelliHints.java 7/24/2005
003: *
004: * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
005: */
006: package com.jidesoft.hints;
007:
008: import com.jidesoft.plaf.UIDefaultsLookup;
009: import com.jidesoft.popup.JidePopup;
010: import com.jidesoft.swing.DelegateAction;
011:
012: import javax.swing.*;
013: import javax.swing.event.DocumentEvent;
014: import javax.swing.event.DocumentListener;
015: import javax.swing.event.PopupMenuEvent;
016: import javax.swing.event.PopupMenuListener;
017: import javax.swing.text.BadLocationException;
018: import javax.swing.text.JTextComponent;
019: import java.awt.*;
020: import java.awt.event.*;
021:
022: /**
023: * <code>AbstractIntelliHints</code> is an abstract implementation of {@link com.jidesoft.hints.IntelliHints}. It covers
024: * functions such as showing the hint popup at the correct position, delegating keystrokes,
025: * updating and selecting hint. The only thing that is left out to subclasses
026: * is the creation of the hint popup.
027: *
028: * @author Santhosh Kumar T
029: * @author JIDE Software, Inc.
030: */
031: public abstract class AbstractIntelliHints implements IntelliHints {
032:
033: /**
034: * The key of a client property. If a component has intellihints registered, you can use this client
035: * property to get the IntelliHints instance.
036: */
037: public static final String CLIENT_PROPERTY_INTELLI_HINTS = "INTELLI_HINTS"; //NOI18N
038:
039: private JidePopup _popup;
040: private JTextComponent _textComponent;
041:
042: private boolean _followCaret = false;
043:
044: // we use this flag to workaround the bug that setText() will trigger the hint popup.
045: private boolean _keyTyped = false;
046:
047: // Specifies whether the hints popup should be displayed automatically.
048: // Default is true for backward compatibility.
049: private boolean _autoPopup = true;
050:
051: /**
052: * Creates an IntelliHints object for a given JTextComponent.
053: *
054: * @param textComponent the text component.
055: */
056: public AbstractIntelliHints(JTextComponent textComponent) {
057: _textComponent = textComponent;
058: getTextComponent().putClientProperty(
059: CLIENT_PROPERTY_INTELLI_HINTS, this );
060:
061: _popup = createPopup();
062:
063: getTextComponent().getDocument().addDocumentListener(
064: documentListener);
065: getTextComponent().addKeyListener(new KeyListener() {
066: public void keyTyped(KeyEvent e) {
067: }
068:
069: public void keyPressed(KeyEvent e) {
070: }
071:
072: public void keyReleased(KeyEvent e) {
073: if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
074: setKeyTyped(true);
075: }
076: }
077: });
078: getTextComponent().addFocusListener(new FocusListener() {
079: public void focusGained(FocusEvent e) {
080: }
081:
082: public void focusLost(FocusEvent e) {
083: Container topLevelAncestor = _popup
084: .getTopLevelAncestor();
085: if (topLevelAncestor == null) {
086: return;
087: }
088: Component oppositeComponent = e.getOppositeComponent();
089: if (topLevelAncestor == oppositeComponent
090: || topLevelAncestor
091: .isAncestorOf(oppositeComponent)) {
092: return;
093: }
094: hideHintsPopup();
095: }
096: });
097:
098: DelegateAction.replaceAction(getTextComponent(),
099: JComponent.WHEN_FOCUSED, getShowHintsKeyStroke(),
100: showAction);
101:
102: KeyStroke[] keyStrokes = getDelegateKeyStrokes();
103: for (int i = 0; i < keyStrokes.length; i++) {
104: KeyStroke keyStroke = keyStrokes[i];
105: DelegateAction.replaceAction(getTextComponent(),
106: JComponent.WHEN_FOCUSED, keyStroke,
107: new LazyDelegateAction(keyStroke));
108: }
109:
110: getDelegateComponent().setRequestFocusEnabled(false);
111: getDelegateComponent().addMouseListener(new MouseAdapter() {
112: @Override
113: public void mouseClicked(MouseEvent e) {
114: hideHintsPopup();
115: setHintsEnabled(false);
116: acceptHint(getSelectedHint());
117: setHintsEnabled(true);
118: }
119: });
120: }
121:
122: protected JidePopup createPopup() {
123: JidePopup popup = new JidePopup();
124: popup.setLayout(new BorderLayout());
125: popup.setResizable(true);
126: popup.setPopupBorder(BorderFactory.createLineBorder(
127: UIDefaultsLookup.getColor("controlDkShadow"), 1));
128: popup.setMovable(false);
129: popup.add(createHintsComponent());
130: popup.addPopupMenuListener(new PopupMenuListener() {
131: public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
132: }
133:
134: public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
135: DelegateAction.restoreAction(getTextComponent(),
136: JComponent.WHEN_FOCUSED, KeyStroke
137: .getKeyStroke(KeyEvent.VK_ESCAPE, 0),
138: hideAction);
139: DelegateAction.restoreAction(getTextComponent(),
140: JComponent.WHEN_FOCUSED, KeyStroke
141: .getKeyStroke(KeyEvent.VK_ENTER, 0),
142: acceptAction);
143: }
144:
145: public void popupMenuCanceled(PopupMenuEvent e) {
146: }
147: });
148: popup.setTransient(true);
149: return popup;
150: }
151:
152: public JTextComponent getTextComponent() {
153: return _textComponent;
154: }
155:
156: /**
157: * After user has selected a item in the hints popup, this method will update JTextComponent accordingly
158: * to accept the hint.
159: * <p/>
160: * For JTextArea, the default implementation will insert the hint into current caret position.
161: * For JTextField, by default it will replace the whole content with the item user selected. Subclass can
162: * always choose to override it to accept the hint in a different way. For example, {@link com.jidesoft.hints.FileIntelliHints}
163: * will append the selected item at the end of the existing text in order to complete a full file path.
164: */
165: public void acceptHint(Object selected) {
166: if (selected == null)
167: return;
168:
169: if (getTextComponent() instanceof JTextArea) {
170: int pos = getTextComponent().getCaretPosition();
171: String text = getTextComponent().getText();
172: int start = text.lastIndexOf("\n", pos - 1);
173: String remain = pos == -1 ? "" : text.substring(pos);
174: text = text.substring(0, start + 1);
175: text += selected;
176: pos = text.length();
177: text += remain;
178: getTextComponent().setText(text);
179: getTextComponent().setCaretPosition(pos);
180: } else {
181: String hint = "" + selected;
182: getTextComponent().setText(hint);
183: getTextComponent().setCaretPosition(hint.length());
184: }
185: }
186:
187: /**
188: * Shows the hints popup which contains the hints.
189: * It will call {@link #updateHints(Object)}. Only if it returns true,
190: * the popup will be shown.
191: */
192: protected void showHintsPopup() {
193: if (getTextComponent().isEnabled()
194: && getTextComponent().hasFocus()
195: && updateHints(getContext())) {
196: DelegateAction.replaceAction(getTextComponent(),
197: JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(
198: KeyEvent.VK_ESCAPE, 0), hideAction);
199: DelegateAction.replaceAction(getTextComponent(),
200: JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(
201: KeyEvent.VK_ENTER, 0), acceptAction, true);
202:
203: int x = 0;
204: int y = 0;
205: int height = 0;
206:
207: try {
208: int pos = getCaretPositionForPopup();
209: Rectangle position = getCaretRectangleForPopup(pos);
210: y = position.y;
211: x = position.x;
212: height = position.height;
213: } catch (BadLocationException e) {
214: // this should never happen!!!
215: e.printStackTrace();
216: }
217:
218: _popup.setOwner(getTextComponent());
219: _popup.showPopup(new Insets(y, x, getTextComponent()
220: .getHeight()
221: - height - y, 0));
222: } else {
223: _popup.hidePopup();
224: }
225: }
226:
227: /**
228: * Gets the caret rectangle where caret is displayed. The popup will be show around the area so that the returned rectangle area
229: * is always visible. This method will be called twice.
230: *
231: * @param caretPosition the caret position.
232: * @return the popup position relative to the text component. <br>Please note, this position is actually a rectangle area. The reason is the popup could be
233: * shown below or above the rectangle. Usually, the popup will be shown below the rectangle. In this case, the x and y of the rectangle will
234: * be the top-left corner of the popup. However if there isn't enough space for the popup because it's close to screen bottom border, we will
235: * show the popup above the rectangle. In this case, the bottom-left corner of the popup will be at x and (y - height). Simply speaking,
236: * the popup will never cover the area specified by the rectangle (either below it or above it).
237: * @throws BadLocationException if the given position does not represent a valid location in the associated document.
238: */
239: protected Rectangle getCaretRectangleForPopup(int caretPosition)
240: throws BadLocationException {
241: return getTextComponent().getUI().modelToView(
242: getTextComponent(), caretPosition);
243: }
244:
245: /**
246: * Gets the caret position which is used as the anchor point to display the popup.
247: * By default, it {@link #isFollowCaret()} is true, it will return caret position.
248: * Otherwise it will return the caret position at the beginning of the caret line.
249: * Subclass can override to return any caret position.
250: *
251: * @return the caret position which is used as the anchor point to display the popup.
252: */
253: protected int getCaretPositionForPopup() {
254: int caretPosition = Math.min(getTextComponent().getCaret()
255: .getDot(), getTextComponent().getCaret().getMark());
256: if (isFollowCaret()) {
257: return caretPosition;
258: } else {
259: try {
260: Rectangle viewRect = getTextComponent().getUI()
261: .modelToView(getTextComponent(), caretPosition);
262: viewRect.x = 0;
263: return getTextComponent().getUI().viewToModel(
264: getTextComponent(), viewRect.getLocation());
265: } catch (BadLocationException e) {
266: return 0;
267: }
268: }
269: }
270:
271: /**
272: * Gets the context for hints. The context is the information that IntelliHints needs
273: * in order to generate a list of hints. For example, for code-completion, the context is
274: * current word the cursor is on. for file completion, the context is the full string starting from
275: * the file system root.
276: * <p>We provide a default context in AbstractIntelliHints. If it's a JTextArea,
277: * the context will be the string at the caret line from line beginning to the caret position. If it's a JTextField,
278: * the context will be whatever string in the text field. Subclass can always
279: * override it to return the context that is appropriate.
280: *
281: * @return the context.
282: */
283: protected Object getContext() {
284: if (getTextComponent() instanceof JTextArea) {
285: int pos = getTextComponent().getCaretPosition();
286: if (pos == 0) {
287: return "";
288: } else {
289: String text = getTextComponent().getText();
290: int start = text.lastIndexOf("\n", pos - 1);
291: return text.substring(start + 1, pos);
292: }
293: } else {
294: return getTextComponent().getText();
295: }
296: }
297:
298: /**
299: * Hides the hints popup.
300: */
301: protected void hideHintsPopup() {
302: if (_popup != null) {
303: _popup.hidePopup();
304: }
305: setKeyTyped(false);
306: }
307:
308: /**
309: * Enables or disables the hints popup.
310: *
311: * @param enabled true to enable the hints popup. Otherwise false.
312: */
313: public void setHintsEnabled(boolean enabled) {
314: if (!enabled) {
315: // disable show hint temporarily
316: getTextComponent().getDocument().removeDocumentListener(
317: documentListener);
318: } else {
319: // enable show hint again
320: getTextComponent().getDocument().addDocumentListener(
321: documentListener);
322: }
323:
324: }
325:
326: /**
327: * Checks if the hints popup is visible.
328: *
329: * @return true if it's visible. Otherwise, false.
330: */
331: public boolean isHintsPopupVisible() {
332: return _popup != null && _popup.isPopupVisible();
333: }
334:
335: /**
336: * Should the hints popup follows the caret.
337: *
338: * @return true if the popup shows up right below the caret. False if the popup always shows
339: * at the bottom-left corner (or top-left if there isn't enough on the bottom of the screen)
340: * of the JTextComponent.
341: */
342: public boolean isFollowCaret() {
343: return _followCaret;
344: }
345:
346: /**
347: * Sets the position of the hints popup. If followCaret is true, the popup
348: * shows up right below the caret. Otherwise, it will stay at the bottom-left corner
349: * (or top-left if there isn't enough on the bottom of the screen) of JTextComponent.
350: *
351: * @param followCaret true or false.
352: */
353: public void setFollowCaret(boolean followCaret) {
354: _followCaret = followCaret;
355: }
356:
357: /**
358: * Returns whether the hints popup is automatically displayed. Default is
359: * true
360: *
361: * @return true if the popup should be automatically displayed. False will
362: * never show it automatically and then need the user to manually activate
363: * it via the getShowHintsKeyStroke() key binding.
364: */
365: public boolean isAutoPopup() {
366: return _autoPopup;
367: }
368:
369: /**
370: * Sets whether the popup should be displayed automatically. If autoPopup
371: * is true then is the popup automatically displayed whenever updateHints()
372: * return true. If autoPopup is false it's not automatically displayed and
373: * will need the user to activate the key binding defined by
374: * getShowHintsKeyStroke().
375: *
376: * @param autoPopup true or false
377: */
378: public void setAutoPopup(boolean autoPopup) {
379: this ._autoPopup = autoPopup;
380: }
381:
382: /**
383: * Gets the delegate keystrokes.
384: * <p/>
385: * When hint popup is visible, the keyboard focus never leaves the text component.
386: * However the hint popup usually contains a component that user will try to use navigation key to
387: * select an item. For example, use UP and DOWN key to navigate the list.
388: * Those keystrokes, if the popup is visible, will be delegated
389: * to the the component that returns from {@link #getDelegateComponent()}.
390: *
391: * @return an array of keystrokes that will be delegate to {@link #getDelegateComponent()} when hint popup is shown.
392: */
393: abstract protected KeyStroke[] getDelegateKeyStrokes();
394:
395: /**
396: * Gets the delegate component in the hint popup.
397: *
398: * @return the component that will receive the keystrokes that are delegated to hint popup.
399: */
400: abstract protected JComponent getDelegateComponent();
401:
402: /**
403: * Gets the keystroke that will trigger the hint popup. Usually the hints popup
404: * will be shown automatically when user types. Only when the hint popup is hidden
405: * accidentally, this keystroke will show the popup again.
406: * <p/>
407: * By default, it's the DOWN key for JTextField and CTRL+SPACE for JTextArea.
408: *
409: * @return the keystroek that will trigger the hint popup.
410: */
411: protected KeyStroke getShowHintsKeyStroke() {
412: if (getTextComponent() instanceof JTextField) {
413: return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
414: } else {
415: return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
416: KeyEvent.CTRL_MASK);
417: }
418: }
419:
420: private DelegateAction acceptAction = new DelegateAction() {
421: @Override
422: public boolean delegateActionPerformed(ActionEvent e) {
423: JComponent tf = (JComponent) e.getSource();
424: AbstractIntelliHints hints = (AbstractIntelliHints) tf
425: .getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
426: if (hints != null) {
427: hints.hideHintsPopup();
428: if (hints.getSelectedHint() != null) {
429: hints.setHintsEnabled(false);
430: hints.acceptHint(hints.getSelectedHint());
431: hints.setHintsEnabled(true);
432: return true;
433: } else if (getTextComponent().getRootPane() != null) {
434: JButton button = getTextComponent().getRootPane()
435: .getDefaultButton();
436: if (button != null) {
437: button.doClick();
438: return true;
439: }
440: }
441: }
442: return false;
443: }
444: };
445:
446: private static DelegateAction showAction = new DelegateAction() {
447: @Override
448: public boolean delegateActionPerformed(ActionEvent e) {
449: JComponent tf = (JComponent) e.getSource();
450: AbstractIntelliHints hints = (AbstractIntelliHints) tf
451: .getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
452: if (hints != null && tf.isEnabled()
453: && !hints.isHintsPopupVisible()) {
454: hints.showHintsPopup();
455: return true;
456: }
457: return false;
458: }
459: };
460:
461: private DelegateAction hideAction = new DelegateAction() {
462: @Override
463: public boolean isEnabled() {
464: return _textComponent.isEnabled() && isHintsPopupVisible();
465: }
466:
467: @Override
468: public boolean delegateActionPerformed(ActionEvent e) {
469: if (isEnabled()) {
470: hideHintsPopup();
471: return true;
472: }
473: return false;
474: }
475: };
476:
477: private DocumentListener documentListener = new DocumentListener() {
478: private Timer timer = new Timer(200, new ActionListener() {
479: public void actionPerformed(ActionEvent e) {
480: if (isKeyTyped()) {
481: if (isHintsPopupVisible() || isAutoPopup()) {
482: showHintsPopup();
483: }
484: setKeyTyped(false);
485: }
486: }
487: });
488:
489: public void insertUpdate(DocumentEvent e) {
490: startTimer();
491: }
492:
493: public void removeUpdate(DocumentEvent e) {
494: startTimer();
495: }
496:
497: public void changedUpdate(DocumentEvent e) {
498: }
499:
500: void startTimer() {
501: if (timer.isRunning()) {
502: timer.restart();
503: } else {
504: timer.setRepeats(false);
505: timer.start();
506: }
507: }
508: };
509:
510: private boolean isKeyTyped() {
511: return _keyTyped;
512: }
513:
514: private void setKeyTyped(boolean keyTyped) {
515: _keyTyped = keyTyped;
516: }
517:
518: private static class LazyDelegateAction extends DelegateAction {
519: private KeyStroke _keyStroke;
520:
521: public LazyDelegateAction(KeyStroke keyStroke) {
522: _keyStroke = keyStroke;
523: }
524:
525: @Override
526: public boolean delegateActionPerformed(ActionEvent e) {
527: JComponent tf = (JComponent) e.getSource();
528: AbstractIntelliHints hints = (AbstractIntelliHints) tf
529: .getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
530: if (hints != null && tf.isEnabled()) {
531: if (hints.isHintsPopupVisible()) {
532: Object key = hints.getDelegateComponent()
533: .getInputMap().get(_keyStroke);
534: key = key == null ? hints
535: .getTextComponent()
536: .getInputMap(
537: JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
538: .get(_keyStroke)
539: : key;
540: if (key != null) {
541: Object action = hints.getDelegateComponent()
542: .getActionMap().get(key);
543: if (action instanceof Action) {
544: ((Action) action)
545: .actionPerformed(new ActionEvent(
546: hints
547: .getDelegateComponent(),
548: 0, "" + key));
549: return true;
550: }
551: }
552: }
553: }
554: return false;
555: }
556: }
557: }
|