In Java want to implement multi column sorting vai the table header in a sensible way

437 Views Asked by At

In Java 11 (with swingx 1.6.6) I have just implemented multi column sorting as follows:

  • Click on first Column, sorts by that columns ascending
  • Click on second Column, secondary sort by that column ascending
  • and so on, if user clicks on a column they have already clicked on the sort order changes i.e ascending to descending

But I don't have a way to reset the sort and looking at some other applications I think I want it to work in the following way:

  • Click on first Column, sorts by that columns ascending
  • Click on second Column, resets sort to primary sort by just that column ascending (same as default sort works)
  • But, cntl-click on second Column, then secondary sorts by that column and so on, if user clicks on a column they have already clicked on the sort order changes i.e ascending to descending

So whenever anyone click or cntl-clicks that causes a call to toggleSort(), but how do I capture that the user has cntl-clicked rather than clicked and know that so accessible to toggleSort()

For reference, modified toggleSort() method extends org.jdesktop.swingx.sort.TableSortController and I modified swingx code so I could access previous private methods getFirstInCycle() and getNextInCycle())

public void toggleSortOrder(int column)
{
    //Are we allowed to this sort column
    if (this.isSortable(column))
    {
        SortOrder firstInCycle = this.getFirstInCycle();

        //If all already a column in sort cycle
        if (firstInCycle != null)
        {
            //Make a copy of existing keys
            List<SortKey> keys = new ArrayList(this.getSortKeys());

            //Look for any existing sort key for column user has clicked on
            SortKey sortKey = SortUtils.getFirstSortKeyForColumn((List)keys, column);

            //If its the first one
            if (((List)keys).indexOf(sortKey) == 0)
            {
                //Swap the sort order of to next one, i.e ascending to descending
                ((List)keys).set(0, new SortKey(column, this.getNextInCycle(sortKey.getSortOrder())));
            }
            else
            {
                //Add new final sort key for this column
                ((List)keys).add(new SortKey(column, this.getFirstInCycle()));
            }

            //Trim the number of keys if we have to many
            if (((List)keys).size() > this.getMaxSortKeys()) {
                keys = ((List)keys).subList(0, this.getMaxSortKeys());
            }

          
            this.setSortKeys((List)keys);
        }
    }
}
2

There are 2 best solutions below

0
Paul Taylor On BEST ANSWER

Decided better to drop the cntl-click idea and instead restore the three stage cycle by modifying org.jdesktop.swingx.sort,DefaultSortController from

    private final static SortOrder[] DEFAULT_CYCLE 
     = new SortOrder[] {SortOrder.ASCENDING, SortOrder.DESCENDING};

to

  private final static SortOrder[] DEFAULT_CYCLE 
   = new SortOrder[] {SortOrder.ASCENDING, SortOrder.DESCENDING,SortOrder.UNSORTED}; 

and then this is my toggleSortOrder() method in my custom sort controller

/**
 * If new sort key sort ascending as after other existing sort keys
 * If existing sort key and ascending cycle change to descending
 * If existing sort key and descending remove the sort key
 * If already at MAX_SORT_COLUMNS the ignore
 * 
 * @param column
 */
@Override
public void toggleSortOrder(int column)
{
    //Are we allowed to this sort column
    if (this.isSortable(column))
    {
        SortOrder firstInCycle = this.getFirstInCycle();

        //If all already a column in sort cycle
        if (firstInCycle != null)
        {
            //Make a copy of existing keys
            List<SortKey> newKeys = new ArrayList(this.getSortKeys());

            //Look for any existing sort key for column user has clicked on
            SortKey sortKey = SortUtils.getFirstSortKeyForColumn(newKeys, column);

            //Existing Key
            if(sortKey!=null)
            {
                //Get next in cycle
                SortOrder nextSortOrder = this.getNextInCycle(sortKey.getSortOrder());

                //Swap to descending/ascending
                if(nextSortOrder==SortOrder.DESCENDING || nextSortOrder==SortOrder.ASCENDING)
                {
                    newKeys.set((newKeys).indexOf(sortKey), new SortKey(column, nextSortOrder));
                }
                //Remove from sort
                else
                {
                    newKeys.remove(sortKey);
                }
            }
            //New Key
            else
            {
                (newKeys).add(new SortKey(column, this.getFirstInCycle()));
            }

            //Trim the number of keys if we have too many
            if ((newKeys).size() > this.getMaxSortKeys()) {
                newKeys = ((List)newKeys).subList(0, this.getMaxSortKeys());
            }
            this.setSortKeys(newKeys);
        }
    }
}
4
MadProgrammer On

Sooo, I've been digging around in the code and I think you're in for a bag of issues. The handling of the mouse clicked is performed by the UI delegate (specifically the BaseTableHeaderUI), which calls TableRowSorter#toggleSortOrder directly. So the table and table header aren't involved at all, so there's no injection point through which you might be able to control this workflow.

I then thought about simply adding a MouseListener to the JTableHeader itself. My original concern was this would interface with the existing MouseListener been used by the TableHeaderUI, but if all we want to do is remove a SortKey when the header is Control+Clicked, then it "should" be okay, BUT, you'll get two calls to toggleSortOrder

Now, I'm not using SwingX, this is pure Swing only, but the concept should work .

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.RowSorter.SortKey;
import static javax.swing.SortOrder.ASCENDING;
import static javax.swing.SortOrder.DESCENDING;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;

public class Main {

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JTable table = new JTable();
                DefaultTableModel model = new DefaultTableModel(
                        new Object[]{"abc", "def", "ghi", "jkl"},
                        0);

                model.addRow(new Object[]{"A", "B", "C", "I"});
                model.addRow(new Object[]{"B", "C", "D", "J"});
                model.addRow(new Object[]{"C", "D", "E", "K"});
                model.addRow(new Object[]{"D", "E", "F", "L"});
                model.addRow(new Object[]{"E", "F", "G", "M"});
                model.addRow(new Object[]{"F", "G", "H", "N"});

                table.setModel(model);
//                table.setTableHeader(new CustomTableHeader(table));
                table.getTableHeader().setDefaultRenderer(new DefaultTableHeaderCellRenderer());
                table.setRowSorter(new TableRowSorter<DefaultTableModel>(model));

                JTableHeader header = table.getTableHeader();
                header.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                        if (!(e.getSource() instanceof JTableHeader)) {
                            return;
                        }
                        JTableHeader header = (JTableHeader) e.getSource();
                        JTable table = header.getTable();
                        RowSorter<? extends TableModel> rowSorter = table.getRowSorter();
                        if (rowSorter == null) {
                            return;
                        }
                        int column = header.columnAtPoint(e.getPoint());
                        if (column == -1) {
                            return;
                        }
                        List<? extends SortKey> sortKeys = rowSorter.getSortKeys();
                        List<SortKey> newSortKeys = new ArrayList<>(sortKeys);

                        Optional<? extends SortKey> firstMatch = sortKeys
                                .stream()
                                .filter(key -> key.getColumn() == column)
                                .findFirst();

                        if (e.isControlDown()) {
                            if (firstMatch.isPresent()) {
                                SortKey sortKey = firstMatch.get();
                                newSortKeys.remove(sortKey);
                            }
                        }
                        rowSorter.setSortKeys(newSortKeys);
                    }
                });

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new JScrollPane(table));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class DefaultTableHeaderCellRenderer extends DefaultTableCellRenderer {

        public DefaultTableHeaderCellRenderer() {
            setHorizontalAlignment(CENTER);
            setHorizontalTextPosition(LEFT);
            setVerticalAlignment(BOTTOM);
            setOpaque(false);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int column) {
            super.getTableCellRendererComponent(table, value, false, hasFocus, row, column);
            JTableHeader tableHeader = table.getTableHeader();
            if (tableHeader != null) {
                setForeground(tableHeader.getForeground());
            }
            setIcon(getIcon(table, column));
            setBorder(UIManager.getBorder("TableHeader.cellBorder"));
            return this;
        }

        protected Icon getIcon(JTable table, int column) {
            SortKey sortKey = getSortKey(table, column);
            if (sortKey != null && table.convertColumnIndexToView(sortKey.getColumn()) == column) {
                switch (sortKey.getSortOrder()) {
                    case ASCENDING:
                        return UIManager.getIcon("Table.ascendingSortIcon");
                    case DESCENDING:
                        return UIManager.getIcon("Table.descendingSortIcon");
                }
            }
            return null;
        }

        protected SortKey getSortKey(JTable table, int column) {
            RowSorter rowSorter = table.getRowSorter();
            if (rowSorter == null) {
                return null;
            }

            List sortedColumns = rowSorter.getSortKeys();
            if (sortedColumns.size() > 0) {
                return (SortKey) sortedColumns.get(0);
            }
            return null;
        }
    }
}

The problem I see here is figuring out the difference between how JTable switches been sort keys and when a sort key is actually removed via the Control+Clicked event...

And this about the point where I'd be throwing my hands in the air and just toggling the sort order between ascending, descending and none, so you just need to click your way through it, but that's me