1

I have extended JTable so it can sort the data alphabetically and separate the data by sections according to their first letter.

See this question and marked answer for more clarification

The extended table works fine. However when sorted, by clicking on the table's header, I am facing a problem when scrolling down the table (using mouse-wheel). enter image description here

Here is the code to the extended table. (Note: you can also find this code in the hyperlink above).

import java.awt.BorderLayout;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.RowSorter;
import javax.swing.SortOrder;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableModel;
import javax.swing.JScrollPane;
import javax.swing.table.DefaultTableModel;



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++) {
                    System.out.println("row er    : " + row);
                    System.out.println("rowcount er   : " + rowCount);
                    System.out.println("sortColumn er  :   " + sortColumn);
                    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) {
        //super.tableChanged(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);
            }
        }
    }

    public static void main(String[] args) {

        DefaultTableModel model;
        Object[][] data = new Object[50][5];
        String[] columnNames = {"First Name",
            "Last Name",
            "Sport",
            "# of Years",
            "Vegetarian"};

        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 5; j++) {
                data[i][j] = "Amy";
            }
        }

        for (int i = 10; i < 20; i++) {
            for (int j = 0; j < 5; j++) {
                data[i][j] = "Bob";
            }
        }

        for (int i = 20; i < 30; i++) {
            for (int j = 0; j < 5; j++) {
                data[i][j] = "Joe";
            }
        }

        for (int i = 30; i < 50; i++) {
            for (int j = 0; j < 5; j++) {
                data[i][j] = "M";
            }
        }

        SectionedTable table = new SectionedTable();
        model = new DefaultTableModel(data, columnNames);
        table.setModel(model);

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JTable table = new SectionedTable();
                table.setModel(model);
                JScrollPane scrollPane = new JScrollPane(table);
                frame.add(scrollPane, BorderLayout.CENTER);
                frame.setSize(500, 500);
                frame.setVisible(true);
            }
        });
    }
}

I've been struggling to find out what might cause this problem and I'm not near any solution. I think it might be at the rowAtPoint() or getCellRect(), but I'm not sure. Can someone spot where this problem might lie in the code?

Community
  • 1
  • 1
blueFalcon
  • 111
  • 9
  • 2
    *"The X works fine. However.."* LOL! Wish I had a dollar for every time I've heard that phrase.. – Andrew Thompson Jul 17 '16 at 19:05
  • @AndrewThompson I am a big fan of yours. Many of your answers on Swing section has helped me a lot! I challenge you to solve my problem! – blueFalcon Jul 17 '16 at 19:14
  • 2
    @blueFalcon, if you are a fan of Andrew's then you should know that you need to post a proper MCVE/SSCCE when posting a question. Otherwise we don't have the information needed to solve the problem. – camickr Jul 17 '16 at 19:27
  • 1
    I agree with @camickr: please create and post a valid [mcve], with code posted here with your question and not in a link, that would allow **us** to fully test and see if your code works fine and where the problem might be coming from. Question though: what is the purpose of your overriding your JTable's paintComponent method? – Hovercraft Full Of Eels Jul 17 '16 at 19:33
  • I've no clue where the source of my problem could be so I posted a minimum code with all methods that could possibly cause it. I tried to describe my problem explicitly as I could with those images above and posted a Class that you could just copy-and-paste and fully test it. I apologize for posting such a poor question, although I know this is selfish of me asking this kind of question, but I've been struggling with this for weeks and desperately wanting this table to work. @HovercraftFullOfEels, the purpose is to draw the section's first letter and lines to separate them. – blueFalcon Jul 17 '16 at 20:45
  • @blueFalcon, `posted a Class that you could just copy-and-paste and fully test it.` - how can we test it?. Where do you add the table to the frame? Where do you add data to the table?. – camickr Jul 17 '16 at 20:55
  • @camickr, I excluded that. I've edited it and posted a full-version with data and frame. Note, you have to click on the table's header to sort it first. Hope you can help. – blueFalcon Jul 17 '16 at 21:25

1 Answers1

2

I noticed that the getScrollableUnitIncrement(...) method is returning 0 when the scrolling stops functioning.

As a quick hack I added the following to your table:

@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)
{
    int increment = super.getScrollableUnitIncrement(visibleRect, orientation, direction);

    if (increment == 0)
        increment = 16;

    return increment;
}

Don't know if it will cause other problems. From this I was able to determine that 0 is returned and therefore no scrolling is done.

camickr
  • 321,443
  • 19
  • 166
  • 288