Set Size of JComboBox PopupMenu

ahorn42 picture ahorn42 · Sep 30, 2011 · Viewed 7.2k times · Source

i am programming a custom component which extends a JComboBox. My problem is, the PopupMenu won't actualise its size if i am adding or removing an item. So there are e.g. 2 items in the list, but if there were 4 before i had 2 "empty" items in the PopupMenu as well.

The only workaround i found was to do (in JIntelligentComboBox.java line 213)


this.setPopupVisible(false);
this.setPopupVisible(true);

but the result will be a flickering PopupMenu :-(

So what else could i do to refresh/repaint the PopupMenu without flickering?

For testing: the component and a little test programm
To generate my problem you could e.g.:

  • type "e"
  • press "return"
  • type "m"

Thanks in advance

Edit: My goal is a ComboBox that acts like e.g. the adressbar in Firefox or Chrome, i want to show all items of the PopupMenu that contain the typed chars.

cboxtester.java:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.plaf.basic.BasicComboBoxRenderer;


public class cboxtester extends JFrame {

    private DefaultComboBoxModel dcm = new DefaultComboBoxModel(new Object[][] {new Object[] {"Mittagessen", "", 0}, 
                                                                                new Object[] {"Essen", "", 0}, 
                                                                                new Object[] {"Frühstück", "", 0}, 
                                                                                new Object[] {"Abendessen", "", 0}});

    private JIntelligentComboBox icb = new JIntelligentComboBox(dcm);

    private cboxtester(){
        this.add(icb, BorderLayout.CENTER);

        this.add(new JButton("bla"), BorderLayout.EAST);

        this.pack();
        this.setVisible(true);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        cboxtester cbt = new cboxtester();
    }

}

JIntelligentComboBox.java:

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.Vector;

import javax.swing.ComboBoxEditor;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultRowSorter;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.MutableComboBoxModel;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import javax.swing.plaf.metal.MetalComboBoxEditor;


public class JIntelligentComboBox extends JComboBox {

    private ArrayList<Object> itemBackup = new ArrayList<Object>();

    /**  Initisiert die JIntelligentComboBox */
    private void init(){

        class searchComboBoxEditor extends BasicComboBoxEditor {
            public searchComboBoxEditor(){
                super();
            }

            @Override
            public void setItem(Object anObject){
                if (anObject == null) {
                    super.setItem(anObject);
                } else {
                    Object[] o = (Object[]) anObject;
                    super.setItem(o[0]);
                }
            }

            @Override
            public Object getItem(){
                return new Object[]{super.getItem(), super.getItem(), 0};
            }
        }

        this.setEditor(new searchComboBoxEditor());

        this.setEditable(true);

        class searchRenderer extends BasicComboBoxRenderer {

            public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus){
                if (index == 0) {
                    setText("");
                    this.setPreferredSize(new Dimension(1, 1));
                    return this;
                }

                this.setPreferredSize(new Dimension(160, 17));

                if (index == list.getModel().getSize() - 1) {
                    this.setBorder(new EmptyBorder(0, 3, 1, 3));
                } else {
                    this.setBorder(new EmptyBorder(0, 3, 0, 3));
                }

                Object[] v = (Object[]) value;
                //System.out.println(v[0]);
                this.setFont(new Font("Arial", Font.PLAIN, 12));
                this.setBackground(Color.white);

                String s = (String) v[0];
                String lowerS = s.toLowerCase();
                String sf = (String) v[1];
                String lowerSf = sf.toLowerCase();
                ArrayList<String> notMatching = new ArrayList<String>();

                if (!sf.equals("")){
                    int fs = -1;
                    int lastFs = 0;
                    while ((fs = lowerS.indexOf((String) lowerSf, (lastFs == 0) ? -1 : lastFs)) > -1) {
                        notMatching.add(s.substring(lastFs, fs));
                        lastFs = fs + sf.length();
                        //System.out.println(fs+sf.length());
                    }
                    notMatching.add(s.substring(lastFs));
                    //System.out.println(notMatching);
                }

                String html = "";

                if (notMatching.size() > 1) {
                    html = notMatching.get(0);
                    int start = html.length();
                    int sfl = sf.length();
                    for (int i = 1; i < notMatching.size(); i++) {
                        String t = notMatching.get(i);
                        html += "<b style=\"color: black;\">" + s.substring(start, start + sfl) + "</b>" + t;
                        start += sfl + t.length();
                    }
                }

                System.out.println(index + html);

                this.setText("<html><head></head><body style=\"color: gray;\">" + html + "</body></head>");
                return this;
            }

        }  

        this.setRenderer(new searchRenderer());

        // leeres Element oben einfügen
        int size = this.getModel().getSize();
        Object[] tmp = new Object[this.getModel().getSize()];

        for (int i = 0; i < size; i++) {
            tmp[i] = this.getModel().getElementAt(i);
            itemBackup.add(tmp[i]);
        }

        this.removeAllItems();

        this.getModel().addElement(new Object[]{"", "", 0});
        for (int i = 0; i < tmp.length; i++) {
            this.getModel().addElement(tmp[i]);
        }

        // keylistener hinzufügen
        this.getEditor().getEditorComponent().addKeyListener(new KeyListener() {

            @Override
            public void keyPressed(KeyEvent e) {
                // TODO Auto-generated method stub
            }

            @Override
            public void keyReleased(KeyEvent e) {
                // TODO Auto-generated method stub
                searchAndListEntries(((JTextField)JIntelligentComboBox.this.getEditor().getEditorComponent()).getText());
                //System.out.println(((JTextField)JIntelligentComboBox.this.getEditor().getEditorComponent()).getText());
            }

            @Override
            public void keyTyped(KeyEvent e) {
                // TODO Auto-generated method stub
            }
        });
    }

    public JIntelligentComboBox(){
        super();
    }

    public JIntelligentComboBox(MutableComboBoxModel aModel){
        super(aModel);
        init();
    }

    public JIntelligentComboBox(Object[] items){
        super(items);
        init();
    }

    public JIntelligentComboBox(Vector<?> items){
        super(items);
        init();
    }

    @Override
    public MutableComboBoxModel getModel(){
        return (MutableComboBoxModel) super.getModel();
    }

    private void searchAndListEntries(Object searchFor){        
        ArrayList<Object> found = new ArrayList<Object>();

        //System.out.println("sf: "+searchFor);

        for (int i = 0; i < this.itemBackup.size(); i++) {
            Object tmp = this.itemBackup.get(i);
            if (tmp == null || searchFor == null) continue;

            Object[] o = (Object[]) tmp;
            String s = (String) o[0];
            if (s.matches("(?i).*" + searchFor + ".*")){
                found.add(new Object[]{((Object[])tmp)[0], searchFor, ((Object[])tmp)[2]});
            }
        }

        this.removeAllItems();          

        this.getModel().addElement(new Object[] {searchFor, searchFor, 0});

        for (int i = 0; i < found.size(); i++) {
            this.getModel().addElement(found.get(i));
        }

        this.setPopupVisible(true);     
    }



}

Answer

trashgod picture trashgod · Sep 30, 2011

I have revised your sscce below, and I noticed a few things:

  1. The anomaly you observe is not apparent when using apple.laf.AquaComboBoxUI. In particular, entering and deleting text grows and shrinks the list as expected. You might try the revised code on your platform.

  2. I switched from KeyListener to KeyAdapter for expedience, but that's not a solution. You should probably use a DocumentListener. It can't be mutated while in use, as you are doing now, so I didn't pursue this further.

  3. Always build the GUI on the event dispatch thread.

  4. Hard-coded dimensions and novel fonts rarely look right on other look & feel implementations. I simply removed yours to get the appearance shown.

  5. Your constructor modifies the model after constructing the parent, so the result depends on the order of instantiation. A separate model might be easier to manage.

Update: Added code to verify @camickr's solution.

Combo image

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Window;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JTextField;
import javax.swing.MutableComboBoxModel;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import javax.swing.plaf.basic.BasicComboPopup;

public class CBoxTest extends JFrame {

    private CBoxTest() {
        DefaultComboBoxModel dcm = new DefaultComboBoxModel();
        StringBuilder s = new StringBuilder();
        for (char i = 'a'; i < 'm'; i++) {
            s.append(i);
            dcm.addElement(new Object[]{s.toString(), "", 0});
        }
        JIntelligentComboBox icb = new JIntelligentComboBox(dcm);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.add(icb, BorderLayout.CENTER);
        this.add(new JButton("Button"), BorderLayout.EAST);
        this.pack();
        this.setLocationRelativeTo(null);
        this.setVisible(true);
    }

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

            @Override
            public void run() {
                CBoxTest cbt = new CBoxTest();
            }
        });
    }

    class JIntelligentComboBox extends JComboBox {

        private List<Object> itemBackup = new ArrayList<Object>();

        public JIntelligentComboBox(MutableComboBoxModel aModel) {
            super(aModel);
            init();
        }

        private void init() {
            this.setRenderer(new searchRenderer());
            this.setEditor(new searchComboBoxEditor());
            this.setEditable(true);
            int size = this.getModel().getSize();
            Object[] tmp = new Object[this.getModel().getSize()];
            for (int i = 0; i < size; i++) {
                tmp[i] = this.getModel().getElementAt(i);
                itemBackup.add(tmp[i]);
            }
            this.removeAllItems();
            this.getModel().addElement(new Object[]{"", "", 0});
            for (int i = 0; i < tmp.length; i++) {
                this.getModel().addElement(tmp[i]);
            }
            final JTextField jtf = (JTextField) this.getEditor().getEditorComponent();
            jtf.addKeyListener(new KeyAdapter() {

                @Override
                public void keyReleased(KeyEvent e) {
                    searchAndListEntries(jtf.getText());
                }
            });
        }

        @Override
        public MutableComboBoxModel getModel() {
            return (MutableComboBoxModel) super.getModel();
        }

        private void searchAndListEntries(Object searchFor) {
            List<Object> found = new ArrayList<Object>();
            for (int i = 0; i < this.itemBackup.size(); i++) {
                Object tmp = this.itemBackup.get(i);
                if (tmp == null || searchFor == null) {
                    continue;
                }
                Object[] o = (Object[]) tmp;
                String s = (String) o[0];
                if (s.matches("(?i).*" + searchFor + ".*")) {
                    found.add(new Object[]{
                            ((Object[]) tmp)[0], searchFor, ((Object[]) tmp)[2]
                        });
                }
            }
            this.removeAllItems();
            this.getModel().addElement(new Object[]{searchFor, searchFor, 0});
            for (int i = 0; i < found.size(); i++) {
                this.getModel().addElement(found.get(i));
            }
            this.setPopupVisible(true);
            // https://stackoverflow.com/questions/7605995
            BasicComboPopup popup =
                (BasicComboPopup) this.getAccessibleContext().getAccessibleChild(0);
            Window popupWindow = SwingUtilities.windowForComponent(popup);
            Window comboWindow = SwingUtilities.windowForComponent(this);

            if (comboWindow.equals(popupWindow)) {
                Component c = popup.getParent();
                Dimension d = c.getPreferredSize();
                c.setSize(d);
            } else {
                popupWindow.pack();
            }
        }

        class searchRenderer extends BasicComboBoxRenderer {

            @Override
            public Component getListCellRendererComponent(JList list,
                Object value, int index, boolean isSelected, boolean cellHasFocus) {
                if (index == 0) {
                    setText("");
                    return this;
                }
                Object[] v = (Object[]) value;
                String s = (String) v[0];
                String lowerS = s.toLowerCase();
                String sf = (String) v[1];
                String lowerSf = sf.toLowerCase();
                List<String> notMatching = new ArrayList<String>();

                if (!sf.equals("")) {
                    int fs = -1;
                    int lastFs = 0;
                    while ((fs = lowerS.indexOf(lowerSf, (lastFs == 0) ? -1 : lastFs)) > -1) {
                        notMatching.add(s.substring(lastFs, fs));
                        lastFs = fs + sf.length();
                    }
                    notMatching.add(s.substring(lastFs));
                }
                String html = "";
                if (notMatching.size() > 1) {
                    html = notMatching.get(0);
                    int start = html.length();
                    int sfl = sf.length();
                    for (int i = 1; i < notMatching.size(); i++) {
                        String t = notMatching.get(i);
                        html += "<b style=\"color: black;\">"
                            + s.substring(start, start + sfl) + "</b>" + t;
                        start += sfl + t.length();
                    }
                }
                this.setText("<html><head></head><body style=\"color: gray;\">"
                    + html + "</body></head>");
                return this;
            }
        }

        class searchComboBoxEditor extends BasicComboBoxEditor {

            public searchComboBoxEditor() {
                super();
            }

            @Override
            public void setItem(Object anObject) {
                if (anObject == null) {
                    super.setItem(anObject);
                } else {
                    Object[] o = (Object[]) anObject;
                    super.setItem(o[0]);
                }
            }

            @Override
            public Object getItem() {
                return new Object[]{super.getItem(), super.getItem(), 0};
            }
        }
    }
}