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;
010
011import javax.swing.ComboBoxEditor;
012import javax.swing.JTable;
013import javax.swing.event.CellEditorListener;
014import javax.swing.table.TableCellEditor;
015import javax.swing.text.AttributeSet;
016import javax.swing.text.BadLocationException;
017import javax.swing.text.Document;
018import javax.swing.text.PlainDocument;
019import javax.swing.text.StyleConstants;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.gui.util.CellEditorSupport;
023import org.openstreetmap.josm.gui.widgets.JosmTextField;
024
025/**
026 * AutoCompletingTextField is a text field with autocompletion behaviour. It
027 * can be used as table cell editor in {@link JTable}s.
028 *
029 * Autocompletion is controlled by a list of {@link AutoCompletionListItem}s
030 * managed in a {@link AutoCompletionList}.
031 *
032 * @since 1762
033 */
034public class AutoCompletingTextField extends JosmTextField implements ComboBoxEditor, TableCellEditor {
035
036    private Integer maxChars;
037
038    /**
039     * The document model for the editor
040     */
041    class AutoCompletionDocument extends PlainDocument {
042
043        @Override
044        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
045
046            // If a maximum number of characters is specified, avoid to exceed it
047            if (maxChars != null && str != null && getLength() + str.length() > maxChars) {
048                int allowedLength = maxChars-getLength();
049                if (allowedLength > 0) {
050                    str = str.substring(0, allowedLength);
051                } else {
052                    return;
053                }
054            }
055
056            if (autoCompletionList == null) {
057                super.insertString(offs, str, a);
058                return;
059            }
060
061            // input method for non-latin characters (e.g. scim)
062            if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) {
063                super.insertString(offs, str, a);
064                return;
065            }
066
067            // if the current offset isn't at the end of the document we don't autocomplete.
068            // If a highlighted autocompleted suffix was present and we get here Swing has
069            // already removed it from the document. getLength() therefore doesn't include the
070            // autocompleted suffix.
071            //
072            if (offs < getLength()) {
073                super.insertString(offs, str, a);
074                return;
075            }
076
077            String currentText = getText(0, getLength());
078            // if the text starts with a number we don't autocomplete
079            if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) {
080                try {
081                    Long.parseLong(str);
082                    if (currentText.length() == 0) {
083                        // we don't autocomplete on numbers
084                        super.insertString(offs, str, a);
085                        return;
086                    }
087                    Long.parseLong(currentText);
088                    super.insertString(offs, str, a);
089                    return;
090                } catch(NumberFormatException e) {
091                    // either the new text or the current text isn't a number. We continue with
092                    // autocompletion
093                }
094            }
095            String prefix = currentText.substring(0, offs);
096            autoCompletionList.applyFilter(prefix+str);
097            if (autoCompletionList.getFilteredSize()>0) {
098                // there are matches. Insert the new text and highlight the
099                // auto completed suffix
100                //
101                String matchingString = autoCompletionList.getFilteredItem(0).getValue();
102                remove(0,getLength());
103                super.insertString(0,matchingString,a);
104
105                // highlight from insert position to end position to put the caret at the end
106                setCaretPosition(offs + str.length());
107                moveCaretPosition(getLength());
108            } else {
109                // there are no matches. Insert the new text, do not highlight
110                //
111                String newText = prefix + str;
112                remove(0,getLength());
113                super.insertString(0,newText,a);
114                setCaretPosition(getLength());
115
116            }
117        }
118    }
119
120    /** the auto completion list user input is matched against */
121    protected AutoCompletionList autoCompletionList = null;
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    /**
222     * Sets the maximum number of characters allowed.
223     * @param max maximum number of characters allowed
224     * @since 5579
225     */
226    public void setMaxChars(Integer max) {
227        maxChars = max;
228    }
229
230    /* ------------------------------------------------------------------------------------ */
231    /* TableCellEditor interface                                                            */
232    /* ------------------------------------------------------------------------------------ */
233
234    private CellEditorSupport tableCellEditorSupport;
235    private String originalValue;
236
237    @Override
238    public void addCellEditorListener(CellEditorListener l) {
239        tableCellEditorSupport.addCellEditorListener(l);
240    }
241
242    protected void rememberOriginalValue(String value) {
243        this.originalValue = value;
244    }
245
246    protected void restoreOriginalValue() {
247        setText(originalValue);
248    }
249
250    @Override
251    public void removeCellEditorListener(CellEditorListener l) {
252        tableCellEditorSupport.removeCellEditorListener(l);
253    }
254
255    @Override
256    public void cancelCellEditing() {
257        restoreOriginalValue();
258        tableCellEditorSupport.fireEditingCanceled();
259    }
260
261    @Override
262    public Object getCellEditorValue() {
263        return getText();
264    }
265
266    @Override
267    public boolean isCellEditable(EventObject anEvent) {
268        return true;
269    }
270
271    @Override
272    public boolean shouldSelectCell(EventObject anEvent) {
273        return true;
274    }
275
276    @Override
277    public boolean stopCellEditing() {
278        tableCellEditorSupport.fireEditingStopped();
279        return true;
280    }
281
282    @Override
283    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
284        setText( value == null ? "" : value.toString());
285        rememberOriginalValue(getText());
286        return this;
287    }
288}