001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.gui.tagging.ac;
003    
004    import java.awt.Component;
005    import java.awt.Toolkit;
006    import java.awt.datatransfer.Clipboard;
007    import java.awt.datatransfer.Transferable;
008    import java.awt.event.FocusEvent;
009    import java.awt.event.FocusListener;
010    import java.util.Collection;
011    
012    import javax.swing.ComboBoxEditor;
013    import javax.swing.ComboBoxModel;
014    import javax.swing.DefaultComboBoxModel;
015    import javax.swing.JLabel;
016    import javax.swing.JList;
017    import javax.swing.ListCellRenderer;
018    import javax.swing.text.AttributeSet;
019    import javax.swing.text.BadLocationException;
020    import javax.swing.text.JTextComponent;
021    import javax.swing.text.PlainDocument;
022    import javax.swing.text.StyleConstants;
023    
024    import org.openstreetmap.josm.Main;
025    import org.openstreetmap.josm.gui.widgets.JosmComboBox;
026    
027    /**
028     * @author guilhem.bonnefille@gmail.com
029     */
030    public class AutoCompletingComboBox extends JosmComboBox {
031    
032        private boolean autocompleteEnabled = true;
033    
034        private int maxTextLength = -1;
035    
036        /**
037         * Auto-complete a JosmComboBox.
038         *
039         * Inspired by http://www.orbital-computer.de/JComboBox/
040         */
041        class AutoCompletingComboBoxDocument extends PlainDocument {
042            private JosmComboBox comboBox;
043            private boolean selecting = false;
044    
045            public AutoCompletingComboBoxDocument(final JosmComboBox comboBox) {
046                this.comboBox = comboBox;
047            }
048    
049            @Override public void remove(int offs, int len) throws BadLocationException {
050                if (selecting)
051                    return;
052                super.remove(offs, len);
053            }
054    
055            @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
056                if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
057                    return;
058                if (maxTextLength > -1 && str.length()+getLength() > maxTextLength)
059                    return;
060                boolean initial = (offs == 0 && getLength() == 0 && str.length() > 1);
061                super.insertString(offs, str, a);
062    
063                // return immediately when selecting an item
064                // Note: this is done after calling super method because we need
065                // ActionListener informed
066                if (selecting)
067                    return;
068                if (!autocompleteEnabled)
069                    return;
070                // input method for non-latin characters (e.g. scim)
071                if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
072                    return;
073    
074                int size = getLength();
075                int start = offs+str.length();
076                int end = start;
077                String curText = getText(0, size);
078    
079                // item for lookup and selection
080                Object item = null;
081                // if the text is a number we don't autocomplete
082                if (Main.pref.getBoolean("autocomplete.dont_complete_numbers", true)) {
083                    try {
084                        Long.parseLong(str);
085                        if (curText.length() != 0)
086                            Long.parseLong(curText);
087                        item = lookupItem(curText, true);
088                    } catch (NumberFormatException e) {
089                        // either the new text or the current text isn't a number. We continue with
090                        // autocompletion
091                        item = lookupItem(curText, false);
092                    }
093                } else {
094                    item = lookupItem(curText, false);
095                }
096    
097                setSelectedItem(item);
098                if (initial) {
099                    start = 0;
100                }
101                if (item != null) {
102                    String newText = ((AutoCompletionListItem) item).getValue();
103                    if (!newText.equals(curText))
104                    {
105                        selecting = true;
106                        super.remove(0, size);
107                        super.insertString(0, newText, a);
108                        selecting = false;
109                        start = size;
110                        end = getLength();
111                    }
112                }
113                JTextComponent editor = (JTextComponent)comboBox.getEditor().getEditorComponent();
114                // save unix system selection (middle mouse paste)
115                Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
116                if(sysSel != null) {
117                    Transferable old = sysSel.getContents(null);
118                    editor.select(start, end);
119                    sysSel.setContents(old, null);
120                } else {
121                    editor.select(start, end);
122                }
123            }
124    
125            private void setSelectedItem(Object item) {
126                selecting = true;
127                comboBox.setSelectedItem(item);
128                selecting = false;
129            }
130    
131            private Object lookupItem(String pattern, boolean match) {
132                ComboBoxModel model = comboBox.getModel();
133                AutoCompletionListItem bestItem = null;
134                for (int i = 0, n = model.getSize(); i < n; i++) {
135                    AutoCompletionListItem currentItem = (AutoCompletionListItem) model.getElementAt(i);
136                    if (currentItem.getValue().equals(pattern))
137                        return currentItem;
138                    if (!match && currentItem.getValue().startsWith(pattern)) {
139                        if (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0) {
140                            bestItem = currentItem;
141                        }
142                    }
143                }
144                return bestItem; // may be null
145            }
146        }
147    
148        /**
149         * Creates a <code>AutoCompletingComboBox</code> with a default prototype display value.
150         */
151        public AutoCompletingComboBox() {
152            this(JosmComboBox.DEFAULT_PROTOTYPE_DISPLAY_VALUE);
153        }
154    
155        /**
156         * Creates a <code>AutoCompletingComboBox</code> with the specified prototype display value.
157         * @param prototype the <code>Object</code> used to compute the maximum number of elements to be displayed at once before displaying a scroll bar.
158         *                  It also affects the initial width of the combo box.
159         * @since 5520
160         */
161        public AutoCompletingComboBox(String prototype) {
162            super(new AutoCompletionListItem(prototype));
163            setRenderer(new AutoCompleteListCellRenderer());
164            final JTextComponent editor = (JTextComponent) this.getEditor().getEditorComponent();
165            editor.setDocument(new AutoCompletingComboBoxDocument(this));
166            editor.addFocusListener(
167                    new FocusListener() {
168                        public void focusLost(FocusEvent e) {
169                        }
170                        public void focusGained(FocusEvent e) {
171                            // save unix system selection (middle mouse paste)
172                            Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
173                            if(sysSel != null) {
174                                Transferable old = sysSel.getContents(null);
175                                editor.selectAll();
176                                sysSel.setContents(old, null);
177                            } else {
178                                editor.selectAll();
179                            }
180                        }
181                    }
182            );
183        }
184    
185        public void setMaxTextLength(int length)
186        {
187            this.maxTextLength = length;
188        }
189    
190        /**
191         * Convert the selected item into a String
192         * that can be edited in the editor component.
193         *
194         * @param editor    the editor
195         * @param item      excepts AutoCompletionListItem, String and null
196         */
197        @Override public void configureEditor(ComboBoxEditor editor, Object item) {
198            if (item == null) {
199                editor.setItem(null);
200            } else if (item instanceof String) {
201                editor.setItem(item);
202            } else if (item instanceof AutoCompletionListItem) {
203                editor.setItem(((AutoCompletionListItem)item).getValue());
204            } else
205                throw new IllegalArgumentException();
206        }
207    
208        /**
209         * Selects a given item in the ComboBox model
210         * @param item      excepts AutoCompletionListItem, String and null
211         */
212        @Override public void setSelectedItem(Object item) {
213            if (item == null) {
214                super.setSelectedItem(null);
215            } else if (item instanceof AutoCompletionListItem) {
216                super.setSelectedItem(item);
217            } else if (item instanceof String) {
218                String s = (String) item;
219                // find the string in the model or create a new item
220                for (int i=0; i< getModel().getSize(); i++) {
221                    AutoCompletionListItem acItem = (AutoCompletionListItem) getModel().getElementAt(i);
222                    if (s.equals(acItem.getValue())) {
223                        super.setSelectedItem(acItem);
224                        return;
225                    }
226                }
227                super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPritority.UNKNOWN));
228            } else
229                throw new IllegalArgumentException();
230        }
231    
232        /**
233         * sets the items of the combobox to the given strings
234         */
235        public void setPossibleItems(Collection<String> elems) {
236            DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
237            Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
238            model.removeAllElements();
239            for (String elem : elems) {
240                model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPritority.UNKNOWN));
241            }
242            // disable autocomplete to prevent unnecessary actions in
243            // AutoCompletingComboBoxDocument#insertString
244            autocompleteEnabled = false;
245            this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013)
246            autocompleteEnabled = true;
247        }
248    
249        /**
250         * sets the items of the combobox to the given AutoCompletionListItems
251         */
252        public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
253            DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
254            Object oldValue = getSelectedItem();
255            Object editorOldValue = this.getEditor().getItem();
256            model.removeAllElements();
257            for (AutoCompletionListItem elem : elems) {
258                model.addElement(elem);
259            }
260            setSelectedItem(oldValue);
261            this.getEditor().setItem(editorOldValue);
262        }
263    
264    
265        protected boolean isAutocompleteEnabled() {
266            return autocompleteEnabled;
267        }
268    
269        protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
270            this.autocompleteEnabled = autocompleteEnabled;
271        }
272    
273        /**
274         * ListCellRenderer for AutoCompletingComboBox
275         * renders an AutoCompletionListItem by showing only the string value part
276         */
277        public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer {
278    
279            public AutoCompleteListCellRenderer() {
280                setOpaque(true);
281            }
282    
283            public Component getListCellRendererComponent(
284                    JList list,
285                    Object value,
286                    int index,
287                    boolean isSelected,
288                    boolean cellHasFocus)
289            {
290                if (isSelected) {
291                    setBackground(list.getSelectionBackground());
292                    setForeground(list.getSelectionForeground());
293                } else {
294                    setBackground(list.getBackground());
295                    setForeground(list.getForeground());
296                }
297    
298                AutoCompletionListItem item = (AutoCompletionListItem) value;
299                setText(item.getValue());
300                return this;
301            }
302        }
303    }