001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.tagging.ac;
003    
004    import java.awt.Component;
005    import java.awt.event.FocusAdapter;
006    import java.awt.event.FocusEvent;
007    import java.awt.event.KeyAdapter;
008    import java.awt.event.KeyEvent;
009    import java.util.EventObject;
010    
011    import javax.swing.ComboBoxEditor;
012    import javax.swing.JTable;
013    import javax.swing.JTextField;
014    import javax.swing.event.CellEditorListener;
015    import javax.swing.table.TableCellEditor;
016    import javax.swing.text.AttributeSet;
017    import javax.swing.text.BadLocationException;
018    import javax.swing.text.Document;
019    import javax.swing.text.PlainDocument;
020    import javax.swing.text.StyleConstants;
021    
022    import org.openstreetmap.josm.Main;
023    import org.openstreetmap.josm.gui.util.TableCellEditorSupport;
024    
025    /**
026     * AutoCompletingTextField is an 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     *
033     */
034    public class AutoCompletingTextField extends JTextField implements ComboBoxEditor, TableCellEditor {
035        /**
036         * The document model for the editor
037         */
038        class AutoCompletionDocument extends PlainDocument {
039    
040            /**
041             * inserts a string at a specific position
042             *
043             */
044            @Override
045            public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
046                if (autoCompletionList == null) {
047                    super.insertString(offs, str, a);
048                    return;
049                }
050    
051                // input method for non-latin characters (e.g. scim)
052                if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute)) {
053                    super.insertString(offs, str, a);
054                    return;
055                }
056    
057                // if the current offset isn't at the end of the document we don't autocomplete.
058                // If a highlighted autocompleted suffix was present and we get here Swing has
059                // already removed it from the document. getLength() therefore doesn't include the
060                // autocompleted suffix.
061                //
062                if (offs < getLength()) {
063                    super.insertString(offs, str, a);
064                    return;
065                }
066    
067                String currentText = getText(0, getLength());
068                // if the text starts with a number we don't autocomplete
069                if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) {
070                    try {
071                        Long.parseLong(str);
072                        if (currentText.length() == 0) {
073                            // we don't autocomplete on numbers
074                            super.insertString(offs, str, a);
075                            return;
076                        }
077                        Long.parseLong(currentText);
078                        super.insertString(offs, str, a);
079                        return;
080                    } catch(NumberFormatException e) {
081                        // either the new text or the current text isn't a number. We continue with
082                        // autocompletion
083                    }
084                }
085                String prefix = currentText.substring(0, offs);
086                autoCompletionList.applyFilter(prefix+str);
087                if (autoCompletionList.getFilteredSize()>0) {
088                    // there are matches. Insert the new text and highlight the
089                    // auto completed suffix
090                    //
091                    String matchingString = autoCompletionList.getFilteredItem(0).getValue();
092                    remove(0,getLength());
093                    super.insertString(0,matchingString,a);
094    
095                    // highlight from insert position to end position to put the caret at the end
096                    setCaretPosition(offs + str.length());
097                    moveCaretPosition(getLength());
098                } else {
099                    // there are no matches. Insert the new text, do not highlight
100                    //
101                    String newText = prefix + str;
102                    remove(0,getLength());
103                    super.insertString(0,newText,a);
104                    setCaretPosition(getLength());
105    
106                }
107            }
108        }
109    
110        /** the auto completion list user input is matched against */
111        protected AutoCompletionList autoCompletionList = null;
112    
113        /**
114         * creates the default document model for this editor
115         *
116         */
117        @Override
118        protected Document createDefaultModel() {
119            return new AutoCompletionDocument();
120        }
121    
122        protected void init() {
123            addFocusListener(
124                    new FocusAdapter() {
125                        @Override public void focusGained(FocusEvent e) {
126                            selectAll();
127                            applyFilter(getText());
128                        }
129                    }
130            );
131    
132            addKeyListener(
133                    new KeyAdapter() {
134    
135                        @Override
136                        public void keyReleased(KeyEvent e) {
137                            if (getText().equals("")) {
138                                applyFilter("");
139                            }
140                        }
141                    }
142            );
143            tableCellEditorSupport = new TableCellEditorSupport(this);
144        }
145    
146        /**
147         * constructor
148         */
149        public AutoCompletingTextField() {
150            init();
151        }
152    
153        public AutoCompletingTextField(int columns) {
154            super(columns);
155            init();
156        }
157    
158        protected void applyFilter(String filter) {
159            if (autoCompletionList != null) {
160                autoCompletionList.applyFilter(filter);
161            }
162        }
163    
164        /**
165         *
166         * @return the auto completion list; may be null, if no auto completion list is set
167         */
168        public AutoCompletionList getAutoCompletionList() {
169            return autoCompletionList;
170        }
171    
172        /**
173         * sets the auto completion list
174         * @param autoCompletionList the auto completion list; if null, auto completion is
175         *   disabled
176         */
177        public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
178            this.autoCompletionList = autoCompletionList;
179        }
180    
181        public Component getEditorComponent() {
182            return this;
183        }
184    
185        public Object getItem() {
186            return getText();
187        }
188    
189        public void setItem(Object anObject) {
190            if (anObject == null) {
191                setText("");
192            } else {
193                setText(anObject.toString());
194            }
195        }
196    
197        /* ------------------------------------------------------------------------------------ */
198        /* TableCellEditor interface                                                            */
199        /* ------------------------------------------------------------------------------------ */
200    
201        private TableCellEditorSupport tableCellEditorSupport;
202        private String originalValue;
203    
204        public void addCellEditorListener(CellEditorListener l) {
205            tableCellEditorSupport.addCellEditorListener(l);
206        }
207    
208        protected void rememberOriginalValue(String value) {
209            this.originalValue = value;
210        }
211    
212        protected void restoreOriginalValue() {
213            setText(originalValue);
214        }
215    
216        public void removeCellEditorListener(CellEditorListener l) {
217            tableCellEditorSupport.removeCellEditorListener(l);
218        }
219        public void cancelCellEditing() {
220            restoreOriginalValue();
221            tableCellEditorSupport.fireEditingCanceled();
222    
223        }
224    
225        public Object getCellEditorValue() {
226            return getText();
227        }
228    
229        public boolean isCellEditable(EventObject anEvent) {
230            return true;
231        }
232    
233        public boolean shouldSelectCell(EventObject anEvent) {
234            return true;
235        }
236    
237        public boolean stopCellEditing() {
238            tableCellEditorSupport.fireEditingStopped();
239            return true;
240        }
241    
242        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
243            setText( value == null ? "" : value.toString());
244            rememberOriginalValue(getText());
245            return this;
246        }
247    }