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                    if (Main.isTraceEnabled()) {
094                        Main.trace(e.getMessage());
095                    }
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.getFilteredItem(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 public void focusGained(FocusEvent e) {
134                        selectAll();
135                        applyFilter(getText());
136                    }
137                }
138        );
139
140        addKeyListener(
141                new KeyAdapter() {
142
143                    @Override
144                    public void keyReleased(KeyEvent e) {
145                        if (getText().isEmpty()) {
146                            applyFilter("");
147                        }
148                    }
149                }
150        );
151        tableCellEditorSupport = new CellEditorSupport(this);
152    }
153
154    /**
155     * Constructs a new {@code AutoCompletingTextField}.
156     */
157    public AutoCompletingTextField() {
158        this(0);
159    }
160
161    /**
162     * Constructs a new {@code AutoCompletingTextField}.
163     * @param columns the number of columns to use to calculate the preferred width;
164     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
165     */
166    public AutoCompletingTextField(int columns) {
167        this(columns, true);
168    }
169
170    /**
171     * Constructs a new {@code AutoCompletingTextField}.
172     * @param columns the number of columns to use to calculate the preferred width;
173     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
174     * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
175     */
176    public AutoCompletingTextField(int columns, boolean undoRedo) {
177        super(null, null, columns, undoRedo);
178        init();
179    }
180
181    protected void applyFilter(String filter) {
182        if (autoCompletionList != null) {
183            autoCompletionList.applyFilter(filter);
184        }
185    }
186
187    /**
188     * Returns the auto completion list.
189     * @return the auto completion list; may be null, if no auto completion list is set
190     */
191    public AutoCompletionList getAutoCompletionList() {
192        return autoCompletionList;
193    }
194
195    /**
196     * Sets the auto completion list.
197     * @param autoCompletionList the auto completion list; if null, auto completion is
198     *   disabled
199     */
200    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
201        this.autoCompletionList = autoCompletionList;
202    }
203
204    @Override
205    public Component getEditorComponent() {
206        return this;
207    }
208
209    @Override
210    public Object getItem() {
211        return getText();
212    }
213
214    @Override
215    public void setItem(Object anObject) {
216        if (anObject == null) {
217            setText("");
218        } else {
219            setText(anObject.toString());
220        }
221    }
222
223    @Override
224    public void setText(String t) {
225        // disallow auto completion for this explicitly set string
226        this.noAutoCompletionString = t;
227        super.setText(t);
228    }
229
230    /**
231     * Sets the maximum number of characters allowed.
232     * @param max maximum number of characters allowed
233     * @since 5579
234     */
235    public void setMaxChars(Integer max) {
236        maxChars = max;
237    }
238
239    /* ------------------------------------------------------------------------------------ */
240    /* TableCellEditor interface                                                            */
241    /* ------------------------------------------------------------------------------------ */
242
243    private transient CellEditorSupport tableCellEditorSupport;
244    private String originalValue;
245
246    @Override
247    public void addCellEditorListener(CellEditorListener l) {
248        tableCellEditorSupport.addCellEditorListener(l);
249    }
250
251    protected void rememberOriginalValue(String value) {
252        this.originalValue = value;
253    }
254
255    protected void restoreOriginalValue() {
256        setText(originalValue);
257    }
258
259    @Override
260    public void removeCellEditorListener(CellEditorListener l) {
261        tableCellEditorSupport.removeCellEditorListener(l);
262    }
263
264    @Override
265    public void cancelCellEditing() {
266        restoreOriginalValue();
267        tableCellEditorSupport.fireEditingCanceled();
268    }
269
270    @Override
271    public Object getCellEditorValue() {
272        return getText();
273    }
274
275    @Override
276    public boolean isCellEditable(EventObject anEvent) {
277        return true;
278    }
279
280    @Override
281    public boolean shouldSelectCell(EventObject anEvent) {
282        return true;
283    }
284
285    @Override
286    public boolean stopCellEditing() {
287        tableCellEditorSupport.fireEditingStopped();
288        return true;
289    }
290
291    @Override
292    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
293        setText(value == null ? "" : value.toString());
294        rememberOriginalValue(getText());
295        return this;
296    }
297}