Issue after filtering JTable

K33p3r picture K33p3r · Apr 20, 2013 · Viewed 8.6k times · Source

I have an issue with a program I am working on. To briefly explain, I have a JTable with multiple columns and rows. Particular columns have editable fields when upon changing the value other column values change according to the inputted data. Everything works well however when I've added a filter option to the JTable, changes made to an editable column won't change the values of other columns as intended after applying the filter. I've attached a couple of images to show the problem.

The first image shows the unfiltered table working correctly. Changing a Discount column value will reduce the corresponding price stored in the GPL column by the percent the inputted discount and displayed in the corresponding row in the SP column. Changing a Quantity column value will multiply the corresponding SP column price with the inputted quantity and displayed in the corresponding row in the Total column.

Unfiltered table

The second image shows the filtered table not working as intended. Changing a value in either Discount or Quantity columns will not change the intended columns.

Filtered table

I've added the SSCCE code below which contains 2 classes. First is the table itself and the second is the listener for the table.

EDIT I've changed the code of the class according to camickr's answer and now fully works.

TableCellChange class

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.math.BigDecimal;
import java.math.MathContext;
import java.text.DecimalFormat;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

public final class TableCellChange extends JPanel {  

    private static JFrame frameTableCellChange;
    private JPanel panelTable, panelButtons;
    private JButton buttonResetDiscounts, buttonResetQuantities, buttonExit;
    private JTextField textFilterBox, quantityField, discountField;
    private JLabel labelFilter;
    private DefaultTableModel tableModel;
    private JTable table;
    private TableRowSorter<DefaultTableModel> sorter;
    private TableColumn columnDiscount, columnTotal, columnQuantity;
    private TableCellListener tableCellListener;
    private String checkForNull;
    private DecimalFormat decimalFormatUS;
    private Locale localeUSFormat;    
    private BigDecimal valueDiscount, valueGPL, resultDiscount, resultSP, resultTotal,
            backupDiscount = new BigDecimal("0"); 
    private int selectedColumnIndex, selectedRowIndex, valueQuantity, backupQuantity = 1;

    public TableCellChange() {

        super(); 

        panelTable = new JPanel();
        panelButtons = new JPanel();
        setLayout(new BorderLayout());

        createTable();
        createButtons();
        add(panelTable, BorderLayout.NORTH);
        add(panelButtons, BorderLayout.CENTER);

        // Always focus on the JTextField when opening the window
        SwingUtilities.invokeLater(new Runnable()  {
           @Override
           public void run() {  
               textFilterBox.requestFocusInWindow();
           }
        });
    } // -> TableCellChange()

    // Create the buttons for the query result window
    public void createButtons() {
        GridBagLayout gridbag = new GridBagLayout();
        GridBagConstraints gridcons = new GridBagConstraints();
        gridcons.fill = GridBagConstraints.HORIZONTAL;
        panelButtons.setLayout(gridbag);

        labelFilter = new JLabel("Quick search:");
        gridcons.insets = new Insets(5,0,0,0);
        gridcons.gridx = 0;
        gridcons.gridy = 0;
        gridcons.gridwidth = 2;
        gridbag.setConstraints(labelFilter, gridcons);
        labelFilter.setHorizontalAlignment(JLabel.CENTER);
        panelButtons.add(labelFilter);

        // Create text field for filtering
        textFilterBox = new JTextField();
        gridcons.insets = new Insets(5,0,0,0);
        gridcons.gridx = 0;
        gridcons.gridy = 1;
        gridcons.gridwidth = 2;
        gridbag.setConstraints(textFilterBox, gridcons);
                textFilterBox.getDocument().addDocumentListener(
                new DocumentListener() {
                    @Override
                    public void changedUpdate(DocumentEvent e) {
                        tableFilter();
                    }
                    @Override
                    public void insertUpdate(DocumentEvent e) {
                        tableFilter();
                    }
                    @Override
                    public void removeUpdate(DocumentEvent e) {
                        tableFilter();
                    }
                }); // -> DocumentListener()
        panelButtons.add(textFilterBox);

        // Create the button to reset the discount column to 0%
        buttonResetDiscounts = new JButton("Reset all discounts");
        buttonResetDiscounts.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e)
            {          
                BigDecimal valueGPL, valueTotal;
                int valueQuantity;
                for (int i = 0; i < table.getModel().getRowCount(); i++) {
                    valueGPL = new BigDecimal( table.getModel().
                            getValueAt(i, 2).toString().replaceAll("[$,]", "") );
                    table.getModel().setValueAt("0%", i, 3);   
                    table.getModel().setValueAt(DecimalFormat
                            .getCurrencyInstance(localeUSFormat).format(valueGPL), i, 4);
                    valueQuantity = Integer.parseInt( table.getModel().
                            getValueAt(i, 5).toString() );
                    valueTotal = valueGPL.multiply(new BigDecimal(valueQuantity),
                            new MathContext(BigDecimal.ROUND_HALF_EVEN));
                    table.getModel().setValueAt(DecimalFormat
                            .getCurrencyInstance(localeUSFormat).format(valueTotal), i, 6);
                }
            }
        });         
        gridcons.insets = new Insets(10,0,0,0);
        gridcons.gridx = 0;
        gridcons.gridy = 3;
        gridcons.gridwidth = 1;
        gridbag.setConstraints(buttonResetDiscounts, gridcons);
        panelButtons.add(buttonResetDiscounts);

        // Create button to reset the quantity column to 1 
        buttonResetQuantities = new JButton("Reset all quantities");
        buttonResetQuantities.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e)
            {             
                BigDecimal valueSP;
                for (int i = 0; i < table.getModel().getRowCount(); i++) {                    
                    valueSP = new BigDecimal( table.getModel().
                            getValueAt(i, 4).toString().replaceAll("[$,]", "") );
                    table.getModel().setValueAt("1", i, 5); 
                    table.getModel().setValueAt(DecimalFormat.
                            getCurrencyInstance(localeUSFormat).format(valueSP), i, 6);   
                }
            }          
        });         
        gridcons.insets = new Insets(10,0,0,0);
        gridcons.gridx = 1;
        gridcons.gridy = 3;
        gridcons.gridwidth = 1;
        gridbag.setConstraints(buttonResetQuantities, gridcons);
        panelButtons.add(buttonResetQuantities);

        // Create button for closing the window and releasing resources
        buttonExit = new JButton("Exit");
        buttonExit.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e)
            {                
                System.exit(0);
            }
        }); 
        gridcons.insets = new Insets(5,0,0,0);
        gridcons.gridx = 0;
        gridcons.gridy = 5;
        gridcons.gridwidth = 2;
        gridbag.setConstraints(buttonExit, gridcons);
        panelButtons.add(buttonExit);    

    } // -> createButtons()


    // Filters the JTable based on user input
    private void tableFilter() {
        RowFilter<DefaultTableModel, Object> tableRowFilter;// = null;
        // If current expression doesn't parse, don't update
        try {
            tableRowFilter = RowFilter.regexFilter("(?i)" + textFilterBox.
                    getText(), 0, 1, 2);
        } catch (java.util.regex.PatternSyntaxException e) {
            return;
          }
        sorter.setRowFilter(tableRowFilter);       
    } // -> tableFilter

    // Method that creates the JTable
    public void createTable() {

        // Create listener for selecting all text when a text field gains focus
        KeyboardFocusManager.getCurrentKeyboardFocusManager()
        .addPropertyChangeListener("permanentFocusOwner", new PropertyChangeListener() {
        @Override
           public void propertyChange(final PropertyChangeEvent e) {
               if (e.getNewValue() instanceof JTextField) {
                   SwingUtilities.invokeLater(new Runnable() {
                       @Override
                       public void run() {
                           JTextField textField = (JTextField)e.getNewValue();
                           textField.selectAll();
                       }
                   });
               }
           }
        });

        String[] columnNames = {"Model", "Description", "GPL", "Discount", "SP",
                "Quantity", "Total"};

        Object[][] data = {
            {"MR16", "desc1", "$649.00", "0%", "$649.00", new Integer(1), "$649.00"},
            {"MR24", "desc2", "$1,199.00", "0%", "$1,199.00", new Integer(1), "1,199.00"},
            {"MR62", "desc3", "$699.00", "0%", "$699.00", new Integer(1), "$699.00"},
            {"MR66", "desc4", "$1,299.00", "0%", "$1,299.00", new Integer(1), "$1,299.00"},
            {"MX80", "desc5", "$1,995.00", "0%", "$1,995.00", new Integer(1), "$1,995.00"},
            {"MX90", "desc6", "$3,995.00", "0%", "$3,995.00", new Integer(1), "$3,995.00"},
            {"MX400", "desc7", "$15,995.00", "0%", "$15,995.00", new Integer(1), "$15,995.00"},
            {"MX600", "desc8", "$31,995.00", "0%", "$31,995.00", new Integer(1), "$31,995.00"},
            {"MS22-HW", "desc9", "$1,999.00", "0%", "$1,999.00", new Integer(1), "$1,999.00"},
            {"MS42-HW", "desc10", "$3,499.00", "0%", "$3,499.00", new Integer(1), "$3,499.00"},

        };

        // Create the TableModel and populate it  
        tableModel = new DefaultTableModel(data, columnNames) {
            Class [] classes = {String.class, String.class, String.class,
                String.class, String.class, int.class, String.class, Boolean.class};                
            @Override
            public Class getColumnClass(int column) {
                return classes[column];
            }
        };

        // Create a JTable and populate it with the content of the TableModel
        table = new JTable(tableModel) {
            @Override
            public boolean isCellEditable(int row, int column) {
                if (column == 0 || column == 1 || column == 2 || column == 4 ||
                        column == 6) {
                    return false;
                }
                return true;
            }
        };

        // This sorter is used for text filtering
        sorter = new TableRowSorter<>(tableModel);
        for (int column = 3; column < 6; column++) {
            sorter.setSortable(column, false);
        }
        table.setRowSorter(sorter);

        columnTotal= table.getColumnModel().getColumn(6);
        columnTotal.setPreferredWidth(100);

        // Filter user input in the quantity text field to only allow digits
        discountField =new JTextField();
        discountField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e)
            {
                if(!Character.isDigit(e.getKeyChar()) && e.getKeyChar() !=KeyEvent.VK_BACK_SPACE) {
                    discountField.setEditable(false);
                    discountField.setBackground(Color.WHITE);
                } else {
                    discountField.setEditable(true);
                }
            }
        });

        // Set the text field to the cells of the quantity column 
        columnQuantity = table.getColumnModel().getColumn(5);
        columnQuantity.setCellEditor(new DefaultCellEditor (discountField));

        // Filter user input in the discount text field to only allow digits
        quantityField =new JTextField();
        quantityField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e)
            {
                if(!Character.isDigit(e.getKeyChar()) && e.getKeyChar() !=KeyEvent.VK_BACK_SPACE) {
                    quantityField.setEditable(false);
                    quantityField.setBackground(Color.WHITE);
                    //JOptionPane.showMessageDialog(null,"Only digit input is allowed!");
                } else {
                    quantityField.setEditable(true);
                }
            }
        });

        // Set the text field to the cells of the quantity column 
        columnDiscount = table.getColumnModel().getColumn(3);
        columnDiscount.setCellEditor(new DefaultCellEditor(discountField));

        // Create an US number format
        localeUSFormat = Locale.US;
        decimalFormatUS = (DecimalFormat) DecimalFormat.getInstance(localeUSFormat);
        decimalFormatUS.setMaximumFractionDigits(2);

        // Create abstract action which listens for changes made in the JTable  
        Action actionTableListener = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {

                TableCellListener tcl = (TableCellListener)e.getSource();
                // Get the current row and column index of the table
                selectedRowIndex = tcl.getRow();
                selectedColumnIndex = tcl.getColumn();  
                TableModel model = tcl.getTable().getModel();

                // Have a string variable check for null cell value 
                checkForNull = model.getValueAt(selectedRowIndex,selectedColumnIndex).toString();

                // Change the discounted and total price values 
                if (selectedColumnIndex == 3) {

                    // Check if the discount value is null and replace with 
                    // last used value if true                   
                    if (checkForNull.equals("")) {
                        model.setValueAt(backupDiscount + "%",selectedRowIndex, selectedColumnIndex);
                        return;
                    }

                    // Get the discount value and replace the '%' with nothing
                    valueDiscount = new BigDecimal(( model
                            .getValueAt(selectedRowIndex,selectedColumnIndex)
                            .toString().replaceAll("[%]","") ));
                    // 
                    model.setValueAt(valueDiscount + "%",selectedRowIndex, selectedColumnIndex);

                    // Check if the discount value is greater than 100
                    if ( (valueDiscount.compareTo(new BigDecimal(100)) == 1 ) ) {
                        model.setValueAt(backupDiscount + "%",selectedRowIndex, selectedColumnIndex);
                        JOptionPane.showMessageDialog(null,"Discount cannot be more than 100%.");
                    } else {
                        backupDiscount = valueDiscount;
                        valueDiscount = valueDiscount.divide(new BigDecimal(100)
                                , 2, BigDecimal.ROUND_HALF_EVEN);

                        // Calculate SP and Total values based on the discount input
                        valueGPL = new BigDecimal( ( model
                                .getValueAt(selectedRowIndex,selectedColumnIndex - 1)
                                .toString().replaceAll("[$,]","") ) );
                        // Get the quantity value
                        valueQuantity = Integer.parseInt( ( model
                                .getValueAt(selectedRowIndex,selectedColumnIndex + 2)
                                .toString() ) );
                        // Calculate the new discount value
                        resultDiscount = valueGPL.multiply(valueDiscount, 
                                new MathContext(BigDecimal.ROUND_HALF_EVEN));
                        // Calculate the new SP value
                        resultSP = valueGPL.subtract(resultDiscount, 
                                new MathContext(BigDecimal.ROUND_HALF_EVEN));
                        // Calculate the new result value
                        resultTotal = resultSP.multiply(new BigDecimal(valueQuantity), 
                                new MathContext(BigDecimal.ROUND_HALF_EVEN));
                        // Display the new SP value
                        model.setValueAt(DecimalFormat.getCurrencyInstance(localeUSFormat)
                                .format(resultSP),selectedRowIndex, selectedColumnIndex + 1);
                        // Display the new Total value
                        model.setValueAt(DecimalFormat.getCurrencyInstance(localeUSFormat)
                                .format(resultTotal),selectedRowIndex, selectedColumnIndex + 3);
                      }
                }
                // Change the total price values based on the quantity column 
                if (selectedColumnIndex == 5) {
                    // Check if the quantity value is null and replace with 
                    // last used value if true
                    if (checkForNull.equals("")) {
                        model.setValueAt(backupQuantity,selectedRowIndex, selectedColumnIndex);
                        return;
                    }

                    // Change total price value based on the quantity column
                    resultSP = new BigDecimal( ( model.
                            getValueAt(selectedRowIndex,
                            selectedColumnIndex - 1).toString().replaceAll("[$,]","") ) );
                    valueQuantity = Integer.parseInt( ( model.getValueAt(selectedRowIndex,
                            selectedColumnIndex).toString() ) );

                    // Check if the value quantity is over a certain limit
                    if (valueQuantity <= 0 || valueQuantity >= 999999) {
                        model.setValueAt(backupQuantity,selectedRowIndex, selectedColumnIndex);
                        JOptionPane.showMessageDialog(null,"Quantity value is too high or invalid!");
                    } else {

                        // If the value is under the limit: backup the new quantity
                        // value, calculate the new total value and display it
                        backupQuantity = valueQuantity;
                        resultTotal = resultSP.multiply(new BigDecimal(valueQuantity),
                                new MathContext(BigDecimal.ROUND_HALF_EVEN));
                        model.setValueAt(DecimalFormat.getCurrencyInstance(localeUSFormat)
                                .format(resultTotal), selectedRowIndex, selectedColumnIndex + 1);
                      }
                } 

            }
        }; // -> AbstractAction() 

        tableCellListener = new TableCellListener(table, actionTableListener);
        table.setPreferredScrollableViewportSize(table.
               getPreferredSize());       
        table.setRowHeight(22);

        setVisibleRowCount(table,10);

        table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
        table.setFillsViewportHeight(true);
        table.getTableHeader().setReorderingAllowed(false);
        table.getTableHeader().setResizingAllowed(false);
        panelTable.add(new JScrollPane(table));
    } // -> createTable() 

    // Method to display a fixed number of rows in the JTable viewport
    public static void setVisibleRowCount(JTable table, int rows){ 
        int height = 0; 
        for(int row=0; row<rows; row++) {
            height += table.getRowHeight(row);
        } 
        table.setPreferredScrollableViewportSize(new Dimension( 
            table.getPreferredScrollableViewportSize().width, height )); 
     }

      // Create and display the contents of the frame
      public static void showGUI() {
        // Disable boldface controls
        UIManager.put("swing.boldMetal", Boolean.FALSE);

        // Create the frame
        frameTableCellChange = new JFrame("Table frame");
        frameTableCellChange.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);     
        frameTableCellChange.addWindowListener(new WindowAdapter() {
            @Override
                public void windowClosing(WindowEvent we) {
                System.exit(0);
            }
        });

        // Create and set up the content pane.
        TableCellChange newContentPane = new TableCellChange();
        newContentPane.setOpaque(true); //content panes must be opaque
        frameTableCellChange.setContentPane(newContentPane);

        // Arrange and display the window.
        frameTableCellChange.pack(); //must be called first 
        frameTableCellChange.setLocationRelativeTo(null); //center window
        frameTableCellChange.setResizable(false); 
        frameTableCellChange.setVisible(true);        
      } //-> showQueryResultGUI() 

   public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
         try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.
                    UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException |
                IllegalAccessException |
                javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(TableCellChange.class.getName()).
                    log(java.util.logging.Level.SEVERE, null, ex);
        }
        // Display the frame and it's contents      
        TableCellChange.showGUI();
            }
        });
    } //-> main(String[] args)      

} //-> TableCellChange class

EDIT This class was created by Rob Camick (a.k.a. camickr), all credits go to him for creating this awesome piece of code. Only comments were removed from the code in order to respect the character limit.

TableCellListener class

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
import javax.swing.JTable;
import javax.swing.SwingUtilities;

/*
 *  This class listens for changes made to the data in the table via the
 *  TableCellEditor. When editing is started, the value of the cell is saved
 *  When editing is stopped the new value is saved. When the old and new
 *  values are different, then the provided Action is invoked.
 *  The source of the Action is a TableCellListener instance.
 */
public class TableCellListener implements PropertyChangeListener, Runnable {
    private JTable table;
    private Action action;

    private int row;
    private int column;
    private Object oldValue;
    private Object newValue;

    public TableCellListener(JTable table, Action action) {
        this.table = table;
        this.action = action;

        this.table.addPropertyChangeListener(this);
    }

    private TableCellListener(JTable table, int row, int column, Object oldValue, Object newValue) {
        this.table = table;
        this.row = row;
        this.column = column;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    public int getColumn() {
        return column;
    }

    public Object getNewValue() {
        return newValue;
    }

    public Object getOldValue() {
        return oldValue;
    }

    public int getRow() {
        return row;
    }

    public JTable getTable() {
        return table;
    }
    @Override
    public void propertyChange(PropertyChangeEvent e) {  
        if ("tableCellEditor".equals(e.getPropertyName())) {
            if (table.isEditing()) {
                processEditingStarted();
            } else {
                processEditingStopped();
            }
        }
    }

    private void processEditingStarted() {
        SwingUtilities.invokeLater(this);
    }
    @Override
    public void run() {
        row = table.convertRowIndexToView(table.getEditingRow());
        row = table.getEditingRow();

        column = table.convertColumnIndexToModel(table.getEditingColumn());

        oldValue = table.getModel().getValueAt(row, column);
        newValue = null;
    }

    private void processEditingStopped() {
        newValue = table.getModel().getValueAt(row, column);   
        if (!newValue.equals(oldValue)) {

            TableCellListener tcl = new TableCellListener(
                getTable(), getRow(), getColumn(), getOldValue(), getNewValue());

            ActionEvent event = new ActionEvent(
                tcl,
                ActionEvent.ACTION_PERFORMED,
                "");
            action.actionPerformed(event);
        }
    }
}

I understand that when filtering a table the indexes of the table view change and must be synchronized with the indexes of the underlying model. How can that be done in order for the filtered table to work?

Answer

tenorsax picture tenorsax · Apr 20, 2013

You may need to do the conversion from view to model. Take a look at Sorting and Filtering part of How to Use Tables tutorial:

When a table uses a sorter, the data the users sees may be in a different order than that specified by the data model, and may not include all rows specified by the data model. The data the user actually sees is known as the view, and has its own set of coordinates. JTable provides methods that convert from model coordinates to view coordinates — convertColumnIndexToView and convertRowIndexToView — and that convert from view coordinates to model coordinates — convertColumnIndexToModel and convertRowIndexToModel.

EDIT: convert from view (table) to model:

Replace these rows:

row = table.convertRowIndexToView(table.getEditingRow());
row = table.getEditingRow();

With:

row = table.convertRowIndexToModel(table.getEditingRow());