Ticket #21198: 21198.patch

File 21198.patch, 13.5 KB (added by marcello@…, 4 years ago)
  • src/org/openstreetmap/josm/data/tagging/ac/AutoCompletionItem.java

     
    7474        return value;
    7575    }
    7676
     77    /**
     78     * Here we return the value instead of a representation of the inner object
     79     * state because both
     80     * {@link javax.swing.plaf.basic.BasicComboBoxEditor#setItem(Object)} and
     81     * {@link javax.swing.DefaultListCellRenderer#getListCellRendererComponent}
     82     * expect it, thus making derived Editor and CellRenderer classes
     83     * superfluous.
     84     */
     85
    7786    @Override
    7887    public String toString() {
    79         StringBuilder sb = new StringBuilder();
    80         sb.append("<val='")
    81           .append(value)
    82           .append("',")
    83           .append(priority)
    84           .append('>');
    85         return sb.toString();
     88        return value;
    8689    }
    8790
    8891    @Override
  • src/org/openstreetmap/josm/gui/tagging/ac/AutoCompletingComboBox.java

     
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.gui.tagging.ac;
    33
    4 import java.awt.Component;
    54import java.awt.datatransfer.Clipboard;
    65import java.awt.datatransfer.Transferable;
    76import java.awt.event.FocusEvent;
     
    1110import java.util.Collections;
    1211import java.util.LinkedList;
    1312import java.util.Locale;
    14 import java.util.stream.IntStream;
    1513
    16 import javax.swing.ComboBoxEditor;
    1714import javax.swing.ComboBoxModel;
    1815import javax.swing.DefaultComboBoxModel;
    19 import javax.swing.JLabel;
    20 import javax.swing.JList;
    21 import javax.swing.ListCellRenderer;
    2216import javax.swing.text.AttributeSet;
    2317import javax.swing.text.BadLocationException;
    2418import javax.swing.text.JTextComponent;
     
    4337public class AutoCompletingComboBox extends JosmComboBox<AutoCompletionItem> {
    4438
    4539    private boolean autocompleteEnabled = true;
     40    private boolean locked = false;
    4641
    4742    private int maxTextLength = -1;
    4843    private boolean useFixedLocale;
     
    9287     * Inspired by <a href="http://www.orbital-computer.de/JComboBox">Thomas Bierhance example</a>.
    9388     */
    9489    class AutoCompletingComboBoxDocument extends PlainDocument {
    95         private boolean selecting;
    9690
    9791        @Override
    9892        public void remove(int offs, int len) throws BadLocationException {
    99             if (selecting)
    100                 return;
    10193            try {
    10294                super.remove(offs, len);
    10395            } catch (IllegalArgumentException e) {
     
    109101        @Override
    110102        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
    111103            // TODO get rid of code duplication w.r.t. AutoCompletingTextField.AutoCompletionDocument.insertString
     104            // System.out.println(Arrays.toString(Thread.currentThread().getStackTrace()).replace( ',', '\n' ));
    112105
    113             if (selecting || (offs == 0 && str.equals(getText(0, getLength()))))
     106            if (maxTextLength > -1 && str.length() + getLength() > maxTextLength)
    114107                return;
    115             if (maxTextLength > -1 && str.length()+getLength() > maxTextLength)
    116                 return;
    117             boolean initial = offs == 0 && getLength() == 0 && str.length() > 1;
     108
    118109            super.insertString(offs, str, a);
    119110
    120             // return immediately when selecting an item
    121             // Note: this is done after calling super method because we need
    122             // ActionListener informed
    123             if (selecting)
    124                 return;
     111            if (locked)
     112                return; // don't get in a loop
     113
    125114            if (!autocompleteEnabled)
    126115                return;
     116
    127117            // input method for non-latin characters (e.g. scim)
    128118            if (a != null && a.isDefined(StyleConstants.ComposedTextAttribute))
    129119                return;
    130120
    131             // if the current offset isn't at the end of the document we don't autocomplete.
     121            // if the cursor isn't at the end of the text we don't autocomplete.
    132122            // If a highlighted autocompleted suffix was present and we get here Swing has
    133123            // already removed it from the document. getLength() therefore doesn't include the autocompleted suffix.
    134124            if (offs + str.length() < getLength()) {
     
    135125                return;
    136126            }
    137127
    138             int size = getLength();
    139             int start = offs+str.length();
    140             int end = start;
    141             String curText = getText(0, size);
     128            String prefix = getText(0, getLength()); // the whole text after insertion
    142129
    143             // item for lookup and selection
    144             Object item;
    145             // if the text is a number we don't autocomplete
    146             if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)) {
    147                 try {
    148                     Long.parseLong(str);
    149                     if (!curText.isEmpty())
    150                         Long.parseLong(curText);
    151                     item = lookupItem(curText, true);
    152                 } catch (NumberFormatException e) {
    153                     // either the new text or the current text isn't a number. We continue with autocompletion
    154                     item = lookupItem(curText, false);
    155                 }
    156             } else {
    157                 item = lookupItem(curText, false);
    158             }
     130            if (Config.getPref().getBoolean("autocomplete.dont_complete_numbers", true)
     131                    && prefix.matches("^\\d+$"))
     132                return;
    159133
    160             if (initial) {
    161                 start = 0;
    162             }
    163             if (item != null) {
    164                 String newText = ((AutoCompletionItem) item).getValue();
    165                 if (!newText.equals(curText)) {
    166                     selecting = true;
    167                     super.remove(0, size);
    168                     super.insertString(0, newText, a);
    169                     AutoCompletingComboBox.this.setSelectedItem(item);
    170                     selecting = false;
    171                     start = size;
    172                     end = getLength();
    173                 }
    174             }
    175             final JTextComponent editorComponent = getEditorComponent();
     134            autocomplete(prefix);
     135
    176136            // save unix system selection (middle mouse paste)
    177137            Clipboard sysSel = ClipboardUtils.getSystemSelection();
    178138            if (sysSel != null) {
    179139                Transferable old = ClipboardUtils.getClipboardContent(sysSel);
    180                 editorComponent.select(start, end);
    181140                if (old != null) {
    182141                    sysSel.setContents(old, null);
    183142                }
    184             } else {
    185                 editorComponent.select(start, end);
    186143            }
    187144        }
    188 
    189         private Object lookupItem(String pattern, boolean match) {
    190             ComboBoxModel<AutoCompletionItem> model = getModel();
    191             AutoCompletionItem bestItem = null;
    192             for (int i = 0, n = model.getSize(); i < n; i++) {
    193                 AutoCompletionItem currentItem = model.getElementAt(i);
    194                 if (currentItem.getValue().equals(pattern))
    195                     return currentItem;
    196                 if (!match && currentItem.getValue().startsWith(pattern)
    197                 && (bestItem == null || currentItem.getPriority().compareTo(bestItem.getPriority()) > 0)) {
    198                     bestItem = currentItem;
    199                 }
    200             }
    201             return bestItem; // may be null
    202         }
    203145    }
    204146
    205147    /**
     
    217159     */
    218160    public AutoCompletingComboBox(String prototype) {
    219161        super(new AutoCompletionItem(prototype));
    220         setRenderer(new AutoCompleteListCellRenderer());
    221162        final JTextComponent editorComponent = this.getEditorComponent();
    222163        editorComponent.setDocument(new AutoCompletingComboBoxDocument());
    223164        editorComponent.addFocusListener(new InnerFocusListener(editorComponent));
     
    224165    }
    225166
    226167    /**
    227      * Sets the maximum text length.
    228      * @param length the maximum text length in number of characters
     168     * Autocomplete a string.
     169     * <p>
     170     * Look in the model for an item whose true prefix matches the string. If
     171     * found, set the editor to the item and select the item in the model too.
     172     *
     173     * @param prefix The prefix to autocomplete.
    229174     */
    230     public void setMaxTextLength(int length) {
    231         this.maxTextLength = length;
     175    private void autocomplete(String prefix) {
     176        // candidate item for autocomplete
     177        AutoCompletionItem item = findBestCandidate(prefix);
     178        if (item != null) {
     179            try {
     180                locked = true;
     181                setSelectedItem(item);
     182                getEditor().setItem(item);
     183                // select the autocompleted suffix in the editor
     184                getEditorComponent().select(prefix.length(), item.getValue().length());
     185            } finally {
     186                locked = false;
     187            }
     188        }
    232189    }
    233190
    234191    /**
    235      * Convert the selected item into a String that can be edited in the editor component.
    236      *
    237      * @param cbEditor    the editor
    238      * @param item      excepts AutoCompletionListItem, String and null
     192     * Find the best candidate for autocompletion.
     193     * @param prefix The true prefix to match.
     194     * @return The best candidate (may be null)
    239195     */
    240     @Override
    241     public void configureEditor(ComboBoxEditor cbEditor, Object item) {
    242         if (item == null) {
    243             cbEditor.setItem(null);
    244         } else if (item instanceof String) {
    245             cbEditor.setItem(item);
    246         } else if (item instanceof AutoCompletionItem) {
    247             cbEditor.setItem(((AutoCompletionItem) item).getValue());
    248         } else
    249             throw new IllegalArgumentException("Unsupported item: "+item);
     196    private AutoCompletionItem findBestCandidate(String prefix) {
     197        ComboBoxModel<AutoCompletionItem> model = getModel();
     198        AutoCompletionItem bestCandidate = null;
     199        for (int i = 0, n = model.getSize(); i < n; i++) {
     200            AutoCompletionItem currentItem = model.getElementAt(i);
     201            // the "same" string is always the best candidate, but it is of
     202            // no use for autocompletion
     203            if (currentItem.getValue().equals(prefix))
     204                return null;
     205            if (currentItem.getValue().startsWith(prefix)
     206            && (bestCandidate == null || currentItem.getPriority().compareTo(bestCandidate.getPriority()) > 0)) {
     207                bestCandidate = currentItem;
     208            }
     209        }
     210        return bestCandidate;
    250211    }
    251212
    252213    /**
    253      * Selects a given item in the ComboBox model
    254      * @param item the item of type AutoCompletionItem, String or null
     214     * Sets the maximum text length.
     215     * @param length the maximum text length in number of characters
    255216     */
    256     @Override
    257     public void setSelectedItem(Object item) {
    258         setSelectedItem(item, false);
     217    public void setMaxTextLength(int length) {
     218        this.maxTextLength = length;
    259219    }
    260220
    261221    /**
     
    270230            // disable autocomplete to prevent unnecessary actions in AutoCompletingComboBoxDocument#insertString
    271231            setAutocompleteEnabled(false);
    272232        }
    273         if (item == null) {
    274             super.setSelectedItem(null);
    275         } else if (item instanceof AutoCompletionItem) {
    276             super.setSelectedItem(item);
    277         } else if (item instanceof String) {
    278             String s = (String) item;
    279             // find the string in the model or create a new item
    280             AutoCompletionItem acItem = IntStream.range(0, getModel().getSize())
    281                     .mapToObj(i -> getModel().getElementAt(i))
    282                     .filter(i -> s.equals(i.getValue()))
    283                     .findFirst()
    284                     .orElseGet(() -> new AutoCompletionItem(s, AutoCompletionPriority.UNKNOWN));
    285             super.setSelectedItem(acItem);
    286         } else {
    287             setAutocompleteEnabled(previousState);
    288             throw new IllegalArgumentException("Unsupported item: "+item);
    289         }
     233        setSelectedItem(item);
    290234        setAutocompleteEnabled(previousState);
    291235    }
    292236
     
    409353            return getEditItem();
    410354        }
    411355    }
    412 
    413     /**
    414      * ListCellRenderer for AutoCompletingComboBox
    415      * renders an AutoCompletionListItem by showing only the string value part
    416      */
    417     public static class AutoCompleteListCellRenderer extends JLabel implements ListCellRenderer<AutoCompletionItem> {
    418 
    419         /**
    420          * Constructs a new {@code AutoCompleteListCellRenderer}.
    421          */
    422         public AutoCompleteListCellRenderer() {
    423             setOpaque(true);
    424         }
    425 
    426         @Override
    427         public Component getListCellRendererComponent(
    428                 JList<? extends AutoCompletionItem> list,
    429                 AutoCompletionItem item,
    430                 int index,
    431                 boolean isSelected,
    432                 boolean cellHasFocus) {
    433             if (isSelected) {
    434                 setBackground(list.getSelectionBackground());
    435                 setForeground(list.getSelectionForeground());
    436             } else {
    437                 setBackground(list.getBackground());
    438                 setForeground(list.getForeground());
    439             }
    440 
    441             setText(item.getValue());
    442             return this;
    443         }
    444     }
    445356}