// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.gui.tagging.ac;
import java.awt.Component;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Collection;
import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.StyleConstants;
/**
* @author guilhem.bonnefille@gmail.com
*/
@SuppressWarnings("serial")
public class AutoCompletingComboBox extends JComboBox {
private boolean autocompleteEnabled = true;
/**
* Auto-complete a JComboBox.
*
* Inspired by http://www.orbital-computer.de/JComboBox/
*/
class AutoCompletingComboBoxDocument extends PlainDocument {
private JComboBox comboBox;
private boolean selecting = false;
public AutoCompletingComboBoxDocument(final JComboBox comboBox) {
this.comboBox = comboBox;
}
@Override
public void remove(int offs, int len) throws BadLocationException {
if (selecting)
return;
super.remove(offs, len);
}
@Override
public void insertString(int offs, String str, AttributeSet a)
throws BadLocationException {
if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
return;
boolean initial = (offs == 0 && getLength() == 0 && str.length() > 1);
super.insertString(offs, str, a);
// return immediately when selecting an item
// Note: this is done after calling super method because we need
// ActionListener informed
if (selecting)
return;
if (!autocompleteEnabled)
return;
// input method for non-latin characters (e.g. scim)
if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
return;
int size = getLength();
int start = offs + str.length();
int end = start;
String curText = getText(0, size);
// if the text starts with a number we don't autocomplete
// TODO deal with numbers
/*
* if (Main.pref.getBoolean("autocomplete.dont_complete_numbers",
* true)) { try { Long.parseLong(str); if (curText.length() == 0) //
* we don't autocomplete on numbers return; Long.parseLong(curText);
* return; } catch (NumberFormatException e) { // either the new
* text or the current text isn't a number. We continue with //
* autocompletion } }
*/
// lookup and select a matching item
Object item = lookupItem(curText);
setSelectedItem(item);
if (initial) {
start = 0;
}
if (item != null) {
String newText = ((AutoCompletionListItem) item).getValue();
if (!newText.equals(curText)) {
selecting = true;
super.remove(0, size);
super.insertString(0, newText, a);
selecting = false;
start = size;
end = getLength();
}
}
JTextComponent editor = (JTextComponent) comboBox.getEditor()
.getEditorComponent();
editor.setSelectionStart(start);
editor.setSelectionEnd(end);
}
private void setSelectedItem(Object item) {
selecting = true;
comboBox.setSelectedItem(item);
selecting = false;
}
private Object lookupItem(String pattern) {
ComboBoxModel model = comboBox.getModel();
AutoCompletionListItem bestItem = null;
for (int i = 0, n = model.getSize(); i < n; i++) {
AutoCompletionListItem currentItem = (AutoCompletionListItem) model
.getElementAt(i);
if (currentItem.getValue().equals(pattern)) {
return currentItem;
}
if (currentItem.getValue().startsWith(pattern)) {
if (bestItem == null
|| currentItem.getPriority().compareTo(
bestItem.getPriority()) > 0) {
bestItem = currentItem;
}
}
}
return bestItem; // may be null
}
}
/**
* Creates a new combo box with auto completion support.
*/
public AutoCompletingComboBox() {
setRenderer(new AutoCompleteListCellRenderer());
final JTextComponent editor = (JTextComponent) this.getEditor()
.getEditorComponent();
editor.setDocument(new AutoCompletingComboBoxDocument(this));
editor.addFocusListener(new FocusListener() {
public void focusLost(FocusEvent e) {
}
public void focusGained(FocusEvent e) {
editor.selectAll();
}
});
}
/**
* Convert the selected item into a String that can be edited in the editor
* component.
*
* @param editor
* the editor
* @param item
* accepts AutoCompletionListItem, String and null
*/
@Override
public void configureEditor(ComboBoxEditor editor, Object item) {
if (item == null) {
editor.setItem(null);
} else if (item instanceof String) {
editor.setItem(item);
} else if (item instanceof AutoCompletionListItem) {
editor.setItem(((AutoCompletionListItem) item).getValue());
} else
throw new IllegalArgumentException();
}
/**
* Selects a given item in the ComboBox model
*
* @param item
* accepts AutoCompletionListItem, String and null
*/
@Override
public void setSelectedItem(Object item) {
if (item == null) {
super.setSelectedItem(null);
} else if (item instanceof AutoCompletionListItem) {
super.setSelectedItem(item);
} else if (item instanceof String) {
String s = (String) item;
// find the string in the model or create a new item
for (int i = 0; i < getModel().getSize(); i++) {
AutoCompletionListItem acItem = (AutoCompletionListItem) getModel()
.getElementAt(i);
if (s.equals(acItem.getValue())) {
super.setSelectedItem(acItem);
return;
}
}
super.setSelectedItem(new AutoCompletionListItem(s,
AutoCompletionItemPritority.UNKNOWN));
} else
throw new IllegalArgumentException();
}
/**
* sets the items of the combobox to the given strings
*/
public void setPossibleItems(Collection<String> elems) {
DefaultComboBoxModel model = (DefaultComboBoxModel) this.getModel();
Object oldValue = this.getEditor().getItem();
model.removeAllElements();
for (String elem : elems) {
model.addElement(new AutoCompletionListItem(elem,
AutoCompletionItemPritority.UNKNOWN));
}
this.getEditor().setItem(oldValue);
}
/**
* sets the items of the combobox to the given AutoCompletionListItems
*/
public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
DefaultComboBoxModel model = (DefaultComboBoxModel) this.getModel();
Object oldValue = this.getEditor().getItem();
model.removeAllElements();
for (AutoCompletionListItem elem : elems) {
model.addElement(elem);
}
this.getEditor().setItem(oldValue);
}
protected boolean isAutocompleteEnabled() {
return autocompleteEnabled;
}
protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
this.autocompleteEnabled = autocompleteEnabled;
}
/**
* ListCellRenderer for AutoCompletingComboBox renders an
* AutoCompletionListItem by showing only the string value part
*/
public static class AutoCompleteListCellRenderer extends JLabel implements
ListCellRenderer {
/**
* Creates a new instance of the cell renderer.
*/
public AutoCompleteListCellRenderer() {
setOpaque(true);
}
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
AutoCompletionListItem item = (AutoCompletionListItem) value;
setText(item.getValue());
return this;
}
}
}