// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.tagging.ac; import java.awt.Component; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.EventObject; import java.util.Objects; import javax.swing.ComboBoxEditor; import javax.swing.JTable; import javax.swing.event.CellEditorListener; import javax.swing.table.TableCellEditor; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.PlainDocument; import javax.swing.text.StyleConstants; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.gui.util.CellEditorSupport; import org.openstreetmap.josm.gui.widgets.JosmTextField; /** * AutoCompletingTextField is a text field with autocompletion behaviour. It * can be used as table cell editor in {@link JTable}s. * * Autocompletion is controlled by a list of {@link AutoCompletionListItem}s * managed in a {@link AutoCompletionList}. * * @since 1762 */ public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor { private Integer maxChars; /** * The document model for the editor */ class AutoCompletionDocument extends PlainDocument { @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { // If a maximum number of characters is specified, avoid to exceed it if (maxChars != null && str != null && getLength() + str.length() > maxChars) { int allowedLength = maxChars-getLength(); if (allowedLength > 0) { str = str.substring(0, allowedLength); } else { return; } } if (autoCompletionList == null) { super.insertString(offs, str, a); return; } // input method for non-latin characters (e.g. scim) if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) { super.insertString(offs, str, a); 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 < getLength()) { super.insertString(offs, str, a); return; } String currentText = getText(0, getLength()); // if the text starts with a number we don't autocomplete if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) { try { Long.parseLong(str); if (currentText.isEmpty()) { // we don't autocomplete on numbers super.insertString(offs, str, a); return; } Long.parseLong(currentText); super.insertString(offs, str, a); return; } catch (NumberFormatException e) { // either the new text or the current text isn't a number. We continue with autocompletion Main.trace(e); } } String prefix = currentText.substring(0, offs); autoCompletionList.applyFilter(prefix+str); if (autoCompletionList.getFilteredSize() > 0 && !Objects.equals(str, noAutoCompletionString)) { // there are matches. Insert the new text and highlight the auto completed suffix String matchingString = autoCompletionList.getFilteredItem(0).getValue(); remove(0, getLength()); super.insertString(0, matchingString, a); // highlight from insert position to end position to put the caret at the end setCaretPosition(offs + str.length()); moveCaretPosition(getLength()); } else { // there are no matches. Insert the new text, do not highlight // String newText = prefix + str; remove(0, getLength()); super.insertString(0, newText, a); setCaretPosition(getLength()); } } } /** the auto completion list user input is matched against */ protected AutoCompletionList autoCompletionList; /** a string which should not be auto completed */ protected String noAutoCompletionString; @Override protected Document createDefaultModel() { return new AutoCompletionDocument(); } protected final void init() { addFocusListener( new FocusAdapter() { @Override public void focusGained(FocusEvent e) { selectAll(); applyFilter(getText()); } } ); addKeyListener( new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { if (getText().isEmpty()) { applyFilter(""); } } } ); tableCellEditorSupport = new CellEditorSupport(this); } /** * Constructs a new {@code AutoCompletingTextField}. */ public AutoCompletingTextField() { this(0); } /** * Constructs a new {@code AutoCompletingTextField}. * @param columns the number of columns to use to calculate the preferred width; * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation */ public AutoCompletingTextField(int columns) { this(columns, true); } /** * Constructs a new {@code AutoCompletingTextField}. * @param columns the number of columns to use to calculate the preferred width; * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor */ public AutoCompletingTextField(int columns, boolean undoRedo) { super(null, null, columns, undoRedo); init(); } protected void applyFilter(String filter) { if (autoCompletionList != null) { autoCompletionList.applyFilter(filter); } } /** * Returns the auto completion list. * @return the auto completion list; may be null, if no auto completion list is set */ public AutoCompletionList getAutoCompletionList() { return autoCompletionList; } /** * Sets the auto completion list. * @param autoCompletionList the auto completion list; if null, auto completion is * disabled */ public void setAutoCompletionList(AutoCompletionList autoCompletionList) { this.autoCompletionList = autoCompletionList; } @Override public Component getEditorComponent() { return this; } @Override public Object getItem() { return getText(); } @Override public void setItem(Object anObject) { if (anObject == null) { setText(""); } else { setText(anObject.toString()); } } @Override public void setText(String t) { // disallow auto completion for this explicitly set string this.noAutoCompletionString = t; super.setText(t); } /** * Sets the maximum number of characters allowed. * @param max maximum number of characters allowed * @since 5579 */ public void setMaxChars(Integer max) { maxChars = max; } /* ------------------------------------------------------------------------------------ */ /* TableCellEditor interface */ /* ------------------------------------------------------------------------------------ */ private transient CellEditorSupport tableCellEditorSupport; private String originalValue; @Override public void addCellEditorListener(CellEditorListener l) { tableCellEditorSupport.addCellEditorListener(l); } protected void rememberOriginalValue(String value) { this.originalValue = value; } protected void restoreOriginalValue() { setText(originalValue); } @Override public void removeCellEditorListener(CellEditorListener l) { tableCellEditorSupport.removeCellEditorListener(l); } @Override public void cancelCellEditing() { restoreOriginalValue(); tableCellEditorSupport.fireEditingCanceled(); } @Override public Object getCellEditorValue() { return getText(); } @Override public boolean isCellEditable(EventObject anEvent) { return true; } @Override public boolean shouldSelectCell(EventObject anEvent) { return true; } @Override public boolean stopCellEditing() { tableCellEditorSupport.fireEditingStopped(); return true; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { setText(value == null ? "" : value.toString()); rememberOriginalValue(getText()); return this; } }