Search code examples
javaswingjtablejcomboboxtablecelleditor

JComboBox in JTable is not displaying the selection


As simple as Renderers and Editors sound and despite the dozen or so SO bookmarks I return to regarding similar issues I’m missing something elementary. I want to drag any old text file into a 2-column JTable, have the first column display the filename and the second contain a JComboBox whose options depend on the contents of the dragged file. (In the code below I just fake a few entries.)

This all works fine until I make a selection from a combo box - the selection doesn’t display - just a combo box, populated correctly but no selection made. I know it must have something to do with my misuse of renderers/editors but after at least two weeks of flailing I’m seeking professional help. And if you think I’ve totally missed the boat on how renderers and editors are written, well, I’m glad you didn’t see my earlier attempts.

Hopefully this code qualifies as an SSCCE - sincere apologies if I’ve included something I shouldn’t have. I’ve retained the DnD stuff just in case it has some significance.

For what it’s worth, I use a static list of ComboBoxModels (one per row) since each JComboBox contains different options, and likewise TableCellEditors (although I don’t know if that’s the right way to go about it).

To run this just drag any file into the table that appears and then make a selection from the JComboBox in the right column and watch it ignore you. Thanks very much, even if you have some advice without taking the trouble of running this.

Java 1.7/OS X 10.9.5/Eclipse Mars.2

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.AbstractCellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.MutableComboBoxModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ListDataListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

public class Main extends JFrame {

    static List<AComboBoxModel> priceComboModels = new ArrayList<AComboBoxModel>();
    static List<DefaultCellEditor> editors = new ArrayList<DefaultCellEditor>();

    public Main() {
        setLayout(new BorderLayout());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(500, 400));
        JPanel panel = new JPanel(new BorderLayout());
        JTable table = new JTable(0, 2) {
            public TableCellEditor getCellEditor(int rinx, int cinx) {
                if (cinx == 0) {
                    return super.getCellEditor(rinx, cinx);
                }
                return editors.get(rinx);
            }
        };
        table.setPreferredScrollableViewportSize(new Dimension(360, 80));
        table.setTransferHandler(new ATransferHandler());
        table.setModel(new ATableModel());
        TableColumnModel tcm = table.getColumnModel();
        tcm.getColumn(0).setHeaderValue("File Name");
        tcm.getColumn(1).setHeaderValue("Selection");
            TableColumn column = tcm.getColumn(1);
            column.setCellRenderer(new ACellRenderer());
            column.setCellEditor(new ACellEditor());
        table.setDragEnabled(true);
        table.setFillsViewportHeight(true);

        JScrollPane sp = new JScrollPane(
            table,
            JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
            JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
        );

        panel.add(sp, BorderLayout.CENTER);
        panel.setPreferredSize(new Dimension(200, 300));
        add(panel, BorderLayout.CENTER);
        pack();
    }

    public static int addComboModel(AComboBoxModel model) {
        priceComboModels.add(model);
        return priceComboModels.size() - 1;
    }

    public static AComboBoxModel getComboModelAt(int inx) {
        return priceComboModels.get(inx);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Main().setVisible(true);
            }
        });
    }
}
class ATableModel extends DefaultTableModel {
    List<ARecord> data = new ArrayList<ARecord>();

    public void addRow(ARecord row) {
        data.add(row);
        fireTableRowsInserted(data.size() - 1, data.size() - 1);
    }

    @Override
    public int getRowCount() {
        return data == null ? 0 : data.size();
    }

    @Override
    public int getColumnCount() {
        return 2;
    }

    public void setValueAt(Object value, int rinx, int cinx) {
        ARecord row = data.get(rinx);

        switch (cinx) {
        case 0:
            row.setFilename((String) value);
            break;
        case 1:
            row.setCbox((JComboBox) value);
            break;
        }
    }

    @Override
    public Object getValueAt(int rinx, int cinx) {
        Object returnValue = null;
        ARecord row = data.get(rinx);

        switch (cinx) {
        case 0:
            returnValue = row.getFilename();
            break;
        case 1:
            returnValue = row.getCbox();
            break;
        }
        return returnValue;
    }

    // I assume this is unnecessary since column 1 defaults to text
    // and column 2 is handled by ACellRenderer. I think.
//  @Override
//  public Class getColumnClass(int cinx) {
//      return cinx == 0 ? String.class : JComboBox.class;
//  }
}
//////////////////////////////////////////////////////////////////////////////////

// This class handles the drag and drop.
class ATransferHandler extends TransferHandler {

    int getSourceActions(JList<String> lst) {
        return TransferHandler.COPY;
    }

    Transferable createTransferable(JList<String> list) {
        return null;
    }

    void exportDone(JList<String> lst, Transferable data, int action) {
    }

    public boolean canImport(TransferHandler.TransferSupport info) {
        return true;
    }

    //////////////////////////////////////////////////////////////////////////
    // This is the method of interest where the dropped text file is handled.
    //////////////////////////////////////////////////////////////////////////

    public boolean importData(TransferHandler.TransferSupport info) {
        if (! info.isDrop()) return false;
        JTable table = (JTable)info.getComponent();
        Transferable tr = info.getTransferable();
        List<File> files = null;
        try {
            files = (List<File>)tr.getTransferData(DataFlavor.javaFileListFlavor);
        } catch(UnsupportedFlavorException | IOException e) {
        }

        ATableModel tm = (ATableModel)table.getModel();
        String[] options;

        // For each dropped text file...

        for (File fl : files) {
            String fname = fl.getName();

            // Just fill the JComboBox with some unique options for now
            // (in practice this comes from the dropped text file contents).
            String dummyText = fname.substring(0, 5);
            options = new String[] { dummyText + "_A", dummyText + "_B", dummyText + "_C" };

            // Create a ComboBoxModel for this JComboBox containing the selection options.
            AComboBoxModel cboModel = new AComboBoxModel(options);

            // Create the combo box itself.
            JComboBox<String> cbox = new JComboBox<String>();

            // Assign the model to the box.
            cbox.setModel(cboModel);

            // Create and add to the editor list the table cell editor.
            Main.editors.add(new DefaultCellEditor(cbox));

            // Also add the ComboBoxModel to the model list.
            Main.addComboModel(cboModel);

            // Add the row to the model data.
            tm.addRow(new ARecord(fname, cbox));            
        }
        return true;
    }
}
///////////////////////////////////////////////////////////////////////////////////////////
class ARecord {
    String filename;
    JComboBox cbox;

    // Just a bean to describe a table row (a filename and a JComboBox).
    public ARecord(String filename, JComboBox cbox) {
        super();
        this.filename = filename;
        this.cbox = cbox;
    }
    public String getFilename() {
        return filename;
    }
    public void setFilename(String filename) {
        this.filename = filename;
    }
    public JComboBox getCbox() {
        return cbox;
    }
    public void setCbox(JComboBox cbox) {
        this.cbox = cbox;
    }
}
///////////////////////////////////////////////////////////////////////////////////////////

// This is the model for the JComboBoxes. A different model is instantiated
// for each row since each one has different contents.
class AComboBoxModel implements MutableComboBoxModel {
    List<String> items = new ArrayList<String>();

    public AComboBoxModel(String[] items) {
        this.items = Arrays.asList(items);
    }
    @Override
    public int getSize() {
        return items.size();
    }
    @Override
    public Object getElementAt(int index) {
        return items.get(index);
    }
    @Override
    public void addListDataListener(ListDataListener l) {       
    }
    @Override
    public void removeListDataListener(ListDataListener l) {        
    }
    @Override
    public void setSelectedItem(Object anItem) {
    }
    @Override
    public Object getSelectedItem() {
        return null;
    }
    @Override
    public void addElement(Object item) {
    }
    @Override
    public void removeElement(Object obj) {
    }
    @Override
    public void insertElementAt(Object item, int index) {
    }
    @Override
    public void removeElementAt(int index) {
    }
}
//////////////////////////////////////////////////////////////////////////////////////

// I won't pretend that I'm confident as to how this should work. My guess is that
// I should just retrieve the appropriate ComboBoxModel, assign it and return.
class ACellRenderer extends JComboBox implements TableCellRenderer {

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
            int rinx, int cinx) {       
        setModel(Main.getComboModelAt(rinx));
        return this;
    }
}
/////////////////////////////////////////////////////////////////////////////////////////

class ACellEditor extends AbstractCellEditor implements TableCellEditor {

    static JComboBox box = null;

    // This is where I think I'm actually lost. I don't understand the significance of
    // returning a JComboBox when one was already created when the text file was
    // dropped. Is it correct to just assign the appropriate ComboBoxModel to a JComboBox
    // and return it here?
    public Component getTableCellEditorComponent(JTable table,
            Object value,
            boolean isSelected,
            int rinx,
            int cinx) {

        box = (JComboBox)(table.getModel().getValueAt(rinx, cinx));
        box.setModel(Main.getComboModelAt(rinx));
        return box;
    }

    @Override
    public Object getCellEditorValue() {
        return box;
    }
}

Solution

  • make a selection from the JComboBox in the right column and watch it ignore you

    Something is wrong with your custom editor and I'm not sure what. You have a big problem in that you are trying to use a JComboBox as the data of the editor. This is completely wrong.

    But the good new is that there is no need for you to use a custom renderer or a custom editor.

    You should NOT be storing a JComboBox in the TableModel. You simply store the String of the selected item from the combo box. (This will be done for you automatically by the default combo box editor).

    There is no need for you to create a new editor for every file that is dragged to the table.

    the second contain a JComboBox whose options depend on the contents of the dragged file

    So the only part of the table that you need to customize is the getCellEditor(...) method.

    I would guess you would have a different editor for a given file extension.

    So the basic code might be something like:

    int modelColumn = convertColumnIndexToModel( column );
    
    if (modelColumn == 1)
    {
        String file = getModel.getValueAt(row, 0);
    
        if (file.endsWith(".txt"))
            return txtEditor;
        else if (file.endsWith(".html"))
            return htmlEditor;
    }
    
    return super.getCellEditor(row, column);
    

    Check out: How to add unique JComboBoxes to a column in a JTable (Java) for a working example. The logic in that posting does have a separate editor by row for demonstration purposes only. The example demonstrates that the code works with the default renderers and editors. All you need to do is provide the items for each combo box editor.

    In your case the editor will be based on the file type so the logic needs to test the data in the first column.

    Note: the nested if/else statement is not a good solution. You might want to use a Hashmap of filetype/editor. Then the getCellEditor(...) method would just be a Hashmap lookup once you extract the filetype for the File.

    So your dragging code should have nothing to do with the editors of the table. You need to know before hand which file types you want to support and define the valid items for each of those file types.

    Also, your TableModel should NOT extend DefaultTableModel. You are providing your own data storage and implementing all the methods so you should just be extending the AbstractTableModel.