001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.ac;
003
004import java.awt.Component;
005import java.awt.datatransfer.Clipboard;
006import java.awt.datatransfer.StringSelection;
007import java.awt.datatransfer.Transferable;
008import java.awt.event.KeyEvent;
009import java.awt.event.KeyListener;
010import java.util.EventObject;
011import java.util.regex.Pattern;
012
013import javax.swing.JTable;
014import javax.swing.SwingUtilities;
015import javax.swing.event.CellEditorListener;
016import javax.swing.table.TableCellEditor;
017import javax.swing.text.AbstractDocument;
018
019import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
020import org.openstreetmap.josm.gui.util.CellEditorSupport;
021import org.openstreetmap.josm.gui.widgets.JosmTextField;
022import org.openstreetmap.josm.spi.preferences.Config;
023
024/**
025 * An auto-completing TextField.
026 * <p>
027 * When the user starts typing, this textfield will suggest the
028 * {@link AutoCompComboBoxModel#findBestCandidate best matching item} from its model.  The items in
029 * the model can be of any type while the items' {@code toString} values are used for
030 * autocompletion.
031 *
032 * @param <E> the type of items in the model
033 * @since 18221
034 */
035public class AutoCompTextField<E> extends JosmTextField implements TableCellEditor, KeyListener {
036
037    /** true if the combobox should autocomplete */
038    private boolean autocompleteEnabled = true;
039    /** a filter to enforce max. text length */
040    private transient MaxLengthDocumentFilter docFilter;
041    /** the model */
042    protected AutoCompComboBoxModel<E> model;
043    /** Whether to autocomplete numbers */
044    private final boolean AUTOCOMPLETE_NUMBERS = !Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true);
045    /** a regex that matches numbers */
046    private static final Pattern IS_NUMBER = Pattern.compile("^\\d+$");
047
048    protected final void init() {
049        model = new AutoCompComboBoxModel<>();
050        docFilter = new MaxLengthDocumentFilter();
051        ((AbstractDocument) getDocument()).setDocumentFilter(docFilter);
052        addKeyListener(this);
053        tableCellEditorSupport = new CellEditorSupport(this);
054    }
055
056    /**
057     * Constructs a new {@code AutoCompTextField}.
058     */
059    public AutoCompTextField() {
060        this(0);
061    }
062
063    /**
064     * Constructs a new {@code AutoCompTextField}.
065     * @param model the model to use
066     */
067    public AutoCompTextField(AutoCompComboBoxModel<E> model) {
068        this(0);
069        this.model = model;
070    }
071
072    /**
073     * Constructs a new {@code AutoCompTextField}.
074     * @param columns the number of columns to use to calculate the preferred width;
075     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
076     */
077    public AutoCompTextField(int columns) {
078        this(columns, true);
079    }
080
081    /**
082     * Constructs a new {@code AutoCompTextField}.
083     * @param columns the number of columns to use to calculate the preferred width;
084     * if columns is set to zero, the preferred width will be whatever naturally results from the component implementation
085     * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor
086     */
087    public AutoCompTextField(int columns, boolean undoRedo) {
088        super(null, null, columns, undoRedo);
089        init();
090    }
091
092    /**
093     * Returns the {@link AutoCompComboBoxModel} currently used.
094     *
095     * @return the model
096     */
097    public AutoCompComboBoxModel<E> getModel() {
098        return model;
099    }
100
101    /**
102     * Sets the data model that the {@code AutoCompTextField} uses to obtain the list of items.
103     *
104     * @param model the {@link AutoCompComboBoxModel} that provides the list of items used for autocomplete
105     */
106    public void setModel(AutoCompComboBoxModel<E> model) {
107        AutoCompComboBoxModel<E> oldModel = this.model;
108        this.model = model;
109        firePropertyChange("model", oldModel, model);
110    }
111
112    /**
113     * Returns {@code true} if autocompletion is enabled.
114     *
115     * @return {@code true} if autocompletion is enabled.
116     */
117    public final boolean isAutocompleteEnabled() {
118        return autocompleteEnabled;
119    }
120
121    /**
122     * Enables or disables the autocompletion.
123     *
124     * @param enabled {@code true} to enable autocompletion
125     * @return {@code true} if autocomplete was enabled before calling this
126     */
127    public boolean setAutocompleteEnabled(boolean enabled) {
128        boolean oldEnabled = this.autocompleteEnabled;
129        this.autocompleteEnabled = enabled;
130        return oldEnabled;
131    }
132
133    /**
134     * Sets the maximum number of characters allowed.
135     * @param length maximum number of characters allowed
136     */
137    public void setMaxTextLength(int length) {
138        docFilter.setMaxLength(length);
139    }
140
141    /**
142     * Autocompletes what the user typed in.
143     * <p>
144     * Gets the user input from the editor, finds the best matching item in the model, sets the
145     * editor text to it, and highlights the autocompleted part. If there is no matching item, removes the
146     * list selection.
147     *
148     * @param oldText the text before the last keypress was processed
149     */
150    private void autocomplete(String oldText) {
151        String newText = getText();
152        if (getSelectionEnd() != newText.length())
153            // selection not at the end
154            return;
155        // if the user typed some control character (eg. Alt+A) the selection may still be there
156        String unSelected = newText.substring(0, getSelectionStart());
157        if (unSelected.length() <= oldText.length())
158            // do not autocomplete on control or deleted chars
159            return;
160        if (!AUTOCOMPLETE_NUMBERS && IS_NUMBER.matcher(newText).matches())
161            return;
162
163        fireAutoCompEvent(AutoCompEvent.AUTOCOMP_BEFORE, null);
164        E item = getModel().findBestCandidate(newText);
165        fireAutoCompEvent(AutoCompEvent.AUTOCOMP_DONE, item);
166
167        if (item != null) {
168            String text = item.toString();
169            setText(text);
170            // select the autocompleted suffix in the editor
171            select(newText.length(), text.length());
172            // copy the whole autocompleted string to the unix system-wide selection (aka
173            // middle-click), else only the selected suffix would be copied
174            copyToSysSel(text);
175        }
176    }
177
178    /**
179     * Copies a String to the UNIX system-wide selection (aka middle-click).
180     *
181     * @param s the string to copy
182     */
183    void copyToSysSel(String s) {
184        Clipboard sysSel = ClipboardUtils.getSystemSelection();
185        if (sysSel != null) {
186            Transferable transferable = new StringSelection(s);
187            sysSel.setContents(transferable, null);
188        }
189    }
190
191    /**
192     * Adds an AutoCompListener.
193     *
194     * @param l the AutoComp listener to be added
195     */
196    public synchronized void addAutoCompListener(AutoCompListener l) {
197        listenerList.add(AutoCompListener.class, l);
198    }
199
200    /**
201     * Removes the specified AutoCompListener.
202     *
203     * @param l the autoComp listener to be removed
204     */
205    public synchronized void removeAutoCompListener(AutoCompListener l) {
206        listenerList.remove(AutoCompListener.class, l);
207    }
208
209    /**
210     * Returns an array of all the current <code>AutoCompListener</code>s.
211     *
212     * @return all of the <code>AutoCompListener</code>s added or an empty
213     *         array if no listeners have been added
214     */
215    public synchronized AutoCompListener[] getAutoCompListeners() {
216        return listenerList.getListeners(AutoCompListener.class);
217    }
218
219    /**
220     * Notifies all listeners that have registered interest for notification on this event type.
221     * The event instance is lazily created. The listener list is processed in last to first order.
222     *
223     * @param id The Autocomp event id
224     * @param item The item selected for autocompletion.
225     * @see javax.swing.event.EventListenerList
226     */
227    protected void fireAutoCompEvent(int id, Object item) {
228        // Guaranteed to return a non-null array
229        Object[] listeners = listenerList.getListenerList();
230        AutoCompEvent e = new AutoCompEvent(this, id, item);
231
232        // Process the listeners last to first, notifying
233        // those that are interested in this event
234        for (int i = listeners.length - 2; i >= 0; i -= 2) {
235            if (listeners[i] == AutoCompListener.class) {
236                switch (id) {
237                    case AutoCompEvent.AUTOCOMP_DONE:
238                        ((AutoCompListener) listeners[i + 1]).autoCompPerformed(e);
239                        break;
240                    case AutoCompEvent.AUTOCOMP_BEFORE:
241                        ((AutoCompListener) listeners[i + 1]).autoCompBefore(e);
242                        break;
243                    default:
244                        break;
245                }
246            }
247        }
248    }
249
250    /* ------------------------------------------------------------------------------------ */
251    /* KeyListener interface                                                                */
252    /* ------------------------------------------------------------------------------------ */
253
254    /**
255     * Listens to key events and eventually schedules an autocomplete.
256     *
257     * @param e the key event
258     */
259    @Override
260    public void keyTyped(KeyEvent e) {
261        // if selection is at the end
262        if (autocompleteEnabled && getSelectionEnd() == getText().length()) {
263            final String oldText = getText().substring(0, getSelectionStart());
264            // We got the event before the editor component could see it. Let the editor do its job first.
265            SwingUtilities.invokeLater(() -> autocomplete(oldText));
266        }
267    }
268
269    @Override
270    public void keyPressed(KeyEvent e) {
271        // not interested
272    }
273
274    @Override
275    public void keyReleased(KeyEvent e) {
276        // not interested
277    }
278
279    /* ------------------------------------------------------------------------------------ */
280    /* TableCellEditor interface                                                            */
281    /* ------------------------------------------------------------------------------------ */
282
283    private transient CellEditorSupport tableCellEditorSupport;
284    private String originalValue;
285
286    @Override
287    public void addCellEditorListener(CellEditorListener l) {
288        tableCellEditorSupport.addCellEditorListener(l);
289    }
290
291    protected void rememberOriginalValue(String value) {
292        this.originalValue = value;
293    }
294
295    protected void restoreOriginalValue() {
296        setText(originalValue);
297    }
298
299    @Override
300    public void removeCellEditorListener(CellEditorListener l) {
301        tableCellEditorSupport.removeCellEditorListener(l);
302    }
303
304    @Override
305    public void cancelCellEditing() {
306        restoreOriginalValue();
307        tableCellEditorSupport.fireEditingCanceled();
308    }
309
310    @Override
311    public Object getCellEditorValue() {
312        return getText();
313    }
314
315    @Override
316    public boolean isCellEditable(EventObject anEvent) {
317        return true;
318    }
319
320    @Override
321    public boolean shouldSelectCell(EventObject anEvent) {
322        return true;
323    }
324
325    @Override
326    public boolean stopCellEditing() {
327        tableCellEditorSupport.fireEditingStopped();
328        return true;
329    }
330
331    @Override
332    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
333        setText(value == null ? "" : value.toString());
334        rememberOriginalValue(getText());
335        return this;
336    }
337}