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