DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

Autocomplete Combobox In Java With Filtering And Inserting New Text

07.25.2009
| 24360 views |
  • submit to reddit
        //Hi all!
//Before you try the code you should to download log4j library for debug.



import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

/**
 * Autocomplete combobox with filtering and
 * text inserting of new text
 * @author Exterminator13
 */
public class AutoCompleteCombo extends JComboBox{

    private static final Logger logger = Logger.getLogger(AutoCompleteCombo.class);
    private Model model = new Model();
    private final JTextComponent textComponent = (JTextComponent) getEditor().getEditorComponent();
    private boolean modelFilling = false;

    private boolean updatePopup;

    public AutoCompleteCombo() {

        setEditable(true);

        logger.debug("setPattern() called from constructor");
        setPattern(null);
        updatePopup = false;
        
        textComponent.setDocument(new AutoCompleteDocument());
        setModel(model);
        setSelectedItem(null);

        new Timer(20, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (updatePopup && isDisplayable()) {
                    setPopupVisible(false);
                    if (model.getSize() > 0) {
                        setPopupVisible(true);
                    }
                    updatePopup = false;
                }
            }
        }).start();
    }

      private class AutoCompleteDocument extends PlainDocument {

        boolean arrowKeyPressed = false;

        public AutoCompleteDocument() {
            textComponent.addKeyListener(new KeyAdapter() {

                @Override
                public void keyPressed(KeyEvent e) {
                    int key = e.getKeyCode();
                    if (key == KeyEvent.VK_ENTER) {
                        logger.debug("[key listener] enter key pressed");
                        //there is no such element in the model for now
                        String text = textComponent.getText();
                        if (!model.data.contains(text)) {
                            logger.debug("addToTop() called from keyPressed()");
                            addToTop(text);
                        }
                    } else if (key == KeyEvent.VK_UP ||
                               key == KeyEvent.VK_DOWN) {
                        arrowKeyPressed = true;
                        logger.debug("arrow key pressed");
                    }
                }
            });
        }

        void updateModel() throws BadLocationException {
            String textToMatch = getText(0, getLength());
            logger.debug("setPattern() called from updateModel()");
            setPattern(textToMatch);
        }

        @Override
        public void remove(int offs, int len) throws BadLocationException {

            if (modelFilling) {
                logger.debug("[remove] model is being filled now");
                return;
            }

            super.remove(offs, len);
            if (arrowKeyPressed) {
                arrowKeyPressed = false;
                logger.debug("[remove] arrow key was pressed, updateModel() was NOT called");
            } else {
                logger.debug("[remove] calling updateModel()");
                updateModel();
            }
            clearSelection();
        }
        
        @Override
        public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {

            if (modelFilling) {
                logger.debug("[insert] model is being filled now");
                return;
            }

            // insert the string into the document
            super.insertString(offs, str, a);

//            if (enterKeyPressed) {
//                logger.debug("[insertString] enter key was pressed");
//                enterKeyPressed = false;
//                return;
//            }

            String text = getText(0, getLength());
            if (arrowKeyPressed) {
                logger.debug("[insert] arrow key was pressed, updateModel() was NOT called");
                model.setSelectedItem(text);
                logger.debug( String.format("[insert] model.setSelectedItem(%s)", text) );
                arrowKeyPressed = false;
            } else if(!text.equals(getSelectedItem())){
                logger.debug("[insert] calling updateModel()");
                updateModel();
            }

            clearSelection();
        }

    }


    public void setText(String text) {
        if (model.data.contains(text)) {
            setSelectedItem(text);
        } else {
            addToTop(text);
            setSelectedIndex(0);
        }
    }

    public String getText() {
        return getEditor().getItem().toString();
    }

    private String previousPattern = null;

    private void setPattern(String pattern) {

        if(pattern!=null && pattern.trim().isEmpty())
            pattern = null;

        if(previousPattern==null && pattern ==null ||
           pattern!=null && pattern.equals(previousPattern)) {
            logger.debug("[setPatter] pattern is the same as previous: "+previousPattern);
            return;
        }

        previousPattern = pattern;

        modelFilling = true;
//        logger.debug("setPattern(): start");

        model.setPattern(pattern);
        
        if(logger.isDebugEnabled()) {
            StringBuilder b = new StringBuilder(100);
            b.append("pattern filter '").append(pattern==null ? "null" : pattern).append("' set:\n");
            for(int i=0; i<model.getSize(); i++) {
                b.append(", ").append('[').append(model.getElementAt(i)).append(']');
            }
            int ind = b.indexOf(", ");
            if(ind != -1) {
                b.delete(ind, ind+2);
            }
//            b.append('\n');
            logger.debug(b);
        }
//        logger.debug("setPattern(): end");
        modelFilling = false;
        if(pattern != null)
            updatePopup = true;
    }


    private void clearSelection() {
        int i = getText().length();
        textComponent.setSelectionStart(i);
        textComponent.setSelectionEnd(i);
    }

//    @Override
//    public void setSelectedItem(Object anObject) {
//        super.setSelectedItem(anObject);
//        clearSelection();
//    }



    public synchronized void addToTop(String aString) {
        model.addToTop(aString);
    }

      private class Model extends AbstractListModel implements ComboBoxModel {

//        String pattern;
        String selected;
        final String delimiter = ";;;";
        final int limit = 20;

        class Data {

            private List<String> list = new ArrayList<String>(limit);
            private List<String> lowercase = new ArrayList<String>(limit);
            private List<String> filtered;

            void add(String s) {
                list.add(s);
                lowercase.add(s.toLowerCase());
            }

            void addToTop(String s) {
                list.add(0, s);
                lowercase.add(0, s.toLowerCase());
            }

            void remove(int index) {
                list.remove(index);
                lowercase.remove(index);
            }

            List<String> getList() {
                return list;
            }

            List<String> getFiltered() {
                if(filtered==null)
                    filtered = list;
                return filtered;
            }

            int size() {
                return list.size();
            }

            void setPattern(String pattern) {
                if (pattern == null || pattern.isEmpty()) {
                    filtered = list;
                    AutoCompleteCombo.this.setSelectedItem(model.getElementAt(0));
                    logger.debug( String.format("[setPattern] combo.setSelectedItem(null)") );
                } else {
                    filtered = new ArrayList<String>(limit);
                    pattern = pattern.toLowerCase();
                    for(int i=0; i<lowercase.size(); i++) {
                        //case insensitive search
                        if (lowercase.get(i).contains(pattern)) {
                            filtered.add( list.get(i) );
                        }
                    }
                    AutoCompleteCombo.this.setSelectedItem(pattern);
                    logger.debug( String.format("[setPattern] combo.setSelectedItem(%s)", pattern) );
                }
                logger.debug( String.format("pattern:'%s', filtered: %s", pattern, filtered) );
            }

            boolean contains(String s) {
                if(s==null || s.trim().isEmpty())
                    return true;
                s = s.toLowerCase();
                for (String item : lowercase) {
                    if (item.equals(s)) {
                        return true;
                    }
                }
                return false;
            }
        }

        Data data = new Data();

          void readData() {
              String[] countries = {
                  "Afghanistan",
                  "Albania",
                  "Algeria",
                  "Andorra",
                  "Angola",
                  "Argentina",
                  "Armenia",
                  "Austria",
                  "Azerbaijan",
                  "Bahamas",
                  "Bahrain",
                  "Bangladesh",
                  "Barbados",
                  "Belarus",
                  "Belgium",
                  "Benin",
                  "Bhutan",
                  "Bolivia",
                  "Bosnia & Herzegovina",
                  "Botswana",
                  "Brazil",
                  "Bulgaria",
                  "Burkina Faso",
                  "Burma",
                  "Burundi",
                  "Cambodia",
                  "Cameroon",
                  "Canada",
                  "China",
                  "Colombia",
                  "Comoros",
                  "Congo",
                  "Croatia",
                  "Cuba",
                  "Cyprus",
                  "Czech Republic",
                  "Denmark",
                  "Georgia",
                  "Germany",
                  "Ghana",
                  "Great Britain",
                  "Greece",
                  "Somalia",
                  "Spain",
                  "Sri Lanka",
                  "Sudan",
                  "Suriname",
                  "Swaziland",
                  "Sweden",
                  "Switzerland",
                  "Syria",
                  "Uganda",
                  "Ukraine",
                  "United Arab Emirates",
                  "United Kingdom",
                  "United States",
                  "Uruguay",
                  "Uzbekistan",
                  "Vanuatu",
                  "Venezuela",
                  "Vietnam",
                  "Yemen",
                  "Zaire",
                  "Zambia",
                  "Zimbabwe"};

              for (String country : countries) {
                  data.add(country);
              }
          }

          boolean isThreadStarted = false;

          void writeData() {
              StringBuilder b = new StringBuilder(limit * 60);

              for (String url : data.getList()) {
                  b.append(delimiter).append(url);
              }
              b.delete(0, delimiter.length());

              //waiting thread is already being run
              if (isThreadStarted) {
                  return;
              }

              //we do saving in different thread
              //for optimization reasons (saving may take much time)
              new Thread(new Runnable() {

                  @Override
                  public void run() {
                      //we do sleep because saving operation
                      //may occur more than one per waiting period
                      try {
                          Thread.sleep(2000);
                      } catch (InterruptedException ex) {
                      }
                      //we need this synchronization to
                      //synchronize with AutoCompleteCombo.addElement method
                      //(race condition may occur)
                      synchronized (AutoCompleteCombo.this) {

                          //HERE MUST BE SAVING OPERATION
                          //(SAVING INTO FILE OR SOMETHING)
                          //don't forget replace readData() method
                          //to read saved data when creating bean

                          isThreadStarted = false;
                      }
                  }
              }).start();
              isThreadStarted = true;
          }

        public Model() {
            readData();
        }

        public void setPattern(String pattern) {

            int size1 = getSize();

            data.setPattern(pattern);

            int size2 = getSize();

            if(size1<size2) {
                fireIntervalAdded(this, size1, size2-1);
                fireContentsChanged(this, 0, size1-1);
            } else if(size1>size2) {
                fireIntervalRemoved(this, size2, size1-1);
                fireContentsChanged(this, 0, size2-1);
            }
        }

        public void addToTop(String aString) {
            if(aString==null || data.contains(aString))
                return;
            if(data.size()==0)
                data.add(aString);
            else
                data.addToTop(aString);

            while(data.size()>limit) {
                int index = data.size()-1;
                data.remove(index);
            }

            setPattern(null);
            model.setSelectedItem(aString);
            logger.debug( String.format("[addToTop] model.setSelectedItem(%s)", aString) );

            //saving into options
            if (data.size() > 0) {
                writeData();
            }
        }

        @Override
        public Object getSelectedItem() {
            return selected;
        }

          @Override
          public void setSelectedItem(Object anObject) {
              if ((selected != null && !selected.equals(anObject)) ||
                      selected == null && anObject != null) {
                  selected = (String) anObject;
                  fireContentsChanged(this, -1, -1);
              }
          }

        @Override
        public int getSize() {
            return data.getFiltered().size();
        }

        @Override
        public Object getElementAt(int index) {
            return data.getFiltered().get(index);
        }

    }



    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {

//                Logger root = Logger.getRootLogger();
//                root.addAppender(new ConsoleAppender(new PatternLayout("%d{ISO8601} [%5p] %m at %l%n")));
                Logger root = Logger.getRootLogger();
                root.addAppender(new ConsoleAppender(new PatternLayout("%d{ISO8601} %m at %L%n")));

//                BasicConfigurator.configure();

                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridLayout(3, 1));
                final JLabel label = new JLabel("label ");
                frame.add(label);
                final AutoCompleteCombo combo = new AutoCompleteCombo();
//                combo.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() {
//
//                    @Override
//                    public void keyReleased(KeyEvent e) {
//                        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
//                            String text = combo.getEditor().getItem().toString();
//                            if(text.isEmpty())
//                                return;
//                            combo.addToTop(text);
//                        }
//                    }
//                });
                frame.add(combo);
                JComboBox combo2 = new JComboBox(new String[] {"Item 1", "Item 2", "Item 3", "Item 4"});
                combo2.setEditable(true);
                frame.add(combo2);
                frame.pack();
                frame.setSize( 500, frame.getHeight() );
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

    

Comments

Snippets Manager replied on Tue, 2012/04/17 - 12:19pm

How we could use this code in an app passing our own data list?

Snippets Manager replied on Mon, 2009/08/10 - 12:30pm

I tried this code using eclipse, Java 1.5 (apple's Java VM version 12.3.0), on OS X 10.5.7. Due to being on 1.5, I had to remove the @Override annotations and replace calls to String.isEmpty() with String.length()==0. I then got the following exception: Exception in thread "AWT-EventQueue-0" 2009-08-10 09:29:09,765 setPattern() called from updateModel() at 94 java.lang.ArrayIndexOutOfBoundsException: -1 at java.util.ArrayList.get(ArrayList.java:323) at dzone.AutoCompleteCombo$Model.getElementAt(AutoCompleteCombo.java:476) at apple.laf.CUIAquaComboBox$CoreAquaItemListener.itemStateChanged(CUIAquaComboBox.java:90) at javax.swing.JComboBox.fireItemStateChanged(JComboBox.java:1162) at javax.swing.JComboBox.selectedItemChanged(JComboBox.java:1219) at javax.swing.JComboBox.contentsChanged(JComboBox.java:1266) at javax.swing.AbstractListModel.fireContentsChanged(AbstractListModel.java:100) at dzone.AutoCompleteCombo$Model.setSelectedItem(AutoCompleteCombo.java:467) at javax.swing.JComboBox.setSelectedItem(JComboBox.java:551) at dzone.AutoCompleteCombo$Model$Data.setPattern(AutoCompleteCombo.java:278) at dzone.AutoCompleteCombo$Model.setPattern(AutoCompleteCombo.java:423) at dzone.AutoCompleteCombo.setPattern(AutoCompleteCombo.java:182) at dzone.AutoCompleteCombo.access$3(AutoCompleteCombo.java:166) at dzone.AutoCompleteCombo$AutoCompleteDocument.updateModel(AutoCompleteCombo.java:95) at dzone.AutoCompleteCombo$AutoCompleteDocument.insertString(AutoCompleteCombo.java:142) ... The exception seems to happen consistently when the second character is entered into the text-box. -Chris