2

I have a table with x num of rows, I have a second table with the same number of rows but different columns and metadata, they have different table models. but each row represents the same object (a song).

I want to synchronize row sorting between the two tables so for example if I sort on column 2 of table 1 then rows of the table will be sorted in the same order. But currently, I just have sorted by matching sort keys so sort on the same column (but because different data get different results)

e.g

Starting point

Table 1
1    tom    
2    jane
3    fred
4    steve
5    jim

Table 2
1    oranges
2    apples
3    pears
4    lemons
5    plums

If I sort by table 1, column 2 Ascending I want to get

Table 1
2    jane
5    jim
3    fred
4    steve
1    tom

Table 2
2    apples
5    plums
3    pears
4    lemons
1    oranges

but I get

Table 1
2    jane
5    jim
3    fred
4    steve
1    tom

Table 2
2    apples
4    lemons
1    oranges
3    pears
5    plums

My sorting is done by calling setSortKeys() on table 2 to the getSortKeys() of table 1 and vice versa. I can see why it doesn't work, I am telling table 2 to sort by column 2 ascending the same as table 1 but of course these columns have different data in each table. But I can't work out a way to get table 2 to sort to the final order of table 1 instead.

One solution would be for both tables to share the same table model and just show the columns relevant to their table, but that would require a major rework of the code, so I am hoping for a more specific solution just to resolve the sorting issue.

I am using Java 11, and swingx latest version 1.6.4 (i know very old) but this delegates sorting to standard Java (earlier version that I was previously using had its own sorting) so not really a swingx question.

The real world situation, within my application is as follows, each row represents a song, and the tabs show metadata for that song. the tabs under the edit menu all share same model and all work using the setSortKeys() method described above. So here i have sorted on Mood Aggressive column

Edit metadata tab

and if I go to another tab, we see the rows are sorted in same order

Another Edit metadata tab, sorted same order

but if I go to the Edit ID3 tab, we see the rows have been sorted in different order.

ID3 Edit tab sorted different order

This is because ID3 Edit tab shows the metadata in different format (ID3) and has different table model so column x represent in the model stores different data.

Note because all models store the rowno in first column, sorting my the rowno column works for all tabs.

So from a user point of view they are just viewing different tabs of the same table, and therefore would expect sort to be consistent for the tabs

Paul Taylor
  • 13,411
  • 42
  • 184
  • 351
  • @AndrewThompson Added real world example with screenshots (although for some reason that I dont understand only showing screenshots as links) – Paul Taylor Feb 03 '22 at 11:26
  • JXTable has a createDefaultRowSorter() method, I wonder if it would be valid to share the RowSorter between the tables somehow ? – Paul Taylor Feb 03 '22 at 11:36
  • @AndrewThompson yes thats right thanks, do you understand the real world problem now ? – Paul Taylor Feb 03 '22 at 14:09
  • *"do you understand the real world problem now ?"* On the upside, yes, thanks for clarifying. On the downside, I have no idea how to implement the required feature. – Andrew Thompson Feb 03 '22 at 21:26
  • FWIW Found that for the multiple tabs that already share the table model that they can also shared the RowSorter and this works better than my previous method of having the tabs with their own RowSorter listening to sort events on other tabs. However a RowSorter is associated with a table model so at least in default case cant share a table sorter if they dont share a model – Paul Taylor Feb 08 '22 at 16:37
  • I think you need to share the same model across the tabs or else you will not be able to have the same sorting order. In your case you can assume that the tabs provide different views of the same model. You can hide the columns which are not required in a particular tab. – Hitesh A. Bosamiya Feb 09 '22 at 07:00
  • I think there's no way to automatically sort different tables. The easiest way, as others said, seems to be to store all the data in one model and display only one column in each table. Otherwise, you'll have to manually re-arrange the model for the second table so that the rows in it are in the same order as the ids of the first table. – Alexey Ivanov Feb 09 '22 at 14:49
  • @AlexeyIvanov 'Otherwise, you'll have to manually re-arrange the model for the second table so that the rows in it are in the same order as the ids of the first table'. Yes I think that is what i need to do, basically sorting model in order of the viewIndex of the sorted table, so I think this is feasible using RowSorterListener to listen to table being sorted, and then look at the viewindex of the model of the sorted table and then setting the view index of the other table to the same somehow. – Paul Taylor Feb 09 '22 at 19:40
  • @PaulTaylor Perhaps an extra column for the second table model which contains the row number of the first table could solve your problem. When the order in the 1st table changes, you update the data in that extra column and sort by it. – Alexey Ivanov Feb 09 '22 at 21:53

3 Answers3

1

I came up with the following approach which translates rowIndex for the second table using rowSorter of the first table.

    TableOneModel tableOneData = new TableOneModel( /* omitted */ );
    JTable tableOne = new JTable(tableOneData);
    TableRowSorter<TableOneModel> sorterOne = new TableRowSorter<>(tableOneData);
    tableOne.setRowSorter(sorterOne);

    TableTwoModel tableTwoData = new TableTwoModel(
            /* omitted */,
            sorterOne);
    JTable tableTwo = new JTable(tableTwoData);

The model for the first table, TableOneModel, is a subclass of AbstractTableModel implementing the required methods:

private static class TableOneModel extends AbstractTableModel {
    private final String[] columnNames;
    private final Object[][] data;

    public TableOneModel(String[] columnNames, Object[][] data) {
        this.columnNames = columnNames;
        this.data = data;
    }

    public int getRowCount() { return data.length; }

    public int getColumnCount() { return columnNames.length; }

    public Object getValueAt(int rowIndex, int columnIndex) {
        return data[rowIndex][columnIndex];
    }
}

The model for second table, TableTwoModel, stores the reference to the rowSorter of the first table to do the translation of row indexes:

private static class TableTwoModel extends TableOneModel
        implements RowSorterListener {

    private final RowSorter<? extends TableModel> otherSorter;

    public TableTwoModel(String[] columnNames, Object[][] data,
                         RowSorter<? extends TableModel> sorter) {
        super(columnNames, data);
        this.otherSorter = sorter;
        installListeners();
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        return super.getValueAt(
                otherSorter.convertRowIndexToModel(rowIndex),
                columnIndex);
    }

    private void installListeners() {
        otherSorter.addRowSorterListener(this);
    }

    @Override
    public void sorterChanged(RowSorterEvent e) {
        fireTableDataChanged();
    }
}

When the sorting order of the first table changes, the second table model calls fireTableDataChanged() to notify the view it needs to update all the data.

Edit: As Paul mentioned in the comment, the sort order of the second table should also change the first table. So the sync should work both ways.

In the updated version, both tables use TableTwoModel and the first table identifies itself as the leading one. (Just as I've been writing the update, I realised this wasn't necessary.) Thus, the implementation of TableTwoModel remains basically unchanged. I changed sorterChanged in TableTwoModel to call fireTableDataChanged() only for SORTED event type that is when the sorting of the table is complete. It's a little optimisation.

The tricky part was to sync/reset RowSorter of the tables. However, the solution proved to be pretty simple. This is achieved by installing RowSorterListener to each row sorter. If the event type is SORT_ORDER_CHANGED and the list of sort keys of this RowSorter is non-empty, the sort keys of the other are set to null. Thus, only one table is sorted and the other follows the sort order of the sorted one.

Alexey Ivanov
  • 11,541
  • 4
  • 39
  • 68
  • I tested my implementation with `RowSorter` set to the second table, it just works without any modifications. Calling `tableTwo.getRowSorter().setSortKeys(null)` returns the second table to match the order of the first table. – Alexey Ivanov Feb 11 '22 at 21:08
  • A ready-to-run test case: [TableSyncDemo.java](https://github.com/aylivex/stack-overflow/blob/main/src/TableSyncDemo.java) – Alexey Ivanov Feb 12 '22 at 12:47
  • Thank for making a test case, your solution is a more elegant one than mine, however Im not sure would be able to adapt so that could sort by either table and affect the other one, which to be fair I didnt make clear in the question. An interesting aproach but I may stick with what I have, but I have awarded you the bounty – Paul Taylor Feb 12 '22 at 15:10
  • You can do it in the opposite way too. Your first table will store reference to the `RowSorter` of the second table. You'll have to decide which one takes precedence when no sort keys are defined. If the first table becomes sorted by a column, you'll need to reset sort keys of the second; if the second table becomes sorted, you'll need to reset the sort keys of the first one. – Alexey Ivanov Feb 12 '22 at 15:15
  • I've updated my app to sync the 2nd table to the 1st one too. The link above points to the latest version. For the sake of history, [the first version](https://github.com/aylivex/stack-overflow/blob/3101876/src/TableSyncDemo.java) which sync 1st to 2nd only; and [the second version](https://github.com/aylivex/stack-overflow/blob/e5ede6e/src/TableSyncDemo.java) which syncs both ways. – Alexey Ivanov Feb 12 '22 at 16:59
  • [The third version](https://github.com/aylivex/stack-overflow/blob/1b05d35/src/TableSyncDemo.java) removes the unnecessary `leading` flag that is present in the second version. – Alexey Ivanov Feb 12 '22 at 17:42
  • Okay thanks that seems to be a neat solution – Paul Taylor Feb 13 '22 at 09:07
  • This approach makes the two models depend on each other. You can aggregate the two models into a larger one that spans across columns of the first and second tables. Like `new ConsolidatedTableModel(table1Model, table2Model)`. Then visually the tables would provide views for different columns of the consolidated model. Sorting would affect both views. There would be no need to translate row indexes, when a table becomes sorted, you reset the sort key for the other table. I haven't explored this approach, it's just an idea. – Alexey Ivanov Feb 13 '22 at 11:38
0

Here is what I meant in the comments:

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import java.awt.*;

public class TablesExample extends JPanel {
    static class MyTableModel extends AbstractTableModel {
        private String[] columnNames = {"Row Id",
                "Person",
                "Fruit"};
        private Object[][] data = {
                {"1", "Tom", "Orange"},
                {"2", "Jane", "Apple"},
                {"3", "Fred", "Pear"},
                {"4", "Steve", "Lemon"},
                {"5", "Jim", "Plum"}
        };

        public int getColumnCount() {
            return columnNames.length;
        }

        public int getRowCount() {
            return data.length;
        }

        public String getColumnName(int col) {
            return columnNames[col];
        }

        public Object getValueAt(int row, int col) {
            return data[row][col];
        }
    }

    public static void main(String[] args) {
        JFrame frame = new JFrame("Tables Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JTabbedPane tabbedPane = new JTabbedPane();
        tabbedPane.setPreferredSize(new Dimension(500, 100));

        TablesExample newContentPane = new TablesExample();
        newContentPane.setOpaque(true);

        MyTableModel model = new MyTableModel();
        TableRowSorter<MyTableModel> sorter = new TableRowSorter<>(model);
        JTable table = new JTable(model);
        table.setRowSorter(sorter);
        TableColumn column2 = table.getColumnModel().getColumn(2);
        column2.setMinWidth(0);
        column2.setMaxWidth(0);
        column2.setWidth(0);

        JScrollPane scrollPane = new JScrollPane();
        scrollPane.setViewportView(table);
        tabbedPane.add("Persons", scrollPane);

        JTable table2 = new JTable(model);
        table2.setRowSorter(sorter);
        TableColumn column1 = table2.getColumnModel().getColumn(1);
        column1.setMinWidth(0);
        column1.setMaxWidth(0);
        column1.setWidth(0);

        JScrollPane scrollPane2 = new JScrollPane();
        scrollPane2.setViewportView(table2);
        tabbedPane.add("Fruits", scrollPane2);

        frame.setContentPane(tabbedPane);

        frame.pack();
        frame.setVisible(true);
    }
}
  • Yes that would work, but trouble is my two tables have a model with same number of rows they do show different datasets (and hence columns) and whilst it is technically possible to merge the two models into one model that will have an affect on alot of other code and will be alot of work so Im not keen to do it. – Paul Taylor Feb 09 '22 at 19:36
  • I am really skeptical that you will be able to achieve this without a common model. If your other code requires subsets of the model then you can write some method(s) to get the subset as required (or through some other logic as desired and I guess it will be simpler than solving it this way). Here the sorting (if we want to call that as sorting) is based on specific row order (once the table gets sorted in another tab -- you will have row order which is to be used in another tab). – Hitesh A. Bosamiya Feb 10 '22 at 11:39
  • Problem is the model is not just used by the table, for example the column indexes are used throughout the code so if i change the model to combine the columns from both models then every reference to the data is affected. – Paul Taylor Feb 10 '22 at 12:41
0

Have a prototype working.

So using swingx we implement a subclass of TableSortController and override toggleSortOrder() and set this as the rowSorter of the main table

        public void toggleSortOrder(int column)
        {
            .........
            setSortKeys(newKeys);
        
            for(int i=0; i < getModelWrapper().getRowCount(); i++)
            {
                SecondTable.instanceOf().getRealModel()
                    .setValueAt(convertRowIndexToView(i), i, ID3TagNames.INDEX_SYNC_SORT);
            }
        
            newKeys = new ArrayList<>();
            SecondTable.instanceOf().getTable().getRowSorter().setSortKeys(newKeys);
        
            newKeys.add(new SortKey(ID3TagNames.INDEX_SYNC_SORT, this.getFirstInCycle()));
            SecondTable.instanceOf().getTable().getRowSorter().setSortKeys(newKeys);

        }

Logic is does a normal sort on main table, then sets hidden column on second table to store the view index of each row. Then remove any existing sort on second table, then sort by hidden column.

Note the two calls to setSortKey() are needed because if you sort by one column on main table, and then do another sort in both cases will be sorting second table by INDEX_SYNC_SORT ascending and hence the following code in superclass DefaultRowSorter.setSortKeys() will prevent a sort being done because the sortkey will be the same as previous sort

        if (!this.sortKeys.equals(old)) {
            fireSortOrderChanged();
            if (viewToModel == null) {
                // Currently unsorted, use sort so that internal fields
                // are correctly set.
                sort();
            } else {
                sortExistingData();
            }
        }

For now in this prototype we have a default sort controller on the SecondTable as we don't want this to do the special processing as well. But probably want to sort on both and therefore would need the toggleSort() code to check what table they are linked to and act accordingly.

Paul Taylor
  • 13,411
  • 42
  • 184
  • 351