All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jidesoft.hints.AbstractIntelliHints Maven / Gradle / Ivy

There is a newer version: 3.6.18
Show newest version
/*
 * @(#)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 {

    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 (KeyStroke keyStroke : keyStrokes) {
            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 = com.jidesoft.popup.JidePopupFactory.getSharedInstance().createPopup();
        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; String newText; int pos = getTextComponent().getCaretPosition(); if (isMultilineTextComponent()) { 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; text += remain; newText = text; } else { newText = selected.toString(); } getTextComponent().setText(newText); // DocumentFilters in JTextComponent's document model may alter the // provided text. The line separator has to be searched in the actual text String actualText = getTextComponent().getText(); int separatorIndex = actualText.indexOf('\n', pos); getTextComponent().setCaretPosition( separatorIndex == -1 ? actualText.length() : separatorIndex); } /** * Returns whether this IntelliHints' JTextComponent supports * single-line text or multi-line text. * * @return true if the component supports multiple text lines, * false otherwise */ protected boolean isMultilineTextComponent() { return getTextComponent() instanceof JTextArea || getTextComponent() instanceof JEditorPane; } /** * This method will call {@link #showHints()} if and only if the text component is enabled and * has focus. */ protected void showHintsPopup() { if (!getTextComponent().isEnabled() || !getTextComponent().hasFocus()) { return; } showHints(); } /** * Shows the hints popup which contains the hints. It will call {@link #updateHints(Object)}. * Only if it returns true, the popup will be shown. You can call this method to fore the hints * to be displayed. */ public void showHints() { if (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!!! } _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 (isMultilineTextComponent()) { 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 (isMultilineTextComponent()) { return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_MASK); } else { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0); } } private DelegateAction acceptAction = new DelegateAction() { @Override public boolean delegateActionPerformed(ActionEvent e) { JComponent tf = (JComponent) e.getSource(); IntelliHints hints = getIntelliHints(tf); if (hints instanceof AbstractIntelliHints) { AbstractIntelliHints aih = (AbstractIntelliHints) hints; aih.hideHintsPopup(); if (aih.getSelectedHint() != null) { aih.setHintsEnabled(false); aih.acceptHint(hints.getSelectedHint()); aih.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(); IntelliHints hints = getIntelliHints(tf); if (hints instanceof AbstractIntelliHints) { AbstractIntelliHints aih = (AbstractIntelliHints) hints; if (tf.isEnabled() && !aih.isHintsPopupVisible()) { aih.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(); IntelliHints hints = getIntelliHints(tf); if (hints instanceof AbstractIntelliHints) { AbstractIntelliHints aih = (AbstractIntelliHints) hints; if (tf.isEnabled()) { if (aih.isHintsPopupVisible()) { Object key = aih.getDelegateComponent().getInputMap().get(_keyStroke); key = key == null ? aih.getTextComponent().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).get(_keyStroke) : key; if (key != null) { Object action = aih.getDelegateComponent().getActionMap().get(key); if (action instanceof Action) { ((Action) action).actionPerformed(new ActionEvent(aih.getDelegateComponent(), 0, "" + key)); return true; } } } } } return false; } } /** * Gets the IntelliHints object if it was installed on the component before. * * @param component the component that has IntelliHints installed * * @return the IntelliHints. */ public static IntelliHints getIntelliHints(JComponent component) { return (IntelliHints) component.getClientProperty(CLIENT_PROPERTY_INTELLI_HINTS); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy