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