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        init();
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        super(columns);
166        init();
167    }
168
169    protected void applyFilter(String filter) {
170        if (autoCompletionList != null) {
171            autoCompletionList.applyFilter(filter);
172        }
173    }
174
175    /**
176     * Returns the auto completion list.
177     * @return the auto completion list; may be null, if no auto completion list is set
178     */
179    public AutoCompletionList getAutoCompletionList() {
180        return autoCompletionList;
181    }
182
183    /**
184     * Sets the auto completion list.
185     * @param autoCompletionList the auto completion list; if null, auto completion is
186     *   disabled
187     */
188    public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
189        this.autoCompletionList = autoCompletionList;
190    }
191
192    @Override
193    public Component getEditorComponent() {
194        return this;
195    }
196
197    @Override
198    public Object getItem() {
199        return getText();
200    }
201
202    @Override
203    public void setItem(Object anObject) {
204        if (anObject == null) {
205            setText("");
206        } else {
207            setText(anObject.toString());
208        }
209    }
210
211    /**
212     * Sets the maximum number of characters allowed.
213     * @param max maximum number of characters allowed
214     * @since 5579
215     */
216    public void setMaxChars(Integer max) {
217        maxChars = max;
218    }
219
220    /* ------------------------------------------------------------------------------------ */
221    /* TableCellEditor interface                                                            */
222    /* ------------------------------------------------------------------------------------ */
223
224    private CellEditorSupport tableCellEditorSupport;
225    private String originalValue;
226
227    @Override
228    public void addCellEditorListener(CellEditorListener l) {
229        tableCellEditorSupport.addCellEditorListener(l);
230    }
231
232    protected void rememberOriginalValue(String value) {
233        this.originalValue = value;
234    }
235
236    protected void restoreOriginalValue() {
237        setText(originalValue);
238    }
239
240    @Override
241    public void removeCellEditorListener(CellEditorListener l) {
242        tableCellEditorSupport.removeCellEditorListener(l);
243    }
244
245    @Override
246    public void cancelCellEditing() {
247        restoreOriginalValue();
248        tableCellEditorSupport.fireEditingCanceled();
249    }
250
251    @Override
252    public Object getCellEditorValue() {
253        return getText();
254    }
255
256    @Override
257    public boolean isCellEditable(EventObject anEvent) {
258        return true;
259    }
260
261    @Override
262    public boolean shouldSelectCell(EventObject anEvent) {
263        return true;
264    }
265
266    @Override
267    public boolean stopCellEditing() {
268        tableCellEditorSupport.fireEditingStopped();
269        return true;
270    }
271
272    @Override
273    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
274        setText( value == null ? "" : value.toString());
275        rememberOriginalValue(getText());
276        return this;
277    }
278}