0

How to sort JTable alphabetically with separated lines showing the occurrence of the next letter? For example, consider how Windows Media Player 12 sorts it tracks,

Windows Media Player 12, example

Another example of this problem is for instance how Macintosh 'Finder' sorts recent opening applications by date. I can't figure out how they added additional row showing the date and lines enclosing the matching.

Any ideas of how to approach this problem?

Macintosh, Finder, example

Seek Addo
  • 1,871
  • 2
  • 18
  • 30
blueFalcon
  • 111
  • 9
  • J(X)TreeTable, maybe non free Jide has something similair – mKorbel Jun 18 '16 at 19:27
  • What is J(X)TreeTable? Is the implementation with desired look hard to approach or time-consuming using only IDE and non third-party source? – blueFalcon Jun 19 '16 at 17:40
  • [J(X)TreeTable](http://stackoverflow.com/a/7123228/714968), I think that MediaPalyer is coded as TreeTable too – mKorbel Jun 20 '16 at 09:45

2 Answers2

1

I would group my data alphabetically and then create a separate table for each letter included in my data.

You can customize the display of the header using a TableCellRenderer to get the desired look, if you are looking to add more columns as shown in your examples you can replace the header with a different component altogether.

public class MyTableFrame extends JFrame {

    MyTableFrame() {

        List<String> data = Arrays.asList(
            "Alpha1",
            "Beta1",
            "Alpha2",
            "Charlie2",
            "Alpha3",
            "Beta2",
            "Charlie1"
        );
        // Sort alphabetically
        data.sort(String::compareTo);

        // Group by first letter
        Map<Character, List<String>> collect = data.stream()
                .collect(Collectors.groupingBy(string -> string.charAt(0)));

        JPanel tablesContainer = new JPanel(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.fill = GridBagConstraints.BOTH;
        c.weightx = 1;
        c.weighty = 0;
        c.gridy = 0;

        // Create a table for each Letter
        collect.entrySet().forEach(mapEntry -> {
            Character letter = mapEntry.getKey();
            List<String> startingWithLetter = mapEntry.getValue();
            TableModel tableModel = new TableModel(startingWithLetter, letter);
            JTable table = new JTable(tableModel);
            // Table header must be added separately since there is no scroll pane
            JTableHeader tableHeader = table.getTableHeader();
            tablesContainer.add(tableHeader, c);

            c.gridy++;
            tablesContainer.add(table, c);

            c.gridy++;
        });

        add(tablesContainer);
        setVisible(true);
        pack();
    }

    static class TableModel extends AbstractTableModel {
        private List<String> data;
        private Character columnName;

        TableModel(List<String> data, Character columnName) {
            this.data = data;
            this.columnName = columnName;
        }

        @Override
        public String getColumnName(int column) {
            return columnName.toString();
        }

        @Override
        public int getRowCount() {
            return data.size();
        }

        @Override
        public int getColumnCount() {
            return 1;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            return data.get(rowIndex);
        }
    }

    public static void main(String args[]) {
        new MyTableFrame();
    }
}
Andrew I.
  • 11
  • 1
  • 3
1

To have the table rows appear in a place other than where JTable normally expects them to be, the methods in JTable you’ll need to override are rowAtPoint, getCellRect, and of course, paintComponent. To support those methods, I would keep track of the rows where section headings occur in a sorted Map, and I would keep track of the row positions in a second sorted Map.

I would then recompute those Maps whenever the table model or sort column changes, or whenever the table validates itself for any reason, which means override the tableChanged, sortedChanged, and validate methods, respectively.

The result isn’t as lengthy as you might expect:

public class SectionedTable
extends JTable {
    private static final long serialVersionUID = 1;

    private final NavigableMap<Integer, String> sectionHeadings =
                                                        new TreeMap<>();

    private final NavigableMap<Integer, Integer> rowTopEdges =
                                                        new TreeMap<>();

    // Used when calling SwingUtilities.layoutCompoundLabel.
    private final Rectangle iconBounds = new Rectangle();
    private final Rectangle textBounds = new Rectangle();

    public SectionedTable() {
        init();
    }

    public SectionedTable(TableModel model) {
        super(model);
        init();
    }

    private void init()
    {
        setShowGrid(false);
        setAutoCreateRowSorter(true);

        recomputeSections();
        recomputeRowPositions();
    }

    private void recomputeSections() {
        if (sectionHeadings == null) {
            return;
        }

        sectionHeadings.clear();

        RowSorter<? extends TableModel> sorter = getRowSorter();
        if (sorter == null) {
            return;
        }

        for (RowSorter.SortKey key : sorter.getSortKeys()) {
            SortOrder order = key.getSortOrder();
            if (order != SortOrder.UNSORTED) {
                int sortColumn = key.getColumn();

                String lastSectionStart = "";
                int rowCount = getRowCount();
                for (int row = 0; row < rowCount; row++) {
                    Object value = getValueAt(row, sortColumn);
                    if (value == null) {
                        value = "?";
                    }

                    String s = value.toString();
                    if (s.isEmpty()) {
                        s = "?";
                    }

                    String sectionStart = s.substring(0,
                        s.offsetByCodePoints(0, 1));
                    sectionStart = sectionStart.toUpperCase();

                    if (!sectionStart.equals(lastSectionStart)) {
                        sectionHeadings.put(row, sectionStart);
                        lastSectionStart = sectionStart;
                    }
                }
                break;
            }
        }
    }

    private void recomputeRowPositions() {
        if (rowTopEdges == null) {
            return;
        }

        rowTopEdges.clear();

        int y = getInsets().top;
        int rowCount = getRowCount();
        int rowHeight = getRowHeight();
        for (int row = 0; row < rowCount; row++) {
            rowTopEdges.put(y, row);
            y += getRowHeight(row);
            if (sectionHeadings.containsKey(row)) {
                y += rowHeight;
            }
        }
    }

    @Override
    public void tableChanged(TableModelEvent event) {
        recomputeSections();
        recomputeRowPositions();
        super.tableChanged(event);
    }

    @Override
    public void sorterChanged(RowSorterEvent event) {
        recomputeSections();
        recomputeRowPositions();
        super.sorterChanged(event);
    }

    @Override
    public void validate() {
        super.validate();
        recomputeRowPositions();
    }

    @Override
    public int rowAtPoint(Point location) {
        Map.Entry<Integer, Integer> entry = rowTopEdges.floorEntry(location.y);
        if (entry != null) {
            int row = entry.getValue();
            return row;
        }
        return -1;
    }

    @Override
    public Rectangle getCellRect(int row,
                                 int column,
                                 boolean includeSpacing) {

        Rectangle rect = super.getCellRect(row, column, includeSpacing);

        int sectionHeadingsAbove = sectionHeadings.headMap(row, true).size();
        rect.y += sectionHeadingsAbove * getRowHeight();

        return rect;
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        boolean ltr = getComponentOrientation().isLeftToRight();

        int rowHeight = getRowHeight();
        FontMetrics metrics = g.getFontMetrics();
        int ascent = metrics.getAscent();
        for (Map.Entry<Integer, String> entry : sectionHeadings.entrySet()) {
            int row = entry.getKey();
            String heading = entry.getValue();

            Rectangle bounds = getCellRect(row, 0, true);
            bounds.y -= rowHeight;
            bounds.width = getWidth();
            bounds.grow(-6, 0);

            iconBounds.setBounds(0, 0, 0, 0);
            textBounds.setBounds(0, 0, 0, 0);
            String text = SwingUtilities.layoutCompoundLabel(this,
                metrics, heading, null,
                SwingConstants.CENTER, SwingConstants.LEADING,
                SwingConstants.CENTER, SwingConstants.CENTER,
                bounds, iconBounds, textBounds, 0);

            g.drawString(text, textBounds.x, textBounds.y + ascent);

            int lineY = textBounds.y + ascent / 2;
            if (ltr) {
                g.drawLine(textBounds.x + textBounds.width + 12, lineY,
                                  getWidth() - getInsets().right - 12, lineY);
            } else {
                g.drawLine(textBounds.x - 12, lineY,
                                  getInsets().left + 12, lineY);
            }
        }
    }
}

You can adapt this class to make a Finder table, by changing how the headings are derived from the data. Merely examining one cell with getValueAt won’t be sufficient; instead, you’ll need to translate the sorted row to the corresponding model row, and obtain the data object for that row directly from the TableModel. Then you can compare the rows by age rather than by string data.

VGR
  • 40,506
  • 4
  • 48
  • 63
  • Great solution! However I can't seem to scroll down the table using the SectionTable. Also when my table model changes by calling the method DefaultTableModel.setDataVector(Object[][] dataVector, Object[] columnIdentifiers) the table paints itself back to original losing all section. Any ideas of how I can fix this? – blueFalcon Jun 28 '16 at 18:29
  • Is your table still showing a sort column after you call setDataVector? – VGR Jun 28 '16 at 19:17
  • No, it returns to the original order, unsorted and all sections (the lines and letter) are gone. I need to manually click on the tables-header to sort it again. Also when scrolling down it stalls at the next letter and won't go further. Scrolling up however works fine. – blueFalcon Jun 28 '16 at 19:35
  • I chose to require a sort column, since it’s hard to define the row organization without sorting. You can force sorting after calling setDataVector with `table.getRowSorter().setSortKeys(Collections.singletonList(new RowSorter.SortKey(0, SortOrder.ASCENDING)));` (feel free to replace 0 with the column number of your choice). – VGR Jun 28 '16 at 20:15
  • I've read and studied your code many times but can't seem to understand what causes the scrolling 'down' problem. My table has hundreds of rows and up to 20 sections (letters). Whenever I try to scroll down with the mouse-wheel I get stuck at the current section. However scrolling down by dragging the Srollbar Thumb works fine, as does scrolling up with the mouse-wheel. Do you know what causes this problem and how it can be fixed? – blueFalcon Jul 10 '16 at 15:07
  • I’m not sure. It didn’t happen for me, but I only had about fifty rows. You should post a new question with your code; try to remove as much irrelevant code as possible, so that the code in your new question is as small as possible while still demonstrating the problem. – VGR Jul 11 '16 at 01:04