
com.jidesoft.hints.AbstractIntelliHints Maven / Gradle / Ivy
/*
* @(#)AbstractIntelliHints.java 7/24/2005
*
* Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
*/
package com.jidesoft.hints;
import com.jidesoft.plaf.UIDefaultsLookup;
import com.jidesoft.popup.JidePopup;
import com.jidesoft.swing.DelegateAction;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.*;
/**
* AbstractIntelliHints
is an abstract implementation of {@link com.jidesoft.hints.IntelliHints}. It covers
* functions such as showing the hint popup at the correct position, delegating keystrokes,
* updating and selecting hint. The only thing that is left out to subclasses
* is the creation of the hint popup.
*
* @author Santhosh Kumar T
* @author JIDE Software, Inc.
*/
public abstract class AbstractIntelliHints implements IntelliHints {
/**
* The key of a client property. If a component has intellihints registered, you can use this client
* property to get the IntelliHints instance.
*/
public static final String CLIENT_PROPERTY_INTELLI_HINTS = "INTELLI_HINTS"; //NOI18N
private JidePopup _popup;
private JTextComponent _textComponent;
private boolean _followCaret = false;
// we use this flag to workaround the bug that setText() will trigger the hint popup.
private boolean _keyTyped = false;
// Specifies whether the hints popup should be displayed automatically.
// Default is true for backward compatibility.
private boolean _autoPopup = true;
/**
* Creates an IntelliHints object for a given JTextComponent.
*
* @param textComponent the text component.
*/
public AbstractIntelliHints(JTextComponent textComponent) {
_textComponent = textComponent;
getTextComponent().putClientProperty(CLIENT_PROPERTY_INTELLI_HINTS, this);
_popup = createPopup();
getTextComponent().getDocument().addDocumentListener(documentListener);
getTextComponent().addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
setKeyTyped(true);
}
}
});
getTextComponent().addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
}
public void focusLost(FocusEvent e) {
Container topLevelAncestor = _popup.getTopLevelAncestor();
if (topLevelAncestor == null) {
return;
}
Component oppositeComponent = e.getOppositeComponent();
if (topLevelAncestor == oppositeComponent || topLevelAncestor.isAncestorOf(oppositeComponent)) {
return;
}
hideHintsPopup();
}
});
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_FOCUSED, getShowHintsKeyStroke(), showAction);
KeyStroke[] keyStrokes = getDelegateKeyStrokes();
for (int i = 0; i < keyStrokes.length; i++) {
KeyStroke keyStroke = keyStrokes[i];
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_FOCUSED, keyStroke, new LazyDelegateAction(keyStroke));
}
getDelegateComponent().setRequestFocusEnabled(false);
getDelegateComponent().addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
hideHintsPopup();
setHintsEnabled(false);
acceptHint(getSelectedHint());
setHintsEnabled(true);
}
});
}
protected JidePopup createPopup() {
JidePopup popup = new JidePopup();
popup.setLayout(new BorderLayout());
popup.setResizable(true);
popup.setPopupBorder(BorderFactory.createLineBorder(UIDefaultsLookup.getColor("controlDkShadow"), 1));
popup.setMovable(false);
popup.add(createHintsComponent());
popup.addPopupMenuListener(new PopupMenuListener() {
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
DelegateAction.restoreAction(getTextComponent(), JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), hideAction);
DelegateAction.restoreAction(getTextComponent(), JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), acceptAction);
}
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
popup.setTransient(true);
return popup;
}
public JTextComponent getTextComponent() {
return _textComponent;
}
/**
* After user has selected a item in the hints popup, this method will update JTextComponent accordingly
* to accept the hint.
*
* For JTextArea, the default implementation will insert the hint into current caret position.
* For JTextField, by default it will replace the whole content with the item user selected. Subclass can
* always choose to override it to accept the hint in a different way. For example, {@link com.jidesoft.hints.FileIntelliHints}
* will append the selected item at the end of the existing text in order to complete a full file path.
*/
public void acceptHint(Object selected) {
if (selected == null)
return;
if (getTextComponent() instanceof JTextArea) {
int pos = getTextComponent().getCaretPosition();
String text = getTextComponent().getText();
int start = text.lastIndexOf("\n", pos - 1);
String remain = pos == -1 ? "" : text.substring(pos);
text = text.substring(0, start + 1);
text += selected;
pos = text.length();
text += remain;
getTextComponent().setText(text);
getTextComponent().setCaretPosition(pos);
}
else {
String hint = "" + selected;
getTextComponent().setText(hint);
getTextComponent().setCaretPosition(hint.length());
}
}
/**
* Shows the hints popup which contains the hints.
* It will call {@link #updateHints(Object)}. Only if it returns true,
* the popup will be shown.
*/
protected void showHintsPopup() {
if (getTextComponent().isEnabled() && getTextComponent().hasFocus() && updateHints(getContext())) {
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), hideAction);
DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), acceptAction, true);
int x = 0;
int y = 0;
int height = 0;
try {
int pos = getCaretPositionForPopup();
Rectangle position = getCaretRectangleForPopup(pos);
y = position.y;
x = position.x;
height = position.height;
}
catch (BadLocationException e) {
// this should never happen!!!
e.printStackTrace();
}
_popup.setOwner(getTextComponent());
_popup.showPopup(new Insets(y, x, getTextComponent().getHeight() - height - y, 0));
}
else {
_popup.hidePopup();
}
}
/**
* Gets the caret rectangle where caret is displayed. The popup will be show around the area so that the returned rectangle area
* is always visible. This method will be called twice.
*
* @param caretPosition the caret position.
* @return the popup position relative to the text component.
Please note, this position is actually a rectangle area. The reason is the popup could be
* 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
* 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
* 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,
* the popup will never cover the area specified by the rectangle (either below it or above it).
* @throws BadLocationException if the given position does not represent a valid location in the associated document.
*/
protected Rectangle getCaretRectangleForPopup(int caretPosition) throws BadLocationException {
return getTextComponent().getUI().modelToView(getTextComponent(), caretPosition);
}
/**
* Gets the caret position which is used as the anchor point to display the popup.
* By default, it {@link #isFollowCaret()} is true, it will return caret position.
* Otherwise it will return the caret position at the beginning of the caret line.
* Subclass can override to return any caret position.
*
* @return the caret position which is used as the anchor point to display the popup.
*/
protected int getCaretPositionForPopup() {
int caretPosition = Math.min(getTextComponent().getCaret().getDot(), getTextComponent().getCaret().getMark());
if (isFollowCaret()) {
return caretPosition;
}
else {
try {
Rectangle viewRect = getTextComponent().getUI().modelToView(getTextComponent(), caretPosition);
viewRect.x = 0;
return getTextComponent().getUI().viewToModel(getTextComponent(), viewRect.getLocation());
}
catch (BadLocationException e) {
return 0;
}
}
}
/**
* Gets the context for hints. The context is the information that IntelliHints needs
* in order to generate a list of hints. For example, for code-completion, the context is
* current word the cursor is on. for file completion, the context is the full string starting from
* the file system root.
* We provide a default context in AbstractIntelliHints. If it's a JTextArea,
* the context will be the string at the caret line from line beginning to the caret position. If it's a JTextField,
* the context will be whatever string in the text field. Subclass can always
* override it to return the context that is appropriate.
*
* @return the context.
*/
protected Object getContext() {
if (getTextComponent() instanceof JTextArea) {
int pos = getTextComponent().getCaretPosition();
if (pos == 0) {
return "";
}
else {
String text = getTextComponent().getText();
int start = text.lastIndexOf("\n", pos - 1);
return text.substring(start + 1, pos);
}
}
else {
return getTextComponent().getText();
}
}
/**
* Hides the hints popup.
*/
protected void hideHintsPopup() {
if (_popup != null) {
_popup.hidePopup();
}
setKeyTyped(false);
}
/**
* Enables or disables the hints popup.
*
* @param enabled true to enable the hints popup. Otherwise false.
*/
public void setHintsEnabled(boolean enabled) {
if (!enabled) {
// disable show hint temporarily
getTextComponent().getDocument().removeDocumentListener(documentListener);
}
else {
// enable show hint again
getTextComponent().getDocument().addDocumentListener(documentListener);
}
}
/**
* Checks if the hints popup is visible.
*
* @return true if it's visible. Otherwise, false.
*/
public boolean isHintsPopupVisible() {
return _popup != null && _popup.isPopupVisible();
}
/**
* Should the hints popup follows the caret.
*
* @return true if the popup shows up right below the caret. False if the popup always shows
* at the bottom-left corner (or top-left if there isn't enough on the bottom of the screen)
* of the JTextComponent.
*/
public boolean isFollowCaret() {
return _followCaret;
}
/**
* Sets the position of the hints popup. If followCaret is true, the popup
* shows up right below the caret. Otherwise, it will stay at the bottom-left corner
* (or top-left if there isn't enough on the bottom of the screen) of JTextComponent.
*
* @param followCaret true or false.
*/
public void setFollowCaret(boolean followCaret) {
_followCaret = followCaret;
}
/**
* Returns whether the hints popup is automatically displayed. Default is
* true
*
* @return true if the popup should be automatically displayed. False will
* never show it automatically and then need the user to manually activate
* it via the getShowHintsKeyStroke() key binding.
*/
public boolean isAutoPopup() {
return _autoPopup;
}
/**
* Sets whether the popup should be displayed automatically. If autoPopup
* is true then is the popup automatically displayed whenever updateHints()
* return true. If autoPopup is false it's not automatically displayed and
* will need the user to activate the key binding defined by
* getShowHintsKeyStroke().
*
* @param autoPopup true or false
*/
public void setAutoPopup(boolean autoPopup) {
this._autoPopup = autoPopup;
}
/**
* Gets the delegate keystrokes.
*
* When hint popup is visible, the keyboard focus never leaves the text component.
* However the hint popup usually contains a component that user will try to use navigation key to
* select an item. For example, use UP and DOWN key to navigate the list.
* Those keystrokes, if the popup is visible, will be delegated
* to the the component that returns from {@link #getDelegateComponent()}.
*
* @return an array of keystrokes that will be delegate to {@link #getDelegateComponent()} when hint popup is shown.
*/
abstract protected KeyStroke[] getDelegateKeyStrokes();
/**
* Gets the delegate component in the hint popup.
*
* @return the component that will receive the keystrokes that are delegated to hint popup.
*/
abstract protected JComponent getDelegateComponent();
/**
* Gets the keystroke that will trigger the hint popup. Usually the hints popup
* will be shown automatically when user types. Only when the hint popup is hidden
* accidentally, this keystroke will show the popup again.
*
* By default, it's the DOWN key for JTextField and CTRL+SPACE for JTextArea.
*
* @return the keystroek that will trigger the hint popup.
*/
protected KeyStroke getShowHintsKeyStroke() {
if (getTextComponent() instanceof JTextField) {
return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
}
else {
return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_MASK);
}
}
private DelegateAction acceptAction = new DelegateAction() {
@Override
public boolean delegateActionPerformed(ActionEvent e) {
JComponent tf = (JComponent) e.getSource();
AbstractIntelliHints hints = (AbstractIntelliHints) tf.getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
if (hints != null) {
hints.hideHintsPopup();
if (hints.getSelectedHint() != null) {
hints.setHintsEnabled(false);
hints.acceptHint(hints.getSelectedHint());
hints.setHintsEnabled(true);
return true;
}
else if (getTextComponent().getRootPane() != null) {
JButton button = getTextComponent().getRootPane().getDefaultButton();
if (button != null) {
button.doClick();
return true;
}
}
}
return false;
}
};
private static DelegateAction showAction = new DelegateAction() {
@Override
public boolean delegateActionPerformed(ActionEvent e) {
JComponent tf = (JComponent) e.getSource();
AbstractIntelliHints hints = (AbstractIntelliHints) tf.getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
if (hints != null && tf.isEnabled() && !hints.isHintsPopupVisible()) {
hints.showHintsPopup();
return true;
}
return false;
}
};
private DelegateAction hideAction = new DelegateAction() {
@Override
public boolean isEnabled() {
return _textComponent.isEnabled() && isHintsPopupVisible();
}
@Override
public boolean delegateActionPerformed(ActionEvent e) {
if (isEnabled()) {
hideHintsPopup();
return true;
}
return false;
}
};
private DocumentListener documentListener = new DocumentListener() {
private Timer timer = new Timer(200, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (isKeyTyped()) {
if (isHintsPopupVisible() || isAutoPopup()) {
showHintsPopup();
}
setKeyTyped(false);
}
}
});
public void insertUpdate(DocumentEvent e) {
startTimer();
}
public void removeUpdate(DocumentEvent e) {
startTimer();
}
public void changedUpdate(DocumentEvent e) {
}
void startTimer() {
if (timer.isRunning()) {
timer.restart();
}
else {
timer.setRepeats(false);
timer.start();
}
}
};
private boolean isKeyTyped() {
return _keyTyped;
}
private void setKeyTyped(boolean keyTyped) {
_keyTyped = keyTyped;
}
private static class LazyDelegateAction extends DelegateAction {
private KeyStroke _keyStroke;
public LazyDelegateAction(KeyStroke keyStroke) {
_keyStroke = keyStroke;
}
@Override
public boolean delegateActionPerformed(ActionEvent e) {
JComponent tf = (JComponent) e.getSource();
AbstractIntelliHints hints = (AbstractIntelliHints) tf.getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS);
if (hints != null && tf.isEnabled()) {
if (hints.isHintsPopupVisible()) {
Object key = hints.getDelegateComponent().getInputMap().get(_keyStroke);
key = key == null ? hints.getTextComponent().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(_keyStroke) : key;
if (key != null) {
Object action = hints.getDelegateComponent().getActionMap().get(key);
if (action instanceof Action) {
((Action) action).actionPerformed(new ActionEvent(hints.getDelegateComponent(), 0, "" + key));
return true;
}
}
}
}
return false;
}
}
}