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

org.fife.ui.autocomplete.AutoCompletion Maven / Gradle / Ivy

/*
 * 12/21/2008
 *
 * AutoCompletion.java - Handles auto-completion for a text component.
 * Copyright (C) 2008 Robert Futrell
 * robert_futrell at users.sourceforge.net
 * http://fifesoft.com/rsyntaxtextarea
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA.
 */
package org.fife.ui.autocomplete;

import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;
import javax.swing.text.*;


/**
 * Adds autocompletion to a text component.  Provides a popup window with a
 * list of autocomplete choices on a given keystroke, such as Crtrl+Space.

* * Depending on the {@link CompletionProvider} installed, the following * auto-completion features may be enabled: * *

    *
  • An auto-complete choices list made visible via e.g. Ctrl+Space
  • *
  • A "description" window displayed alongside the choices list that * provides documentation on the currently selected completion choice * (as seen in Eclipse and NetBeans).
  • *
  • Parameter assistance. If this is enabled, if the user enters a * "parameterized" completion, such as a method or a function, then * they will receive a tooltip describing the arguments they have to * enter to the completion. Also, the arguments can be navigated via * tab and shift+tab (ala Eclipse and NetBeans).
  • *
* * @author Robert Futrell * @version 1.0 */ /* * This class handles intercepting window and hierarchy events from the text * component, so the popup window is only visible when it should be visible. * It also handles communication between the CompletionProvider and the actual * popup Window. */ public class AutoCompletion implements HierarchyListener { /** * The text component we're providing completion for. */ private JTextComponent textComponent; /** * The parent window of {@link #textComponent}. */ private Window parentWindow; /** * The popup window containing completion choices. */ private AutoCompletePopupWindow popupWindow; /** * The preferred size of the optional description window. This field * only exists because the user may (and usually will) set the size of * the description window before it exists (it must be parented to a * Window). */ private Dimension preferredDescWindowSize; /** * A "tooltip" describing a function just entered. */ private ParameterizedCompletionDescriptionToolTip descToolTip; /** * Provides the completion options relevant to the current caret position. */ private CompletionProvider provider; /** * The renderer to use for the completion choices. If this is * null, then a default renderer is used. */ private ListCellRenderer renderer; /** * The handler to use when an external URL is clicked in the help * documentation. */ private ExternalURLHandler externalURLHandler; /** * Whether the description window should be displayed along with the * completion choice window. */ private boolean showDescWindow; /** * Whether autocomplete is enabled. */ private boolean autoCompleteEnabled; /** * Whether or not, when there is only a single auto-complete option * that maches the text at the current text position, that text should * be auto-inserted, instead of the completion window displaying. */ private boolean autoCompleteSingleChoices; /** * Whether parameter assistance is enabled. */ private boolean parameterAssistanceEnabled; /** * The keystroke that triggers the completion window. */ private KeyStroke trigger; /** * The previous key in the text component's InputMap for the * trigger key. */ private Object oldTriggerKey; /** * The action previously assigned to {@link #trigger}, so we can reset it * if the user disables auto-completion. */ private Action oldTriggerAction; /** * The previous key in the text component's InputMap for the * parameter completion trigger key. */ private Object oldParenKey; /** * The action previously assigned to the parameter completion key, so we * can reset it when we uninstall. */ private Action oldParenAction; /** * Listens for events in the parent window that affect the visibility of * the popup window. */ private Listener parentWindowListener; /** * The key used in the input map for the AutoComplete action. */ private static final String PARAM_TRIGGER_KEY = "AutoComplete"; /** * Key used in the input map for the parameter completion action. */ private static final String PARAM_COMPLETE_KEY = "AutoCompletion.FunctionStart"; /** * Whether debug messages should be printed to stdout as AutoCompletion * runs. */ private static final boolean DEBUG = initDebug(); /** * Constructor. * * @param provider The completion provider. This cannot be * null. */ public AutoCompletion(CompletionProvider provider) { setCompletionProvider(provider); setTriggerKey(getDefaultTriggerKey()); setAutoCompleteEnabled(true); setAutoCompleteSingleChoices(true); setShowDescWindow(false); parentWindowListener = new Listener(); } /** * Displays a "tooltip" detailing the inputs to the function just entered. * * @param pc The completion. * @param addParamListStart Whether or not * {@link CompletionProvider#getParameterListStart()} should be * added to the text component. */ private void displayDescriptionToolTip(ParameterizedCompletion pc, boolean addParamListStart) { // Get rid of the previous tooltip window, if there is one. hideToolTipWindow(); // Don't bother with a tooltip if there are no parameters. if (pc.getParamCount()==0) { CompletionProvider p = pc.getProvider(); String text = Character.toString(p.getParameterListEnd()); if (addParamListStart) { text = p.getParameterListStart() + text; } textComponent.replaceSelection(text); return; } descToolTip = new ParameterizedCompletionDescriptionToolTip( parentWindow, this, pc); try { int dot = textComponent.getCaretPosition(); Rectangle r = textComponent.modelToView(dot); Point p = new Point(r.x, r.y); SwingUtilities.convertPointToScreen(p, textComponent); r.x = p.x; r.y = p.y; descToolTip.setLocationRelativeTo(r); descToolTip.setVisible(true, addParamListStart); } catch (BadLocationException ble) { // Should never happen UIManager.getLookAndFeel().provideErrorFeedback(textComponent); ble.printStackTrace(); } } /** * Displays the popup window. Hosting applications can call this method * to programmatically begin an auto-completion operation. */ public void doCompletion() { refreshPopupWindow(); } /** * Returns whether, if a single autocomplete choice is available, it should * be automatically inserted, without displaying the popup menu. * * @return Whether to autocomplete single choices. * @see #setAutoCompleteSingleChoices(boolean) */ public boolean getAutoCompleteSingleChoices() { return autoCompleteSingleChoices; } /** * Returns the completion provider. * * @return The completion provider. */ public CompletionProvider getCompletionProvider() { return provider; } /** * Returns whether debug is enabled for AutoCompletion. * * @return Whether debug is enabled. */ static boolean getDebug() { return DEBUG; } /** * Returns the default autocomplete "trigger key" for this OS. For * Windows, for example, it is Ctrl+Space. * * @return The default autocomplete trigger key. */ public static KeyStroke getDefaultTriggerKey() { // Default to CTRL, even on Mac, since Ctrl+Space activates Spotlight int mask = InputEvent.CTRL_MASK; return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, mask); } /** * Returns the handler to use when an external URL is clicked in the * description window. * * @return The handler. * @see #setExternalURLHandler(ExternalURLHandler) */ public ExternalURLHandler getExternalURLHandler() { return externalURLHandler; } int getLineOfCaret() { Document doc = textComponent.getDocument(); Element root = doc.getDefaultRootElement(); return root.getElementIndex(textComponent.getCaretPosition()); } /** * Returns the default list cell renderer used when a completion provider * does not supply its own. * * @return The default list cell renderer. * @see #setListCellRenderer(ListCellRenderer) */ public ListCellRenderer getListCellRenderer() { return renderer; } /** * Returns the text to replace with in the document. This is a * "last-chance" hook for subclasses to make special modifications to the * completion text inserted. The default implementation simply returns * c.getReplacementText(). You usually will not need to modify * this method. * * @param c The completion being inserted. * @param doc The document being modified. * @param start The start of the text being replaced. * @param len The length of the text being replaced. * @return The text to replace with. */ protected String getReplacementText(Completion c, Document doc, int start, int len) { return c.getReplacementText(); } /** * Returns whether the "description window" should be shown alongside * the completion window. * * @return Whether the description window should be shown. * @see #setShowDescWindow(boolean) */ public boolean getShowDescWindow() { return showDescWindow; } /** * Returns the text component for which autocompletion is enabled. * * @return The text component, or null if this * {@link AutoCompletion} is not installed on any text component. * @see #install(JTextComponent) */ public JTextComponent getTextComponent() { return textComponent; } /** * Returns the orientation of the text component we're installed to. * * @return The orientation of the text component, or null if * we are not installed on one. */ ComponentOrientation getTextComponentOrientation() { return textComponent==null ? null : textComponent.getComponentOrientation(); } /** * Returns the "trigger key" used for autocomplete. * * @return The trigger key. * @see #setTriggerKey(KeyStroke) */ public KeyStroke getTriggerKey() { return trigger; } /** * Called when the component hierarchy for our text component changes. * When the text component is added to a new {@link Window}, this method * registers listeners on that Window. * * @param e The event. */ public void hierarchyChanged(HierarchyEvent e) { // NOTE: e many be null as we call this method at other times. //System.out.println("Hierarchy changed! " + e); Window oldParentWindow = parentWindow; parentWindow = SwingUtilities.getWindowAncestor(textComponent); if (parentWindow!=oldParentWindow) { if (oldParentWindow!=null) { parentWindowListener.removeFrom(oldParentWindow); } if (parentWindow!=null) { parentWindowListener.addTo(parentWindow); } } } /** * Hides any child windows being displayed by the auto-completion system. * * @return Whether any windows were visible. */ public boolean hideChildWindows() { //return hidePopupWindow() || hideToolTipWindow(); boolean res = hidePopupWindow(); res |= hideToolTipWindow(); return res; } /** * Hides the popup window, if it is visible. * * @return Whether the popup window was visible. */ private boolean hidePopupWindow() { if (popupWindow!=null) { if (popupWindow.isVisible()) { popupWindow.setVisible(false); return true; } } return false; } /** * Hides the parameter tool tip, if it is visible. * * @return Whether the tool tip window was visible. */ private boolean hideToolTipWindow() { if (descToolTip!=null) { descToolTip.setVisible(false, false); descToolTip = null; return true; } return false; } /** * Determines whether debug should be enabled for the AutoCompletion * library. This method checks a system property, but takes care of * {@link SecurityException}s in case we're in an applet or WebStart. * * @return Whether debug should be enabled. */ private static final boolean initDebug() { boolean debug = false; try { debug = Boolean.getBoolean("AutoCompletion.debug"); } catch (SecurityException se) { // We're in an applet or WebStart. debug = false; } return debug; } /** * Inserts a completion. * * @param c A completion to insert. This cannot be null. */ void insertCompletion(Completion c) { JTextComponent textComp = getTextComponent(); String alreadyEntered = c.getAlreadyEntered(textComp); hidePopupWindow(); Caret caret = textComp.getCaret(); int dot = caret.getDot(); int len = alreadyEntered.length(); int start = dot-len; String replacement = getReplacementText(c, textComp.getDocument(), start, len); caret.setDot(start); caret.moveDot(dot); textComp.replaceSelection(replacement); /* Document doc = textComp.getDocument(); try { if (doc instanceof AbstractDocument) { ((AbstractDocument)doc).replace(start, len, replacement, null); } else { doc.remove(start, len); doc.insertString(start, replacement, null); } } catch (javax.swing.text.BadLocationException ble) { ble.printStackTrace(); } */ if (isParameterAssistanceEnabled() && (c instanceof ParameterizedCompletion)) { ParameterizedCompletion pc = (ParameterizedCompletion)c; displayDescriptionToolTip(pc, true); } } /** * Installs this autocompletion on a text component. If this * {@link AutoCompletion} is already installed on another text component, * it is uninstalled first. * * @param c The text component. * @see #uninstall() */ public void install(JTextComponent c) { if (textComponent!=null) { uninstall(); } this.textComponent = c; installTriggerKey(getTriggerKey()); // Install the function completion key, if there is one. char start = provider.getParameterListStart(); if (start!=0) { InputMap im = c.getInputMap(); ActionMap am = c.getActionMap(); KeyStroke ks = KeyStroke.getKeyStroke(start); oldParenKey = im.get(ks); im.put(ks, PARAM_COMPLETE_KEY); oldParenAction = am.get(PARAM_COMPLETE_KEY); am.put(PARAM_COMPLETE_KEY, new ParameterizedCompletionStartAction(start)); } this.textComponent.addHierarchyListener(this); hierarchyChanged(null); // In case textComponent is already in a window } /** * Installs a "trigger key" action onto the current text component. * * @param ks The keystroke that should trigger the action. * @see #uninstallTriggerKey() */ private void installTriggerKey(KeyStroke ks) { InputMap im = textComponent.getInputMap(); oldTriggerKey = im.get(ks); im.put(ks, PARAM_TRIGGER_KEY); ActionMap am = textComponent.getActionMap(); oldTriggerAction = am.get(PARAM_TRIGGER_KEY); am.put(PARAM_TRIGGER_KEY, new AutoCompleteAction()); } /** * Returns whether autocompletion is enabled. * * @return Whether autocompletion is enabled. * @see #setAutoCompleteEnabled(boolean) */ public boolean isAutoCompleteEnabled() { return autoCompleteEnabled; } /** * Returns whether parameter assistance is enabled. * * @return Whether parameter assistance is enabled. * @see #setParameterAssistanceEnabled(boolean) */ public boolean isParameterAssistanceEnabled() { return parameterAssistanceEnabled; } /** * Returns whether the popup window is visible. * * @return Whether the popup window is visible. */ private boolean isPopupVisible() { return popupWindow!=null && popupWindow.isVisible(); } /** * Refreshes the popup window. First, this method gets the possible * completions for the current caret position. If there are none, and the * popup is visible, it is hidden. If there are some completions and the * popup is hidden, it is made visible and made to display the completions. * If there are some completions and the popup is visible, its list is * updated to the current set of completions. * * @return The current line number of the caret. */ protected int refreshPopupWindow() { final List completions = provider.getCompletions(textComponent); int count = completions.size(); if (count>1 || (count==1 && isPopupVisible()) || (count==1 && !getAutoCompleteSingleChoices())) { if (popupWindow==null) { popupWindow = new AutoCompletePopupWindow(parentWindow, this); // Completion is usually done for code, which is always done // LTR, so make completion stuff RTL only if text component is // also RTL. popupWindow.applyComponentOrientation( getTextComponentOrientation()); if (renderer!=null) { popupWindow.setListCellRenderer(renderer); } if (preferredDescWindowSize!=null) { popupWindow.setDescriptionWindowSize( preferredDescWindowSize); } } popupWindow.setCompletions(completions); if (!popupWindow.isVisible()) { Rectangle r = null; try { r = textComponent.modelToView(textComponent. getCaretPosition()); } catch (BadLocationException ble) { ble.printStackTrace(); return -1; } Point p = new Point(r.x, r.y); SwingUtilities.convertPointToScreen(p, textComponent); r.x = p.x; r.y = p.y; popupWindow.setLocationRelativeTo(r); popupWindow.setVisible(true); } } else if (count==1) { // !isPopupVisible && autoCompleteSingleChoices SwingUtilities.invokeLater(new Runnable() { public void run() { insertCompletion((Completion)completions.get(0)); } }); } else { hidePopupWindow(); } return getLineOfCaret(); } /** * Sets whether auto-completion is enabled. * * @param enabled Whether auto-completion is enabled. * @see #isAutoCompleteEnabled() */ public void setAutoCompleteEnabled(boolean enabled) { if (enabled!=autoCompleteEnabled) { autoCompleteEnabled = enabled; hidePopupWindow(); } } /** * Sets whether, if a single auto-complete choice is available, it should * be automatically inserted, without displaying the popup menu. * * @param autoComplete Whether to auto-complete single choices. * @see #getAutoCompleteSingleChoices() */ public void setAutoCompleteSingleChoices(boolean autoComplete) { autoCompleteSingleChoices = autoComplete; } /** * Sets the completion provider being used. * * @param provider The new completion provider. This cannot be * null. * @throws IllegalArgumentException If provider is * null. */ public void setCompletionProvider(CompletionProvider provider) { if (provider==null) { throw new IllegalArgumentException("provider cannot be null"); } this.provider = provider; hidePopupWindow(); // In case new choices should be displayed. } /** * Sets the size of the description window. * * @param w The new width. * @param h The new height. */ public void setDescriptionWindowSize(int w, int h) { preferredDescWindowSize = new Dimension(w, h); if (popupWindow!=null) { popupWindow.setDescriptionWindowSize(preferredDescWindowSize); } } /** * Sets the handler to use when an external URL is clicked in the * description window. This handler can perform some action, such as * open the URL in a web browser. The default implementation will open * the URL in a browser, but only if running in Java 6. If you want * browser support for Java 5 and below, you will have to install your own * handler to do so. * * @param handler The new handler. * @see #getExternalURLHandler() */ public void setExternalURLHandler(ExternalURLHandler handler) { this.externalURLHandler = handler; } /** * Sets the default list cell renderer to use when a completion provider * does not supply its own. * * @param renderer The renderer to use. If this is null, * a default renderer is used. * @see #getListCellRenderer() */ public void setListCellRenderer(ListCellRenderer renderer) { this.renderer = renderer; if (popupWindow!=null) { popupWindow.setListCellRenderer(renderer); hidePopupWindow(); } } /** * Sets whether parameter assistance is enabled. If parameter assistance * is enabled, and a "parameterized" completion (such as a function or * method) is inserted, the user will get "assistance" in inserting the * parameters in the form of a popup window with documentation and easy * tabbing through the arguments (as seen in Eclipse and NetBeans). * * @param enabled Whether parameter assistance should be enabled. * @see #isParameterAssistanceEnabled() */ public void setParameterAssistanceEnabled(boolean enabled) { parameterAssistanceEnabled = enabled; } /** * Sets whether the "description window" should be shown beside the * completion window. * * @param show Whether to show the description window. * @see #getShowDescWindow() */ public void setShowDescWindow(boolean show) { hidePopupWindow(); // Needed to force it to take effect showDescWindow = show; } /** * Sets the keystroke that should be used to trigger the auto-complete * popup window. * * @param ks The keystroke. * @throws IllegalArgumentException If ks is null. * @see #getTriggerKey() */ public void setTriggerKey(KeyStroke ks) { if (ks==null) { throw new IllegalArgumentException("trigger key cannot be null"); } if (!ks.equals(trigger)) { if (textComponent!=null) { // Put old trigger action back. uninstallTriggerKey(); // Grab current action for new trigger and replace it. installTriggerKey(ks); } trigger = ks; } } /** * Uninstalls this auto-completion from its text component. If it is not * installed on any text component, nothing happens. * * @see #install(JTextComponent) */ public void uninstall() { if (textComponent!=null) { hidePopupWindow(); // Unregisters listeners, actions, etc. uninstallTriggerKey(); // Uninstall the function completion key. char start = provider.getParameterListStart(); if (start!=0) { KeyStroke ks = KeyStroke.getKeyStroke(start); InputMap im = textComponent.getInputMap(); im.put(ks, oldParenKey); ActionMap am = textComponent.getActionMap(); am.put(PARAM_COMPLETE_KEY, oldParenAction); } textComponent.removeHierarchyListener(this); if (parentWindow!=null) { parentWindowListener.removeFrom(parentWindow); } textComponent = null; popupWindow = null; } } /** * Replaces the "trigger key" action with the one that was there * before auto-completion was installed. * * @see #installTriggerKey(KeyStroke) */ private void uninstallTriggerKey() { InputMap im = textComponent.getInputMap(); im.put(trigger, oldTriggerKey); ActionMap am = textComponent.getActionMap(); am.put(PARAM_TRIGGER_KEY, oldTriggerAction); } /** * Updates the LookAndFeel of the popup window. Applications can call * this method as appropriate if they support changing the LookAndFeel * at runtime. */ public void updateUI() { if (popupWindow!=null) { popupWindow.updateUI(); } if (descToolTip!=null) { descToolTip.updateUI(); } } /** * The Action that displays the popup window if * auto-completion is enabled. * * @author Robert Futrell * @version 1.0 */ class AutoCompleteAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (isAutoCompleteEnabled()) { refreshPopupWindow(); } else if (oldTriggerAction!=null) { oldTriggerAction.actionPerformed(e); } } } /** * Listens for events in the parent window of the text component with * auto-completion enabled. * * @author Robert Futrell * @version 1.0 */ private class Listener extends ComponentAdapter implements WindowFocusListener { public void addTo(Window w) { w.addComponentListener(this); w.addWindowFocusListener(this); } public void componentHidden(ComponentEvent e) { hideChildWindows(); } public void componentMoved(ComponentEvent e) { hideChildWindows(); } public void componentResized(ComponentEvent e) { hideChildWindows(); } public void removeFrom(Window w) { w.removeComponentListener(this); w.removeWindowFocusListener(this); } public void windowGainedFocus(WindowEvent e) { } public void windowLostFocus(WindowEvent e) { hideChildWindows(); } } /** * Action that starts a parameterized completion, e.g. after '(' is * typed. * * @author Robert Futrell * @version 1.0 */ private class ParameterizedCompletionStartAction extends AbstractAction { private String start; public ParameterizedCompletionStartAction(char ch) { this.start = Character.toString(ch); } public void actionPerformed(ActionEvent e) { hidePopupWindow(); // Prevents keystrokes from messing up textComponent.replaceSelection(start); if (!isParameterAssistanceEnabled()) { return; } List completions = provider. getParameterizedCompletions(textComponent); if (completions!=null && completions.size()>0) { // TODO: Have tooltip let you select between multiple, like VS ParameterizedCompletion pc = (ParameterizedCompletion)completions.get(0); displayDescriptionToolTip(pc, false); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy