/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools.autocomplete; import java.awt.Component; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Locale; import java.util.Vector; import javax.swing.ComboBoxModel; import javax.swing.JComboBox; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.plaf.basic.BasicComboBoxEditor; import javax.swing.text.BadLocationException; import javax.swing.text.Document; /** * Addition which allows a JComboBox to auto fills itself if the entered string is prefix of any combo box item. * * @author Marco Boeck, Sebastian Land * */ public class AutoCompleteComboBoxAddition { private final class AutoCompletionDocumentListener implements DocumentListener { @Override public void insertUpdate(DocumentEvent e) { try { if (!allowAutoFill) { // see #focusGained() down below return; } Vector<String> vectorOfStrings = new Vector<String>(); for (int i = 0; i < comboBox.getModel().getSize(); i++) { vectorOfStrings.add(String.valueOf(comboBox.getModel().getElementAt(i))); } Document document = e.getDocument(); String documentText = document.getText(0, document.getLength()); final String result = checkForMatch(documentText, vectorOfStrings, caseSensitive); final String newString = (result == null) ? documentText : result; final int startSelect = document.getLength(); final int endSelect = newString.length(); final JTextField editorComponent = (JTextField) comboBox.getEditor().getEditorComponent(); if (startSelect == e.getOffset() + e.getLength()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { comboBox.getModel().setSelectedItem(newString); editorComponent.getDocument().removeDocumentListener(docListener); editorComponent.setText(newString); editorComponent.getDocument().addDocumentListener(docListener); editorComponent.setCaretPosition(startSelect); editorComponent.setSelectionStart(startSelect); editorComponent.setSelectionEnd(endSelect); } }); } } catch (BadLocationException e1) { } } @Override public void removeUpdate(DocumentEvent e) { // not needed, only match on new input } @Override public void changedUpdate(DocumentEvent e) { // never fired for Document } } private final class AutoCompletionComboBoxEditor extends BasicComboBoxEditor { @Override public void setItem(Object anObject) { ((JTextField) getEditorComponent()).getDocument().removeDocumentListener(docListener); super.setItem(anObject); ((JTextField) getEditorComponent()).getDocument().addDocumentListener(docListener); } } /** only set this to true after the first time we gained the focus */ private boolean allowAutoFill; /** if set to true, matching will be case sensitive; false otherwise */ private boolean caseSensitive; /** the document listener for the combo box editor */ private final DocumentListener docListener; /** the JComboBox to which it is attached */ private final JComboBox comboBox; private final BasicComboBoxEditor comboBoxEditor; private static final long serialVersionUID = 1L; /** * Adds an auto completion feature to the given JComboBox. Will set {@link JComboBox#setEditable(boolean)} to true. * When the user enters one or more characters, it will automatically fill in the first match where the characters * are a prefix of an item from the {@link ComboBoxModel}. As soon as the constructor is called * * @param box * the JComboBox which should get the auto completion feature */ public AutoCompleteComboBoxAddition(JComboBox box) { comboBox = box; caseSensitive = false; allowAutoFill = false; comboBox.setEditable(true); comboBoxEditor = new AutoCompletionComboBoxEditor(); comboBox.setEditor(comboBoxEditor); docListener = new AutoCompletionDocumentListener(); ((JTextField) comboBox.getEditor().getEditorComponent()).getDocument().addDocumentListener(docListener); // workaround for java bug #6433257 for (Component c : comboBox.getComponents()) { c.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { // if we did not prevent this, it would auto fill each time the combo box is re-created after a // custom value has been entered, // therefore overwriting any value which happens to be a prefix of something with the first match. // this happens due to some RapidLookComboBoxEditor mechanics. allowAutoFill = true; } @Override public void focusLost(FocusEvent e) { allowAutoFill = false; final JTextField editorComponent = (JTextField) comboBox.getEditor().getEditorComponent(); editorComponent.setCaretPosition(editorComponent.getCaretPosition()); } }); } } /** * Sets the auto-fill feature to case sensitive. * * @param caseSensitive * If set to true, matching is case sensitive; false otherwise */ public void setCaseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; } /** * Returns the first string which starts with the given String. * * @param givenString * the result must start with this string * @param collectionOfStrings * the collection of strings to match against the given string * @param caseSensitive * @see {@link #setCaseSensitive(boolean)} * @return the first match or {@code null} if no match is found */ private String checkForMatch(String givenString, Collection<String> collectionOfStrings, boolean caseSensitive) { if (givenString == null || collectionOfStrings == null) { return null; } String returnString = null; Collections.sort(new ArrayList<String>(collectionOfStrings)); for (String vectorString : collectionOfStrings) { if (vectorString == null) { continue; } if (caseSensitive) { if (vectorString.startsWith(givenString)) { returnString = vectorString; break; } } else { if (vectorString.toLowerCase(Locale.ENGLISH).startsWith(givenString.toLowerCase(Locale.ENGLISH))) { returnString = vectorString; break; } } } return returnString; } }