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