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

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

Go to download

AutoComplete is a library allowing you to add IDE-like auto-completion (aka "code completion" or "Intellisense") to any Swing JTextComponent. Special integration is added for RSyntaxTextArea, since this feature is commonly needed when editing source code. Features include: Drop-down completion choice list. Optional companion "description" window, complete with full HTML support and navigable with hyperlinks. Optional parameter completion assistance for functions and methods, ala Eclipse and NetBeans. Completion information is typically specified in an XML file, but can even be dynamic.

There is a newer version: 3.3.2
Show newest version
/*
 * 12/21/2008
 *
 * AutoCompletePopupWindow.java - A window containing a list of auto-complete
 * choices.
 *
 * This library is distributed under a modified BSD license.  See the included
 * LICENSE.md file for details.
 */
package org.fife.ui.autocomplete;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.ListUI;
import javax.swing.text.Caret;
import javax.swing.text.JTextComponent;

import org.fife.ui.rsyntaxtextarea.PopupWindowDecorator;


/**
 * The actual popup window of choices.  When visible, this window intercepts
 * certain keystrokes in the parent text component and uses them to navigate
 * the completion choices instead.  If Enter or Escape is pressed, the window
 * hides itself and notifies the {@link AutoCompletion} to insert the selected
 * text.
 *
 * @author Robert Futrell
 * @version 1.0
 */
@SuppressWarnings("checkstyle:MultipleVariableDeclarations")
class AutoCompletePopupWindow extends JWindow implements CaretListener,
									ListSelectionListener, MouseListener {

	/**
	 * The parent AutoCompletion instance.
	 */
	private AutoCompletion ac;

	/**
	 * The list of completion choices.
	 */
	private PopupList list;

	/**
	 * The contents of {@link #list()}.
	 */
	private CompletionListModel model;

	/**
	 * A hack to work around the fact that we clear our completion model (and
	 * our selection) when hiding the completion window.  This allows us to
	 * still know what the user selected after the popup is hidden.
	 */
	private Completion lastSelection;

	/**
	 * Optional popup window containing a description of the currently
	 * selected completion.
	 */
	private AutoCompleteDescWindow descWindow;

	/**
	 * 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;

	/**
	 * Whether the completion window and the optional description window
	 * should be displayed above the current caret position (as opposed to
	 * underneath it, which is preferred unless there is not enough space).
	 */
	private boolean aboveCaret;

	/**
	 * The background color of the description window.
	 */
	private Color descWindowColor;

	private int lastLine;

	private boolean keyBindingsInstalled;

	private KeyActionPair escapeKap;
	private KeyActionPair upKap;
	private KeyActionPair downKap;
	private KeyActionPair leftKap;
	private KeyActionPair rightKap;
	private KeyActionPair enterKap;
	private KeyActionPair tabKap;
	private KeyActionPair homeKap;
	private KeyActionPair endKap;
	private KeyActionPair pageUpKap;
	private KeyActionPair pageDownKap;
	private KeyActionPair ctrlCKap;

	private KeyActionPair oldEscape, oldUp, oldDown, oldLeft, oldRight,
			oldEnter, oldTab, oldHome, oldEnd, oldPageUp, oldPageDown,
			oldCtrlC;

	/**
	 * The space between the caret and the completion popup.
	 */
	private static final int VERTICAL_SPACE			= 1;

	/**
	 * The class name of the Substance List UI.
	 */
	private static final String SUBSTANCE_LIST_UI =
			"org.pushingpixels.substance.internal.ui.SubstanceListUI";


	/**
	 * Constructor.
	 *
	 * @param parent The parent window (hosting the text component).
	 * @param ac The auto-completion instance.
	 */
	AutoCompletePopupWindow(Window parent, final AutoCompletion ac) {

		super(parent);
		ComponentOrientation o = ac.getTextComponentOrientation();

		this.ac = ac;
		model = new CompletionListModel();
		list = new PopupList(model);

		list.setCellRenderer(new DelegatingCellRenderer());
		list.addListSelectionListener(this);
		list.addMouseListener(this);

		JPanel contentPane = new JPanel(new BorderLayout());
		JScrollPane sp = new JScrollPane(list,
							JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
							JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

		// In 1.4, JScrollPane.setCorner() has a bug where it won't accept
		// JScrollPane.LOWER_TRAILING_CORNER, even though that constant is
		// defined.  So we have to put the logic added in 1.5 to handle it
		// here.
		JPanel corner = new SizeGrip();
		//sp.setCorner(JScrollPane.LOWER_TRAILING_CORNER, corner);
		boolean isLeftToRight = o.isLeftToRight();
	    String str = isLeftToRight ? JScrollPane.LOWER_RIGHT_CORNER :
	    								JScrollPane.LOWER_LEFT_CORNER;
	    sp.setCorner(str, corner);

		contentPane.add(sp);
		setContentPane(contentPane);
		applyComponentOrientation(o);

		// Give apps a chance to decorate us with drop shadows, etc.
		if (Util.getShouldAllowDecoratingMainAutoCompleteWindows()) {
			PopupWindowDecorator decorator = PopupWindowDecorator.get();
			if (decorator!=null) {
				decorator.decorate(this);
			}
		}

		pack();

		setFocusableWindowState(false);

		lastLine = -1;

	}


	@Override
	public void caretUpdate(CaretEvent e) {
		if (isVisible()) { // Should always be true
			int line = ac.getLineOfCaret();
			if (line!=lastLine) {
				lastLine = -1;
				setVisible(false);
			}
			else {
				doAutocomplete();
			}
		}
		else if (AutoCompletion.getDebug()) {
			Thread.dumpStack();
		}
	}


	/**
	 * Creates the description window.
	 *
	 * @return The description window.
	 */
	private AutoCompleteDescWindow createDescriptionWindow() {

		AutoCompleteDescWindow dw = new AutoCompleteDescWindow(this, ac);
		dw.applyComponentOrientation(ac.getTextComponentOrientation());

		Dimension size = preferredDescWindowSize;
		if (size==null) {
			size = getSize();
		}
		dw.setSize(size);

		if (descWindowColor  != null) {
		    dw.setBackground(descWindowColor);
        }
        else {
            descWindowColor = dw.getBackground();
        }

		return dw;
	}


	/**
	 * Creates the mappings from keys to Actions we'll be putting into the
	 * text component's ActionMap and InputMap.
	 */
	private void createKeyActionPairs() {

		// Actions we'll install.
		EnterAction enterAction = new EnterAction();
		escapeKap = new KeyActionPair("Escape", new EscapeAction());
		upKap = new KeyActionPair("Up", new UpAction());
		downKap = new KeyActionPair("Down", new DownAction());
		leftKap = new KeyActionPair("Left", new LeftAction());
		rightKap = new KeyActionPair("Right", new RightAction());
		enterKap = new KeyActionPair("Enter", enterAction);
		tabKap = new KeyActionPair("Tab", enterAction);
		homeKap = new KeyActionPair("Home", new HomeAction());
		endKap = new KeyActionPair("End", new EndAction());
		pageUpKap = new KeyActionPair("PageUp", new PageUpAction());
		pageDownKap = new KeyActionPair("PageDown", new PageDownAction());
		ctrlCKap = new KeyActionPair("CtrlC", new CopyAction());

		// Buffers for the actions we replace.
		oldEscape = new KeyActionPair();
		oldUp = new KeyActionPair();
		oldDown = new KeyActionPair();
		oldLeft = new KeyActionPair();
		oldRight = new KeyActionPair();
		oldEnter = new KeyActionPair();
		oldTab = new KeyActionPair();
		oldHome = new KeyActionPair();
		oldEnd = new KeyActionPair();
		oldPageUp = new KeyActionPair();
		oldPageDown = new KeyActionPair();
		oldCtrlC = new KeyActionPair();

	}


	protected void doAutocomplete() {
		lastLine = ac.refreshPopupWindow();
	}


	/**
	 * Returns the copy keystroke to use for this platform.
	 *
	 * @return The copy keystroke.
	 */
	private static KeyStroke getCopyKeyStroke() {
		int key = KeyEvent.VK_C;
		int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
		return KeyStroke.getKeyStroke(key, mask);
	}

    /**
     * Returns the background color of the description window.
     *
     * @return The background color, or {@code null} if it has not been created yet.
     * @see #setDescriptionWindowColor(Color)
     */
    public Color getDescriptionWindowColor() {
        if (descWindow != null) {
            return descWindow.getBackground();
        }
        return descWindowColor;
    }


	/**
	 * 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() {
		DelegatingCellRenderer dcr = (DelegatingCellRenderer)list.
															getCellRenderer();
		return dcr.getFallbackCellRenderer();
	}


	/**
	 * Returns the selected value, or null if nothing is selected.
	 *
	 * @return The selected value.
	 */
	public Completion getSelection() {
		return isShowing() ? list.getSelectedValue() : lastSelection;
	}


	/**
	 * Inserts the currently selected completion.
	 *
	 * @see #getSelection()
	 */
	private void insertSelectedCompletion() {
		Completion comp = getSelection();
		ac.insertCompletion(comp);
	}


	/**
	 * Registers keyboard actions to listen for in the text component and
	 * intercept.
	 *
	 * @see #uninstallKeyBindings()
	 */
	private void installKeyBindings() {

		if (AutoCompletion.getDebug()) {
			System.out.println("PopupWindow: Installing keybindings");
		}
		if (keyBindingsInstalled) {
			System.err.println("Error: key bindings were already installed");
			Thread.dumpStack();
			return;
		}

		if (escapeKap==null) { // Lazily create actions.
			createKeyActionPairs();
		}

		JTextComponent comp = ac.getTextComponent();
		InputMap im = comp.getInputMap();
		ActionMap am = comp.getActionMap();

		replaceAction(im, am, KeyEvent.VK_ESCAPE, escapeKap, oldEscape);
		if (AutoCompletion.getDebug() && oldEscape.action==escapeKap.action) {
			Thread.dumpStack();
		}
		replaceAction(im, am, KeyEvent.VK_UP, upKap, oldUp);
		replaceAction(im, am, KeyEvent.VK_LEFT, leftKap, oldLeft);
		replaceAction(im, am, KeyEvent.VK_DOWN, downKap, oldDown);
		replaceAction(im, am, KeyEvent.VK_RIGHT, rightKap, oldRight);
		replaceAction(im, am, KeyEvent.VK_ENTER, enterKap, oldEnter);
		replaceAction(im, am, KeyEvent.VK_TAB, tabKap, oldTab);
		replaceAction(im, am, KeyEvent.VK_HOME, homeKap, oldHome);
		replaceAction(im, am, KeyEvent.VK_END, endKap, oldEnd);
		replaceAction(im, am, KeyEvent.VK_PAGE_UP, pageUpKap, oldPageUp);
		replaceAction(im, am, KeyEvent.VK_PAGE_DOWN, pageDownKap, oldPageDown);

		// Make Ctrl+C copy from description window.  This isn't done
		// automagically because the desc. window is not focusable, and copying
		// from text components can only be done from focused components.
		KeyStroke ks = getCopyKeyStroke();
		oldCtrlC.key = im.get(ks);
		im.put(ks, ctrlCKap.key);
		oldCtrlC.action = am.get(ctrlCKap.key);
		am.put(ctrlCKap.key, ctrlCKap.action);

		comp.addCaretListener(this);

		keyBindingsInstalled = true;

	}


	@Override
	public void mouseClicked(MouseEvent e) {
		if (e.getClickCount()==2) {
			insertSelectedCompletion();
		}
	}


	@Override
	public void mouseEntered(MouseEvent e) {
	}


	@Override
	public void mouseExited(MouseEvent e) {
	}


	@Override
	public void mousePressed(MouseEvent e) {
	}


	@Override
	public void mouseReleased(MouseEvent e) {
	}


	/**
	 * Positions the description window relative to the completion choices
	 * window.  We assume there is room on one side of the other for this
	 * entire window to fit.
	 */
	private void positionDescWindow() {

		boolean showDescWindow = descWindow!=null && ac.getShowDescWindow();
		if (!showDescWindow) {
			return;
		}

		// Don't use getLocationOnScreen() as this throws an exception if
		// window isn't visible yet, but getLocation() doesn't, and is in
		// screen coordinates!
		Point p = getLocation();
		Rectangle screenBounds = Util.getScreenBoundsForPoint(p.x, p.y);
		//Dimension screenSize = getToolkit().getScreenSize();
		//int totalH = Math.max(getHeight(), descWindow.getHeight());

		// Try to position to the right first (LTR)
		int x;
		if (ac.getTextComponentOrientation().isLeftToRight()) {
			x = getX() + getWidth() + 5;
			if (x+descWindow.getWidth()>screenBounds.x+screenBounds.width) { // doesn't fit
				x = getX() - 5 - descWindow.getWidth();
			}
		}
		else { // RTL
			x = getX() - 5 - descWindow.getWidth();
			if (xkey.
	 * @param old A buffer in which to place the old key/Action pair.
	 * @see #putBackAction(InputMap, ActionMap, int, KeyActionPair)
	 */
	private void replaceAction(InputMap im, ActionMap am, int key,
						KeyActionPair kap, KeyActionPair old) {
		KeyStroke ks = KeyStroke.getKeyStroke(key, 0);
		old.key = im.get(ks);
		im.put(ks, kap.key);
		old.action = am.get(kap.key);
		am.put(kap.key, kap.action);
	}


	/**
	 * Selects the first item in the completion list.
	 *
	 * @see #selectLastItem()
	 */
	private void selectFirstItem() {
		if (model.getSize() > 0) {
			list.setSelectedIndex(0);
			list.ensureIndexIsVisible(0);
		}
	}


	/**
	 * Selects the last item in the completion list.
	 *
	 * @see #selectFirstItem()
	 */
	private void selectLastItem() {
		int index = model.getSize() - 1;
		if (index > -1) {
			list.setSelectedIndex(index);
			list.ensureIndexIsVisible(index);
		}
	}


	/**
	 * Selects the next item in the completion list.
	 *
	 * @see #selectPreviousItem()
	 */
	private void selectNextItem() {
		int index = list.getSelectedIndex();
		if (index > -1) {
			index = (index + 1) % model.getSize();
			list.setSelectedIndex(index);
			list.ensureIndexIsVisible(index);
		}
	}


	/**
	 * Selects the completion item one "page down" from the currently selected
	 * one.
	 *
	 * @see #selectPageUpItem()
	 */
	private void selectPageDownItem() {
		int visibleRowCount = list.getVisibleRowCount();
		int i = Math.min(list.getModel().getSize()-1,
						list.getSelectedIndex()+visibleRowCount);
		list.setSelectedIndex(i);
		list.ensureIndexIsVisible(i);
	}


	/**
	 * Selects the completion item one "page up" from the currently selected
	 * one.
	 *
	 * @see #selectPageDownItem()
	 */
	private void selectPageUpItem() {
		int visibleRowCount = list.getVisibleRowCount();
		int i = Math.max(0, list.getSelectedIndex()-visibleRowCount);
		list.setSelectedIndex(i);
		list.ensureIndexIsVisible(i);
	}


	/**
	 * Selects the previous item in the completion list.
	 *
	 * @see #selectNextItem()
	 */
	private void selectPreviousItem() {
		int index = list.getSelectedIndex();
		switch (index) {
		case 0:
			index = list.getModel().getSize() - 1;
			break;
		case -1: // Check for an empty list (would be an error)
			index = list.getModel().getSize() - 1;
			if (index == -1) {
				return;
			}
			break;
		default:
			index = index - 1;
			break;
		}
		list.setSelectedIndex(index);
		list.ensureIndexIsVisible(index);
	}


	/**
	 * Sets the completions to display in the choices list.  The first
	 * completion is selected.
	 *
	 * @param completions The completions to display.
	 */
	public void setCompletions(List completions) {
		model.setContents(completions);
		selectFirstItem();
	}


	/**
	 * Sets the size of the description window.
	 *
	 * @param size The new size.  This cannot be null.
	 */
	public void setDescriptionWindowSize(Dimension size) {
		if (descWindow!=null) {
			descWindow.setSize(size);
		}
		else {
			preferredDescWindowSize = size;
		}
	}


	/**
	 * Sets the color of the description window.
	 *
	 * @param color The new color.  This cannot be null.
     * @see #getDescriptionWindowColor()
	 */
	public void setDescriptionWindowColor(Color color) {
		if (descWindow!=null) {
			descWindow.setBackground(color);
		}
		else {
			descWindowColor = color;
		}
	}


	/**
	 * 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) {
		DelegatingCellRenderer dcr = (DelegatingCellRenderer)list.
													getCellRenderer();
		dcr.setFallbackCellRenderer(renderer);
	}


	/**
	 * Sets the location of this window to be "good" relative to the specified
	 * rectangle.  That rectangle should be the location of the text
	 * component's caret, in screen coordinates.
	 *
	 * @param r The text component's caret position, in screen coordinates.
	 */
	public void setLocationRelativeTo(Rectangle r) {

		// Multi-monitor support - make sure the completion window (and
		// description window, if applicable) both fit in the same window in
		// a multi-monitor environment.  To do this, we decide which monitor
		// the rectangle "r" is in, and use that one (just pick top-left corner
		// as the defining point).
		Rectangle screenBounds = Util.getScreenBoundsForPoint(r.x, r.y);
		//Dimension screenSize = getToolkit().getScreenSize();

		boolean showDescWindow = descWindow!=null && ac.getShowDescWindow();
		int totalH = getHeight();
		if (showDescWindow) {
			totalH = Math.max(totalH, descWindow.getHeight());
		}

		// Try putting our stuff "below" the caret first.  We assume that the
		// entire height of our stuff fits on the screen one way or the other.
		aboveCaret = false;
		int y = r.y + r.height + VERTICAL_SPACE;
		if (y+totalH>screenBounds.height) {
			y = r.y - VERTICAL_SPACE - getHeight();
			aboveCaret = true;
		}

		// Get x-coordinate of completions.  Try to align left edge with the
		// caret first.
		int x = r.x;
		if (!ac.getTextComponentOrientation().isLeftToRight()) {
			x -= getWidth(); // RTL => align right edge
		}
		if (xscreenBounds.x+screenBounds.width) { // completions don't fit
			x = screenBounds.x + screenBounds.width - getWidth();
		}

		setLocation(x, y);

		// Position the description window, if necessary.
		if (showDescWindow) {
			positionDescWindow();
		}

	}


	/**
	 * Toggles the visibility of this popup window.
	 *
	 * @param visible Whether this window should be visible.
	 */
	@Override
	public void setVisible(boolean visible) {

		if (visible!=isVisible()) {

			if (visible) {
				installKeyBindings();
				lastLine = ac.getLineOfCaret();
				selectFirstItem();
				if (descWindow==null && ac.getShowDescWindow()) {
					descWindow = createDescriptionWindow();
					positionDescWindow();
				}
				// descWindow needs a kick-start the first time it's displayed.
				// Also, the newly-selected item in the choices list is
				// probably different from the previous one anyway.
				if (descWindow!=null) {
					Completion c = list.getSelectedValue();
					if (c!=null) {
						descWindow.setDescriptionFor(c);
					}
				}
			}
			else {
				uninstallKeyBindings();
			}

			super.setVisible(visible);

			// Some languages, such as Java, can use quite a lot of memory
			// when displaying hundreds of completion choices.  We proactively
			// clear our list model here to make them available for GC.
			// Otherwise, they stick around, and consider the following:  a
			// user starts code-completion for Java 5 SDK classes, then hides
			// the dialog, then changes the "class path" to use a Java 6 SDK
			// instead.  On pressing Ctrl+space, a new array of Completions is
			// created.  If this window holds on to the previous Completions,
			// you're getting roughly 2x the necessary Completions in memory
			// until the Completions are actually passed to this window.
			if (!visible) { // Do after super.setVisible(false)
				lastSelection = list.getSelectedValue();
				model.clear();
			}
			else {
				list.repaint(); // https://github.com/bobbylight/AutoComplete/issues/70
			}

			// Must set descWindow's visibility one way or the other each time,
			// because of the way child JWindows' visibility is handled - in
			// some ways it's dependent on the parent, in other ways it's not.
			if (descWindow!=null) {
				descWindow.setVisible(visible && ac.getShowDescWindow());
			}

		}

	}


	/**
	 * Stops intercepting certain keystrokes from the text component.
	 *
	 * @see #installKeyBindings()
	 */
	private void uninstallKeyBindings() {

		if (AutoCompletion.getDebug()) {
			System.out.println("PopupWindow: Removing keybindings");
		}
		if (!keyBindingsInstalled) {
			return;
		}

		JTextComponent comp = ac.getTextComponent();
		InputMap im = comp.getInputMap();
		ActionMap am = comp.getActionMap();

		putBackAction(im, am, KeyEvent.VK_ESCAPE, oldEscape);
		putBackAction(im, am, KeyEvent.VK_UP, oldUp);
		putBackAction(im, am, KeyEvent.VK_DOWN, oldDown);
		putBackAction(im, am, KeyEvent.VK_LEFT, oldLeft);
		putBackAction(im, am, KeyEvent.VK_RIGHT, oldRight);
		putBackAction(im, am, KeyEvent.VK_ENTER, oldEnter);
		putBackAction(im, am, KeyEvent.VK_TAB, oldTab);
		putBackAction(im, am, KeyEvent.VK_HOME, oldHome);
		putBackAction(im, am, KeyEvent.VK_END, oldEnd);
		putBackAction(im, am, KeyEvent.VK_PAGE_UP, oldPageUp);
		putBackAction(im, am, KeyEvent.VK_PAGE_DOWN, oldPageDown);

		// Ctrl+C
		KeyStroke ks = getCopyKeyStroke();
		am.put(im.get(ks), oldCtrlC.action); // Original action
		im.put(ks, oldCtrlC.key); // Original key

		comp.removeCaretListener(this);

		keyBindingsInstalled = false;

	}


	/**
	 * Updates the {@code LookAndFeel} of this window and the description
	 * window.
	 */
	public void updateUI() {
		SwingUtilities.updateComponentTreeUI(this);
		if (descWindow!=null) {
			descWindow.updateUI();
		}
	}


	/**
	 * Called when a new item is selected in the popup list.
	 *
	 * @param e The event.
	 */
	@Override
	public void valueChanged(ListSelectionEvent e) {
		if (!e.getValueIsAdjusting()) {
			Completion value = list.getSelectedValue();
			if (value!=null && descWindow!=null) {
				descWindow.setDescriptionFor(value);
				positionDescWindow();
			}
		}
	}


	/**
	 * Copies text.
	 */
	class CopyAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			boolean doNormalCopy = false;
			if (descWindow!=null && descWindow.isVisible()) {
				doNormalCopy = !descWindow.copy();
			}
			if (doNormalCopy) {
				ac.getTextComponent().copy();
			}
		}

	}


	/**
	 * Selects the next item.
	 */
	class DownAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectNextItem();
			}
		}

	}


	/**
	 * Selects the previous item.
	 */
	class EndAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectLastItem();
			}
		}

	}


	/**
	 * Inserts the selected completion.
	 */
	class EnterAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				insertSelectedCompletion();
			}
		}

	}


	/**
	 * Hides this popup window.
	 */
	class EscapeAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				setVisible(false);
			}
		}

	}


	/**
	 * Selects the first item.
	 */
	class HomeAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectFirstItem();
			}
		}

	}


	/**
	 * A mapping from a key (an Object) to an Action.
	 */
	private static class KeyActionPair {

		private Object key;
		private Action action;

		KeyActionPair() {
		}

		KeyActionPair(Object key, Action a) {
			this.key = key;
			this.action = a;
		}

	}


	/**
	 * Moves the caret to the left and updates the completion list accordingly.
	 */
	class LeftAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				JTextComponent comp = ac.getTextComponent();
				Caret c = comp.getCaret();
				int dot = c.getDot();
				if (dot > 0) {
					c.setDot(--dot);
					// Ensure moving left hasn't moved us up a line, thus
					// hiding the popup window.
					if (comp.isVisible()) {
						if (lastLine!=-1) {
							doAutocomplete();
						}
					}
				}
			}
		}

	}


	/**
	 * Moves down one page in the completion list.
	 */
	class PageDownAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectPageDownItem();
			}
		}

	}


	/**
	 * Moves up one page in the completion list.
	 */
	class PageUpAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectPageUpItem();
			}
		}

	}


	/**
	 * The actual list of completion choices in this popup window.
	 */
	private class PopupList extends JList {

		PopupList(CompletionListModel model) {
			super(model);
		}

		@Override
		public void setUI(ListUI ui) {
			if (Util.getUseSubstanceRenderers() &&
					SUBSTANCE_LIST_UI.equals(ui.getClass().getName())) {
				// Substance requires its special ListUI be installed for
				// its renderers to actually render (!), but long completion
				// lists (e.g. PHPCompletionProvider in RSTALanguageSupport)
				// will simply populate too slowly on initial display (when
				// calculating preferred size of all items), so in this case
				// we give a prototype cell value.
				CompletionProvider p = ac.getCompletionProvider();
				BasicCompletion bc = new BasicCompletion(p, "Hello world");
				setPrototypeCellValue(bc);
			}
			else {
				// Our custom UI that is faster for long HTML completion lists.
				ui = new FastListUI();
				setPrototypeCellValue(null);
			}
			super.setUI(ui);
		}

	}


	/**
	 * Moves the caret to the right and updates the completion list accordingly.
	 */
	class RightAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				JTextComponent comp = ac.getTextComponent();
				Caret c = comp.getCaret();
				int dot = c.getDot();
				if (dot < comp.getDocument().getLength()) {
					c.setDot(++dot);
					// Ensure moving right hasn't moved us up a line, thus
					// hiding the popup window.
					if (comp.isVisible()) {
						if (lastLine!=-1) {
							doAutocomplete();
						}
					}
				}
			}
		}

	}


	/**
	 * Moves up in the completion list.
	 */
	class UpAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			if (isVisible()) {
				selectPreviousItem();
			}
		}

	}


}