// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.tagging.ac;
import java.awt.Component;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.im.InputContext;
import java.util.Collection;
import java.util.Locale;
import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
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;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
import org.openstreetmap.josm.gui.widgets.JosmComboBox;
/**
* Auto-completing ComboBox.
* @author guilhem.bonnefille@gmail.com
* @since 272
*/
public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionListItem> {
private boolean autocompleteEnabled = true;
private int maxTextLength = -1;
private boolean useFixedLocale;
private final transient InputContext privateInputContext = InputContext.getInstance();
static final class InnerFocusListener implements FocusListener {
private final JTextComponent editorComponent;
InnerFocusListener(JTextComponent editorComponent) {
this.editorComponent = editorComponent;
}
@Override
public void focusLost(FocusEvent e) {
if (Main.map != null) {
Main.map.keyDetector.setEnabled(true);
}
}
@Override
public void focusGained(FocusEvent e) {
if (Main.map != null) {
Main.map.keyDetector.setEnabled(false);
}
// save unix system selection (middle mouse paste)
Clipboard sysSel = ClipboardUtils.getSystemSelection();
if (sysSel != null) {
Transferable old = ClipboardUtils.getClipboardContent(sysSel);
editorComponent.selectAll();
if (old != null) {
sysSel.setContents(old, null);
}
} else {
editorComponent.selectAll();
}
}
}
/**
* Auto-complete a JosmComboBox.
* <br>
* Inspired by <a href="http://www.orbital-computer.de/JComboBox">Thomas Bierhance example</a>.
*/
class AutoCompletingComboBoxDocument extends PlainDocument {
private final JosmComboBox<AutoCompletionListItem> comboBox;
private boolean selecting;
/**
* Constructs a new {@code AutoCompletingComboBoxDocument}.
* @param comboBox the combobox
*/
AutoCompletingComboBoxDocument(final JosmComboBox<AutoCompletionListItem> 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 {
// TODO get rid of code duplication w.r.t. AutoCompletingTextField.AutoCompletionDocument.insertString
if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
return;
if (maxTextLength > -1 && str.length()+getLength() > maxTextLength)
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;
// if the current offset isn't at the end of the document we don't autocomplete.
// If a highlighted autocompleted suffix was present and we get here Swing has
// already removed it from the document. getLength() therefore doesn't include the autocompleted suffix.
if (offs + str.length() < getLength()) {
return;
}
int size = getLength();
int start = offs+str.length();
int end = start;
String curText = getText(0, size);
// item for lookup and selection
Object item;
// if the text is a number we don't autocomplete
if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) {
try {
Long.parseLong(str);
if (!curText.isEmpty())
Long.parseLong(curText);
item = lookupItem(curText, true);
} catch (NumberFormatException e) {
// either the new text or the current text isn't a number. We continue with autocompletion
item = lookupItem(curText, false);
}
} else {
item = lookupItem(curText, false);
}
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();
}
}
final JTextComponent editorComponent = comboBox.getEditorComponent();
// save unix system selection (middle mouse paste)
Clipboard sysSel = ClipboardUtils.getSystemSelection();
if (sysSel != null) {
Transferable old = ClipboardUtils.getClipboardContent(sysSel);
editorComponent.select(start, end);
if (old != null) {
sysSel.setContents(old, null);
}
} else {
editorComponent.select(start, end);
}
}
private void setSelectedItem(Object item) {
selecting = true;
comboBox.setSelectedItem(item);
selecting = false;
}
private Object lookupItem(String pattern, boolean match) {
ComboBoxModel<AutoCompletionListItem> model = comboBox.getModel();
AutoCompletionListItem bestItem = null;
for (int i = 0, n = model.getSize(); i < n; i++) {
AutoCompletionListItem currentItem = model.getElementAt(i);
if (currentItem.getValue().equals(pattern))
return currentItem;
if (!match && currentItem.getValue().startsWith(pattern)
&& (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0)) {
bestItem = currentItem;
}
}
return bestItem; // may be null
}
}
/**
* Creates a <code>AutoCompletingComboBox</code> with a default prototype display value.
*/
public AutoCompletingComboBox() {
this("Foo");
}
/**
* Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value.
* @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once
* before displaying a scroll bar. It also affects the initial width of the combo box.
* @since 5520
*/
public AutoCompletingComboBox(String prototype) {
super(new AutoCompletionListItem(prototype));
setRenderer(new AutoCompleteListCellRenderer());
final JTextComponent editorComponent = this.getEditorComponent();
editorComponent.setDocument(new AutoCompletingComboBoxDocument(this));
editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
}
/**
* Sets the maximum text length.
* @param length the maximum text length in number of characters
*/
public void setMaxTextLength(int length) {
this.maxTextLength = length;
}
/**
* Convert the selected item into a String that can be edited in the editor component.
*
* @param cbEditor the editor
* @param item excepts AutoCompletionListItem, String and null
*/
@Override
public void configureEditor(ComboBoxEditor cbEditor, Object item) {
if (item == null) {
cbEditor.setItem(null);
} else if (item instanceof String) {
cbEditor.setItem(item);
} else if (item instanceof AutoCompletionListItem) {
cbEditor.setItem(((AutoCompletionListItem) item).getValue());
} else
throw new IllegalArgumentException("Unsupported item: "+item);
}
/**
* Selects a given item in the ComboBox model
* @param item excepts 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 = getModel().getElementAt(i);
if (s.equals(acItem.getValue())) {
super.setSelectedItem(acItem);
return;
}
}
super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPriority.UNKNOWN));
} else {
throw new IllegalArgumentException("Unsupported item: "+item);
}
}
/**
* Sets the items of the combobox to the given {@code String}s.
* @param elems String items
*/
public void setPossibleItems(Collection<String> elems) {
DefaultComboBoxModel<AutoCompletionListItem> model = (DefaultComboBoxModel<AutoCompletionListItem>) this.getModel();
Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
model.removeAllElements();
for (String elem : elems) {
model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPriority.UNKNOWN));
}
// disable autocomplete to prevent unnecessary actions in AutoCompletingComboBoxDocument#insertString
autocompleteEnabled = false;
this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013)
autocompleteEnabled = true;
}
/**
* Sets the items of the combobox to the given {@code AutoCompletionListItem}s.
* @param elems AutoCompletionListItem items
*/
public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
DefaultComboBoxModel<AutoCompletionListItem> model = (DefaultComboBoxModel<AutoCompletionListItem>) this.getModel();
Object oldValue = getSelectedItem();
Object editorOldValue = this.getEditor().getItem();
model.removeAllElements();
for (AutoCompletionListItem elem : elems) {
model.addElement(elem);
}
setSelectedItem(oldValue);
this.getEditor().setItem(editorOldValue);
}
/**
* Determines if autocompletion is enabled.
* @return {@code true} if autocompletion is enabled, {@code false} otherwise.
*/
public final boolean isAutocompleteEnabled() {
return autocompleteEnabled;
}
protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
this.autocompleteEnabled = autocompleteEnabled;
}
/**
* If the locale is fixed, English keyboard layout will be used by default for this combobox
* all other components can still have different keyboard layout selected
* @param f fixed locale
*/
public void setFixedLocale(boolean f) {
useFixedLocale = f;
if (useFixedLocale) {
Locale oldLocale = privateInputContext.getLocale();
Main.info("Using English input method");
if (!privateInputContext.selectInputMethod(new Locale("en", "US"))) {
// Unable to use English keyboard layout, disable the feature
Main.warn("Unable to use English input method");
useFixedLocale = false;
if (oldLocale != null) {
Main.info("Restoring input method to " + oldLocale);
if (!privateInputContext.selectInputMethod(oldLocale)) {
Main.warn("Unable to restore input method to " + oldLocale);
}
}
}
}
}
@Override
public InputContext getInputContext() {
if (useFixedLocale) {
return privateInputContext;
}
return super.getInputContext();
}
/**
* ListCellRenderer for AutoCompletingComboBox
* renders an AutoCompletionListItem by showing only the string value part
*/
public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer<AutoCompletionListItem> {
/**
* Constructs a new {@code AutoCompleteListCellRenderer}.
*/
public AutoCompleteListCellRenderer() {
setOpaque(true);
}
@Override
public Component getListCellRendererComponent(
JList<? extends AutoCompletionListItem> list,
AutoCompletionListItem item,
int index,
boolean isSelected,
boolean cellHasFocus) {
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
setText(item.getValue());
return this;
}
}
}