// Copyright (c) 2006 - 2008, Markus Strauch. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF // THE POSSIBILITY OF SUCH DAMAGE. package net.sf.sdedit.ui.components; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.List; import javax.swing.JTextPane; import javax.swing.SwingUtilities; /** * An <tt>AutoCompletion</tt> object can be added to a <tt>JTextPane</tt> as * a key listener. When the tab key is pressed, and there is a character to the * left to the cursor and whitespace to its right, the associated * {@linkplain SuggestionProvider}'s (see * {@linkplain #AutoCompletion(JTextPane, SuggestionProvider, char...)}) method * {@linkplain SuggestionProvider#getSuggestions(String)} is called with the * string to the left of the cursor (separated by whitespace or one of the given * delimiters) as a parameter. The strings that are returned by this method all * have the parameter string as a prefix. If there is at least one string, the * prefix is replaced by it. If there are even more, successive strokes of the * tab key will cycle through all strings. * * @author Markus Strauch * */ public class AutoCompletion extends KeyAdapter { private ArrayList<String> suggestions; private int counter; private JTextPane textArea; private SuggestionProvider provider; private int wordBegin; private int wordEnd; private State state; private char[] delimiters; private enum State { /** * The key most recently typed was not the trigger key (TAB) or the * cursor is not at an appropriate position or there is no suggestion * for the prefix to the left of the cursor. */ INIT, /** * The most recently typed key was the trigger key (TAB) and there is * more than one suggestion for the prefix to the left of the cursor. * The string to the left of the cursor is one of the suggestions, when * typing the trigger key again, another suggestion will appear. */ CHOOSING } /** * Creates a new <tt>AutoCompletion</tt>. * * @param textPane * the JTextPane in which text should be substituted NOTE: its * <tt>getText()</tt> method must return a string with a single * '\n' as end-of-line character * @param provider * for providing suggestions of what could be substituted * @param delimiters * characters that are to be interpreted as the left limit (not * inclusive) of a prefix that might be substituted */ public AutoCompletion(JTextPane textPane, SuggestionProvider provider, char... delimiters) { this.textArea = textPane; textPane.addKeyListener(this); this.provider = provider; suggestions = new ArrayList<String>(); this.delimiters = delimiters; state = State.INIT; } private boolean isLimit(char c) { if (Character.isWhitespace(c)) { return true; } for (char d : delimiters) { if (d == c) { return true; } } return false; } private boolean isTrigger (KeyEvent e) { return e.getKeyCode() == KeyEvent.VK_TAB || e.getKeyCode() == KeyEvent.VK_SPACE && e.isShiftDown() && e.isControlDown(); } private String findPrefix() { StringBuffer prefix = new StringBuffer(); int pos = textArea.getCaretPosition() - 1; wordEnd = pos + 1; String text = textArea.getText(); char c; while (pos >= 0 && !isLimit((c = text.charAt(pos)))) { prefix.insert(0, c); pos--; } wordBegin = pos + 1; String pref = prefix.toString().trim(); return pref; } private boolean tabPressed() { boolean act = false; if (state == State.INIT) { String prefix = findPrefix(); if (prefix.length() > 0) { suggestions.clear(); suggestions.addAll(provider.getSuggestions(prefix)); if (suggestions.size() > 0) { replaceBy(suggestions.get(0)); act = true; } if (suggestions.size() > 1) { counter = 0; state = State.CHOOSING; } } } else { if (suggestions.size() > 0) { // this should actually not be necessary act = true; counter = (counter + 1) % suggestions.size(); replaceBy(suggestions.get(counter)); } } return act; } private void replaceBy(final String suggestion) { SwingUtilities.invokeLater(new Runnable() { public void run() { textArea.setSelectionStart(wordBegin); textArea.setSelectionEnd(wordEnd); textArea.replaceSelection(suggestion); wordEnd = wordBegin + suggestion.length(); } }); } /** * Implements the behaviour as described in the class comment: * {@linkplain AutoCompletion}. * * @param e */ public void keyPressed(KeyEvent e) { if (isTrigger(e)) { String text = textArea.getText(); int i = textArea.getCaretPosition(); if (text.length() > 0 && !Character.isWhitespace(text.charAt(i - 1)) && (i == text.length() || isLimit(text.charAt(i)))) { if (tabPressed()) { e.consume(); } } } else { state = State.INIT; } } /** * An interface for objects that provide suggestions how a prefix could be * completed to a known string. */ public interface SuggestionProvider { /** * Returns a list of known strings that have the given string as a * prefix. * * @param prefix * the prefix * @return a list of known strings that have the given string as a * prefix */ public List<String> getSuggestions(String prefix); } }