001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.ac;
003
004import java.awt.Component;
005import java.awt.event.FocusAdapter;
006import java.awt.event.FocusEvent;
007import java.awt.event.KeyAdapter;
008import java.awt.event.KeyEvent;
009import java.util.EventObject;
010import java.util.Objects;
011
012import javax.swing.ComboBoxEditor;
013import javax.swing.JTable;
014import javax.swing.event.CellEditorListener;
015import javax.swing.table.TableCellEditor;
016import javax.swing.text.AttributeSet;
017import javax.swing.text.BadLocationException;
018import javax.swing.text.Document;
019import javax.swing.text.PlainDocument;
020import javax.swing.text.StyleConstants;
021
022import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem;
023import org.openstreetmap.josm.gui.util.CellEditorSupport;
024import org.openstreetmap.josm.gui.widgets.JosmTextField;
025import org.openstreetmap.josm.spi.preferences.Config;
026import org.openstreetmap.josm.tools.Logging;
027
028/**
029 * AutoCompletingTextField is a text field with autocompletion behaviour. It
030 * can be used as table cell editor in {@link JTable}s.
031 *
032 * Autocompletion is controlled by a list of {@link AutoCompletionItem}s
033 * managed in a {@link AutoCompletionList}.
034 *
035 * @since 1762
036 */
037public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor {
038
039    private Integer maxChars;
040
041    /**
042     * The document model for the editor
043     */
044    class AutoCompletionDocument extends PlainDocument {
045
046        @Override
047        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
048
049            // If a maximum number of characters is specified, avoid exceeding it
050            if (maxChars != null && str != null && getLength() + str.length() > maxChars) {
051                int allowedLength = maxChars-getLength();
052                if (allowedLength > 0) {
053                    str = str.substring(0, allowedLength);
054                } else {
055                    return;
056                }
057            }
058
059            if (autoCompletionList == null) {
060                super.insertString(offs, str, a);
061                return;
062            }
063
064            // input method for non-latin characters (e.g. scim)
065            if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) {
066                super.insertString(offs, str, a);
067                return;
068            }
069
070            // if the current offset isn't at the end of the document we don't autocomplete.
071            // If a highlighted autocompleted suffix was present and we get here Swing has
072            // already removed it from the document. getLength() therefore doesn't include the
073            // autocompleted suffix.
074            //
075            if (offs < getLength()) {
076                super.insertString(offs, str, a);
077                return;
078            }
079
080            String currentText = getText(0, getLength());
081            // if the text starts with a number we don't autocomplete
082            if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)) {
083                try {
084                    Long.parseLong(str);
085                    if (currentText.isEmpty()) {
086                        // we don't autocomplete on numbers
087                        super.insertString(offs, str, a);
088                        return;
089                    }
090                    Long.parseLong(currentText);
091                    super.insertString(offs, str, a);
092                    return;
093                } catch (NumberFormatException e) {
094                    // either the new text or the current text isn't a number. We continue with autocompletion
095                    Logging.trace(e);
096                }
097            }
098            String prefix = currentText.substring(0, offs);
099            autoCompletionList.applyFilter(prefix+str);
100            if (autoCompletionList.getFilteredSize() > 0 && !Objects.equals(str, noAutoCompletionString)) {
101                // there are matches. Insert the new text and highlight the auto completed suffix
102                String matchingString = autoCompletionList.getFilteredItemAt(0).getValue();
103                remove(0, getLength());
104                super.insertString(0, matchingString, a);
105
106                // highlight from insert position to end position to put the caret at the end
107                setCaretPosition(offs + str.length());
108                moveCaretPosition(getLength());
109            } else {
110                // there are no matches. Insert the new text, do not highlight
111                //
112                String newText = prefix + str;
113                remove(0, getLength());
114                super.insertString(0, newText, a);
115                setCaretPosition(getLength());
116            }
117        }
118    }
119
120    /** the auto completion list user input is matched against */
121    protected AutoCompletionList autoCompletionList;
122    /** a string which should not be auto completed */
123    protected String noAutoCompletionString;
124
125    @Override
126    protected Document createDefaultModel() {
127        return new AutoCompletionDocument();
128    }
129
130    protected final void init() {
131        addFocusListener(
132                new FocusAdapter() {
133                    @Override
134                    public void focusGained(FocusEvent e) {
135                        if (e != null && e.getOppositeComponent() != null) {
136                            // Select all characters when the change of focus occurs inside JOSM only.
137                            // When switching from another application, it is annoying, see #13747
138                            selectAll();
139                        }
140                        applyFilter(getText());
141                    }
142                }
143        );
144
145        addKeyListener(
146                new KeyAdapter() {
147                    @Override
148                    public void keyReleased(KeyEvent e) {
149                        if (getText().isEmpty()) {
150                            applyFilter("");
151                        }
152                    }
153                }
154        );
155        tableCellEditorSupport = new CellEditorSupport(this);
156    }
157
158    /**
159     * Constructs a new {@code AutoCompletingTextField}.
160     */
161    public AutoCompletingTextField() {
162        this(0);
163    }
164
165    /**
166     * Constructs a new {@code AutoCompletingTextField}.
167     * @param columns the number of columns to use to calculate the preferred width;
168     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
169     */
170    public AutoCompletingTextField(int columns) {
171        this(columns, true);
172    }
173
174    /**
175     * Constructs a new {@code AutoCompletingTextField}.
176     * @param columns the number of columns to use to calculate the preferred width;
177     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
178     * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
179     */
180    public AutoCompletingTextField(int columns, boolean undoRedo) {
181        super(null, null, columns, undoRedo);
182        init();
183    }
184
185    protected void applyFilter(String filter) {
186        if (autoCompletionList != null) {
187            autoCompletionList.applyFilter(filter);
188        }
189    }
190
191    /**
192     * Returns the auto completion list.
193     * @return the auto completion list; may be null, if no auto completion list is set
194     */
195    public AutoCompletionList getAutoCompletionList() {
196        return autoCompletionList;
197    }
198
199    /**
200     * Sets the auto completion list.
201     * @param autoCompletionList the auto completion list; if null, auto completion is
202     *   disabled
203     */
204    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
205        this.autoCompletionList = autoCompletionList;
206    }
207
208    @Override
209    public Component getEditorComponent() {
210        return this;
211    }
212
213    @Override
214    public Object getItem() {
215        return getText();
216    }
217
218    @Override
219    public void setItem(Object anObject) {
220        if (anObject == null) {
221            setText("");
222        } else {
223            setText(anObject.toString());
224        }
225    }
226
227    @Override
228    public void setText(String t) {
229        // disallow auto completion for this explicitly set string
230        this.noAutoCompletionString = t;
231        super.setText(t);
232    }
233
234    /**
235     * Sets the maximum number of characters allowed.
236     * @param max maximum number of characters allowed
237     * @since 5579
238     */
239    public void setMaxChars(Integer max) {
240        maxChars = max;
241    }
242
243    /* ------------------------------------------------------------------------------------ */
244    /* TableCellEditor interface                                                            */
245    /* ------------------------------------------------------------------------------------ */
246
247    private transient CellEditorSupport tableCellEditorSupport;
248    private String originalValue;
249
250    @Override
251    public void addCellEditorListener(CellEditorListener l) {
252        tableCellEditorSupport.addCellEditorListener(l);
253    }
254
255    protected void rememberOriginalValue(String value) {
256        this.originalValue = value;
257    }
258
259    protected void restoreOriginalValue() {
260        setText(originalValue);
261    }
262
263    @Override
264    public void removeCellEditorListener(CellEditorListener l) {
265        tableCellEditorSupport.removeCellEditorListener(l);
266    }
267
268    @Override
269    public void cancelCellEditing() {
270        restoreOriginalValue();
271        tableCellEditorSupport.fireEditingCanceled();
272    }
273
274    @Override
275    public Object getCellEditorValue() {
276        return getText();
277    }
278
279    @Override
280    public boolean isCellEditable(EventObject anEvent) {
281        return true;
282    }
283
284    @Override
285    public boolean shouldSelectCell(EventObject anEvent) {
286        return true;
287    }
288
289    @Override
290    public boolean stopCellEditing() {
291        tableCellEditorSupport.fireEditingStopped();
292        return true;
293    }
294
295    @Override
296    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
297        setText(value == null ? "" : value.toString());
298        rememberOriginalValue(getText());
299        return this;
300    }
301}