javasortingjtablemultiple-columnstablerowsorter

Java JTable freeze rows to top by always sorting fixed column and allowing user to sort any secondary column


I'm using a JTable that displays multiple sortable columns. I need any rows that contain a certain status to be frozen to the top of the list regardless of how the user has sorted the rest of the list. For example, using the following unsorted list: Unsorted Table

When the user clicks on the header for File Size, it should freeze any rows with the status "Downloading" to the top and then sort by the File Size column as seen here: Sorted Table

It is possible to solve this by simply using two JTables, but doing so looks ugly and presents its own challenges:

  1. Resizing a column requires some sort of listener to resize the column of the second table when a column in the first is resized
  2. Sorting requires some sort of listener to sort the second table when the first is sorted
  3. Need to constantly move rows of data back and forth between tables as the Status value changes

I decided this approach was not acceptable and thought up another approach: add a hidden Position column that has a value '1' for any rows that I want frozen to the top and a '2' for all other statuses and permanently keep this column sorted in ascending order. I was able to use a TableRowSorter with multiple SortKeys to sort the data in this manner on initial load; however, I am unable to keep the Position column sort happening prior to the user-selected column sort as there is no mechanism to prepend a SortKey. Trying to add another SortKey always adds it AFTER the existing SortKey(s).

I first tried adding a RowSorterListener to the TableRowSorter and re-sorting the table first on the Position column and then on the user-selected column on sorterChanged() event, but this did not work because it created an endless loop because the sorterChanged() event fired every time anything messed with the sort order.

Next, I tried adding a MouseListener to the TableHeader and manually sorting the table based on whichever column the user clicked. This half worked but ultimately failed because the JTable built-in sorter code still fires. So when a user clicks on a header, it sorts it once via my code and then sorts it the opposite direction via the JTable internal sort code. Thus resulting in no sort at all. I couldn't figure out any way to consume or prevent this internal event except to disable the TableHeader completely. This was unacceptable though because it also disables the ability for the user to resize columns and the directional arrows no longer display.

Doing some searching online, I came across a solution that functions perfectly (https://stackoverflow.com/a/37721594/6143124) but ultimately cannot be used because it will not be supported in future versions of Java and prints a warning detailing as much on Java 9:

WARNING: An illegal reflective access operation has occurred 
WARNING: Illegal reflective access by pkg.PredefinedRowSorter to field javax.swing.DefaultRowSorter.sortKeys WARNING: Please consider reporting this to the maintainers of pkg.PredefinedRowSorter 
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

I have been stumped on this one for the better park of a week and would greatly appreciate any help or guidance.

Thanks, Syd


Solution

  • I was able to achieve the desired outcome by creating my own TableRowSorter and overriding the toggleSortOrder() method.

    import java.util.ArrayList;
    import java.util.List;
    
    import javax.swing.SortOrder;
    import javax.swing.table.TableModel;
    import javax.swing.table.TableRowSorter;
    
    public class PredefinedTableRowSorter<M extends TableModel> extends TableRowSorter<TableModel>
    {
        public PredefinedTableRowSorter (TableModel model)
        {
            super(model);
        }
    
        @Override
        public void toggleSortOrder (int column)
        {
            List<SortKey> newSortKeys = new ArrayList<>();
            newSortKeys.add(new SortKey(0, SortOrder.ASCENDING));
    
            SortKey currentKey = getSortKey(column);
            SortOrder sortOrder = SortOrder.ASCENDING;
    
            if (currentKey != null) {
                sortOrder = currentKey.getSortOrder() == SortOrder.ASCENDING ? SortOrder.DESCENDING : SortOrder.ASCENDING;
            }
    
            currentKey = new SortKey(column, sortOrder);
            newSortKeys.add(currentKey);
    
            super.setSortKeys(newSortKeys);
    
            super.sort();
        }
    
        private SortKey getSortKey (int column)
        {
            for (SortKey key : super.getSortKeys()) {
                if (key.getColumn() == column) {
                    return key;
                }
            }
    
            return null;
        }
    }
    

    This solution presented a new issue: the sort arrow icon only appeared on the first column sorted. I overcame this by writing a custom DefaultTableCellRenderer and applying it to the table headers as necessary but it doesn't match the system look and feel (Windows 10 normally shows the sort arrows in the top middle of the header, whereas it gets placed after the text in my custom renderer).