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        public AutoCompletingComboBox() {
149            super(new AutoCompletionListItem(JosmComboBox.DEFAULT_PROTOTYPE_DISPLAY_VALUE));
150            setRenderer(new AutoCompleteListCellRenderer());
151            final JTextComponent editor = (JTextComponent) this.getEditor().getEditorComponent();
152            editor.setDocument(new AutoCompletingComboBoxDocument(this));
153            editor.addFocusListener(
154                    new FocusListener() {
155                        public void focusLost(FocusEvent e) {
156                        }
157                        public void focusGained(FocusEvent e) {
158                            // save unix system selection (middle mouse paste)
159                            Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
160                            if(sysSel != null) {
161                                Transferable old = sysSel.getContents(null);
162                                editor.selectAll();
163                                sysSel.setContents(old, null);
164                            } else {
165                                editor.selectAll();
166                            }
167                        }
168                    }
169            );
170        }
171    
172        public void setMaxTextLength(int length)
173        {
174            this.maxTextLength = length;
175        }
176    
177        /**
178         * Convert the selected item into a String
179         * that can be edited in the editor component.
180         *
181         * @param editor    the editor
182         * @param item      excepts AutoCompletionListItem, String and null
183         */
184        @Override public void configureEditor(ComboBoxEditor editor, Object item) {
185            if (item == null) {
186                editor.setItem(null);
187            } else if (item instanceof String) {
188                editor.setItem(item);
189            } else if (item instanceof AutoCompletionListItem) {
190                editor.setItem(((AutoCompletionListItem)item).getValue());
191            } else
192                throw new IllegalArgumentException();
193        }
194    
195        /**
196         * Selects a given item in the ComboBox model
197         * @param item      excepts AutoCompletionListItem, String and null
198         */
199        @Override public void setSelectedItem(Object item) {
200            if (item == null) {
201                super.setSelectedItem(null);
202            } else if (item instanceof AutoCompletionListItem) {
203                super.setSelectedItem(item);
204            } else if (item instanceof String) {
205                String s = (String) item;
206                // find the string in the model or create a new item
207                for (int i=0; i< getModel().getSize(); i++) {
208                    AutoCompletionListItem acItem = (AutoCompletionListItem) getModel().getElementAt(i);
209                    if (s.equals(acItem.getValue())) {
210                        super.setSelectedItem(acItem);
211                        return;
212                    }
213                }
214                super.setSelectedItem(new AutoCompletionListItem(s, AutoCompletionItemPritority.UNKNOWN));
215            } else
216                throw new IllegalArgumentException();
217        }
218    
219        /**
220         * sets the items of the combobox to the given strings
221         */
222        public void setPossibleItems(Collection<String> elems) {
223            DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
224            Object oldValue = this.getEditor().getItem(); // Do not use getSelectedItem(); (fix #8013)
225            model.removeAllElements();
226            for (String elem : elems) {
227                model.addElement(new AutoCompletionListItem(elem, AutoCompletionItemPritority.UNKNOWN));
228            }
229            // disable autocomplete to prevent unnecessary actions in
230            // AutoCompletingComboBoxDocument#insertString
231            autocompleteEnabled = false;
232            this.getEditor().setItem(oldValue); // Do not use setSelectedItem(oldValue); (fix #8013)
233            autocompleteEnabled = true;
234        }
235    
236        /**
237         * sets the items of the combobox to the given AutoCompletionListItems
238         */
239        public void setPossibleACItems(Collection<AutoCompletionListItem> elems) {
240            DefaultComboBoxModel model = (DefaultComboBoxModel)this.getModel();
241            Object oldValue = getSelectedItem();
242            Object editorOldValue = this.getEditor().getItem();
243            model.removeAllElements();
244            for (AutoCompletionListItem elem : elems) {
245                model.addElement(elem);
246            }
247            setSelectedItem(oldValue);
248            this.getEditor().setItem(editorOldValue);
249        }
250    
251    
252        protected boolean isAutocompleteEnabled() {
253            return autocompleteEnabled;
254        }
255    
256        protected void setAutocompleteEnabled(boolean autocompleteEnabled) {
257            this.autocompleteEnabled = autocompleteEnabled;
258        }
259    
260        /**
261         * ListCellRenderer for AutoCompletingComboBox
262         * renders an AutoCompletionListItem by showing only the string value part
263         */
264        public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer {
265    
266            public AutoCompleteListCellRenderer() {
267                setOpaque(true);
268            }
269    
270            public Component getListCellRendererComponent(
271                    JList list,
272                    Object value,
273                    int index,
274                    boolean isSelected,
275                    boolean cellHasFocus)
276            {
277                if (isSelected) {
278                    setBackground(list.getSelectionBackground());
279                    setForeground(list.getSelectionForeground());
280                } else {
281                    setBackground(list.getBackground());
282                    setForeground(list.getForeground());
283                }
284    
285                AutoCompletionListItem item = (AutoCompletionListItem) value;
286                setText(item.getValue());
287                return this;
288            }
289        }
290    }