Search code examples
javaswingsortingjtablejtableheader

Custom TableHeaderUI breaks custom TableRowSorter


I wrote a custom TableRowSorter that changes the sort order when you click on a JTableHeader from
SortOrder.UNSORTED => SortOrder.ASCENDING => SortOrder.DESCENDING => SortOrder.ASCENDING => SortOrder.DESCENDING => ...
to
SortOrder.UNSORTED => SortOrder.ASCENDING => SortOrder.DESCENDING =>SortOrder.UNSORTED => SortOrder.ASCENDING => SortOrder.DESCENDING => ...

ADU_SortOrder.java

package order;

import java.util.ArrayList;
import java.util.List;

import javax.swing.SortOrder;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

public class ADU_SortOrder<M extends TableModel> extends TableRowSorter<M> {
    public ADU_SortOrder(M model) {
        setModel(model);
    }

    boolean firstTime = true; //Needed in case there are any initial sort keys
    int columnHolder = -1;

    @Override
    public void toggleSortOrder(int column) {
        List<? extends SortKey> sortKeys = getSortKeys();
        if(sortKeys.size() == 0) { //For if there are no initial sort keys
            List<SortKey> keys = new ArrayList<SortKey>();
            keys.add(new SortKey(column, SortOrder.ASCENDING));
            setSortKeys(keys);
            return;
        }

        if (sortKeys.size() > 0 && columnHolder == column || firstTime) {
            if(firstTime) {
                firstTime = false;
                columnHolder = column;
                if(column != sortKeys.get(0).getColumn()) {
                    List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
                    keys.set(0, new SortKey(column, SortOrder.ASCENDING));
                    setSortKeys(keys);
                    return;
                }
            }

            List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
            keys.set(0, new SortKey(column, setNextOrder(sortKeys.get(0).getSortOrder())));
            setSortKeys(keys);
            return;
        } else if(sortKeys.size() > 0 && columnHolder != column && !firstTime) {
            List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
            keys.set(0, new SortKey(column, SortOrder.ASCENDING));
            setSortKeys(keys);
            columnHolder = column;
            return;
        }
        super.toggleSortOrder(column);
    }

    private SortOrder setNextOrder(SortOrder order) {
        switch (order) {
            case ASCENDING:
                return SortOrder.DESCENDING;
            case DESCENDING:
                return SortOrder.UNSORTED;
            case UNSORTED:
                return SortOrder.ASCENDING;
            default:
                return SortOrder.UNSORTED;
        }
    }
}

And it works really well when you implement into something like this

JTableTest.java - Working example

import java.awt.GridLayout;

import java.util.List;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableModel;
import javax.swing.RowSorter;
import javax.swing.SortOrder;

import order.ADU_SortOrder;

@SuppressWarnings("serial")
public class JTableTest extends JFrame {
    private JTableTest() {
        super("JTable Test");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new GridLayout(1, 1));
        createPanel();
        pack();
        setVisible(true);
    }

    JPanel panel = new JPanel(new GridLayout(1, 1));
    JScrollPane scroll;

    private void createPanel() {
        Object[] headers = {"Select", "Title", "Artist", "Length", "Title"};
        Object[][] sampleData = {{true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"},
                {false, "Spanish Train", "Chris De Burgh", "358", "Spanish Train"},
                {true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"}};
        JTable table = new JTable(sampleData, headers);

        //Sort Order Part
        ADU_SortOrder<TableModel> sortOrder = new ADU_SortOrder<TableModel>(table.getModel());
        List<RowSorter.SortKey> sortKeys = new ArrayList<>();
        sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
        sortOrder.setSortKeys(sortKeys);
        table.setRowSorter(sortOrder);

        scroll = new JScrollPane(table);
        panel.add(scroll);
        getContentPane().add(panel);
    }

    public static void main(String[] args) {
        new JTableTest();
    }
}

However, as soon as you add a custom BasicTableHeaderUI such as the example one below.

CustomHeaderUI.java

package ui;
import javax.swing.plaf.basic.BasicTableHeaderUI;
import javax.swing.table.TableColumnModel;

@SuppressWarnings("serial")
public class CustomHeaderUI extends BasicTableHeaderUI {
    public void canReorder(String col, boolean reorder) {
        TableColumnModel cM = header.getColumnModel();

        for(int i = 0; i < cM.getColumnCount(); i++) {
            if(header.getTable().getColumnName(i).equals(col)) {
                canReorder(i, reorder);
                break;
            }
        }
    }

    public void canReorder(String col, int instance, boolean reorder) {
        TableColumnModel cM = header.getColumnModel();
        int column = -1;

        for(int i = 0; i < cM.getColumnCount(); i++) {
            if(header.getTable().getColumnName(i).equals(col)) {
                column++;
                if(column == instance - 1) {
                    canReorder(i, reorder);
                    break;
                }
            }
        }
    }

    public void canReorderAll(String col, boolean reorder) {
        TableColumnModel cM = header.getColumnModel();

        for(int i = 0; i < cM.getColumnCount(); i++) {
            if(header.getTable().getColumnName(i).equals(col))
                canReorder(i, reorder);
        }
    }

    public void canReorder(int col, boolean reorder) {

    }
}

JTableTest.java - SSSCE/MCVE

import java.awt.GridLayout;

import java.util.List;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.RowSorter.SortKey;
import javax.swing.SortOrder;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableModel;

import order.ADU_SortOrder;

import ui.CustomHeaderUI;

@SuppressWarnings("serial")
public class JTableTest extends JFrame {
    private JTableTest() {
        super("JTable Test");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new GridLayout(1, 1));
        createPanel();
        pack();
        setVisible(true);
    }

    JPanel panel = new JPanel(new GridLayout(1, 1));
    JScrollPane scroll;

    private void createPanel() {
        Object[] headers = {"Select", "Title", "Artist", "Length", "Title"};
        Object[][] sampleData = {{true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"},
                {false, "Spanish Train", "Chris De Burgh", "358", "Spanish Train"},
                {true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"}};
        JTable table = new JTable(sampleData, headers);
        ADU_SortOrder<TableModel> sortOrder = new ADU_SortOrder<TableModel>(table.getModel());
        List<SortKey> keys = new ArrayList<SortKey>();
        keys.add(new RowSorter.SortKey( 0, SortOrder.ASCENDING));
        sortOrder.setSortKeys(keys);
        table.setRowSorter(sortOrder);

        //Adds Custom UI
        CustomHeaderUI headerUI = new CustomHeaderUI();
        headerUI.installUI(table.getTableHeader());

        headerUI.canReorder("Title", true);
        headerUI.canReorder("Title", 1, true);
        headerUI.canReorderAll("Title", true);
        headerUI.canReorder(3, true);

        scroll = new JScrollPane(table);
        panel.add(scroll);
        getContentPane().add(panel);
    }

    public static void main(String[] args) {
        new JTableTest();
    }
}

That includes some sample CustomHeaderUI methods shows clearly the two problems. Firsly the sort order is incorrect. If you start on the first column, Column 0, the sortorder is
SortOrder.ASCENDING => SortOrder.UNSORTED => SortOrder.DESCENDING => SortOrder.ASCENDING => SortOrder.UNSORTED => SortOrder.DESCENDING => ...

Starts ascending because of the keys.add(new RowSorter.SortKey( 0, SortOrder.ASCENDING)); not becasue of a glitch with my code

This is not the desired sort order described at the start. The second problem is that when you drag the columns in the MCVE it jumps two columns instead of one column. I would really appreciate help with fixing these problems.


Solution

  • headerUI.installUI(table.getTableHeader());

    In this case, MouseListener is duplicated installed in the JTableHeader.

    javax.swing.ToolTipManager@726e5bdf
    javax.swing.plaf.basic.BasicTableHeaderUI$MouseInputHandler@600da835
    TEST: installUI
    javax.swing.ToolTipManager@726e5bdf
    javax.swing.plaf.basic.BasicTableHeaderUI$MouseInputHandler@600da835
    javax.swing.plaf.basic.BasicTableHeaderUI$MouseInputHandler@17f30210
    
    import java.awt.*;
    import java.awt.event.*;
    // import java.util.List;
    // import java.util.ArrayList;
    import javax.swing.*;
    import javax.swing.plaf.basic.BasicTableHeaderUI;
    import javax.swing.table.*;
    import javax.swing.table.TableColumnModel;
    //import ui.CustomHeaderUI;
    
    public class JTableTest2 extends JFrame {
      private JTableTest2() {
        super("JTable Test");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new GridLayout(1, 1));
        createPanel();
        pack();
        setVisible(true);
      }
    
      JPanel panel = new JPanel(new GridLayout(1, 1));
    
      private void createPanel() {
        Object[] headers = {"Select", "Title", "Artist", "Length", "Title"};
        Object[][] sampleData = {
          {true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"},
          {false, "Spanish Train", "Chris De Burgh", "358", "Spanish Train"},
          {true, "Bat Outta Hell", "Meat Loaf", "673", "Bat Outta Hell"}
        };
        JTable table = new JTable(sampleData, headers);
    
        // Custom TableRowSorter is completely irrelevant to this problem:
        // //Sort Order Part
        // ADU_SortOrder<TableModel> sortOrder = new ADU_SortOrder<>(table.getModel());
        // List<RowSorter.SortKey> sortKeys = new ArrayList<>();
        // sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
        // sortOrder.setSortKeys(sortKeys);
        // table.setRowSorter(sortOrder);
    
        JTableHeader header = table.getTableHeader();
        testPrintMouseListener(header);
    
        //Adds Custom UI
        CustomHeaderUI headerUI = new CustomHeaderUI();
        //XXX: headerUI.installUI(table.getTableHeader());
        table.getTableHeader().setUI(headerUI);
    
        testPrintMouseListener(header);
    
        panel.add(new JScrollPane(table));
        getContentPane().add(panel);
      }
    
      private static void testPrintMouseListener(JTableHeader header) {
        for (MouseListener l : header.getMouseListeners()) {
          System.out.println(l);
        }
      }
    
      public static void main(String... args) {
        EventQueue.invokeLater(() -> {
          new JTableTest2();
        });
      }
    }
    
    class CustomHeaderUI extends BasicTableHeaderUI {
      @Override public void installUI(JComponent c) {
        super.installUI(c);
        System.out.println("TEST: installUI");
      }
    }