1

I've got a JTable that updates frequently. It sits inside of a scroll pane.

Occasionally we'll see a message that we'd like to dig deeper into. Unfortunately due to the number of updates the message will scroll off the visible screen before we can finish looking at it.

I'd like to be able to freeze the viewport/scrolling while we examine the message but still allow the table model to update with new messages.

I can get the selected row to go to the top with this code:

        int firstSelectedRow = table.getSelectedRow();
        Rectangle rowLocation = table.getCellRect(firstSelectedRow, 0, false);
        scroll.getVerticalScrollBar().setValue(rowLocation.y);
        freezeScrolling.setText("Resume Updates");

but that only happens on button press and then it quickly sscrolls away.

Is there a way to tell the viewport/scroll pane to freeze on a selection and the to turn it off so that the scroll pane updates as you'd expect?

skaffman
  • 398,947
  • 96
  • 818
  • 769
chollida
  • 7,834
  • 11
  • 55
  • 85

2 Answers2

4

OK, this one teased me a lot, so I spent some time to find a way for this. I found two options:

  1. The one I prefer because I find it much simpler: block the new entries to the table model and store them in a buffer. Whenever we unfreeze, we flush the buffer to the table model.
  2. The second option consists into locating the current top visible row. If the user scrolls, we capture the top visible row. As the model updates, we locate the captured row in the table and we set the view port position to that.

Here is an SSCCE that illustrates both mechanisms (don't mind the crapiness of the code design, I tested a bunch of stuffs -- extending the JScrollPane is actually not a good idea, it can all be done externally).

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;

public class Tables {

    private static class JScrollPaneExtension extends JScrollPane implements TableModelListener, AdjustmentListener {
        private boolean frozen = false;
        private Vector<String> rowData;
        private final JTable table;
        private final MyTableModel tableModel;

        private JScrollPaneExtension(JTable table, MyTableModel tableModel) {
            super(table);
            this.table = table;
            this.tableModel = tableModel;
            tableModel.addTableModelListener(this);
            getVerticalScrollBar().addAdjustmentListener(this);
        }

        public boolean isFrozen() {
            return frozen;
        }

        public void setFrozen(boolean frozen) {
            if (frozen != this.frozen) {
                this.frozen = frozen;
                if (frozen) {
                    captureCurrentTopRowData();
                } else {
                    rowData = null;
                }
            }
        }

        private void captureCurrentTopRowData() {
            int row = table.rowAtPoint(getViewport().getViewPosition());
            if (row > -1) {
                rowData = (Vector<String>) tableModel.getDataVector().get(row);
            }
        }

        @Override
        public void adjustmentValueChanged(AdjustmentEvent e) {
            if (frozen) {
                captureCurrentTopRowData();
                scrollToRowData();
            }
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            scrollToRowData();
        }

        private void scrollToRowData() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    if (frozen) {
                        int index = tableModel.getDataVector().indexOf(rowData);
                        getViewport().setViewPosition(table.getCellRect(index, 0, true).getLocation());
                    }
                }
            });
        }

    }

    public static class MyTableModel extends DefaultTableModel {
        private int count;

        private boolean frozen = false;

        private List<Vector<String>> buffer = new ArrayList<Vector<String>>();

        public MyTableModel() {
            addColumn("Test");
        }

        public void insertNewRow() {
            Vector<String> rowData = createNewRowData();
            if (isFrozen()) {
                buffer.add(rowData);
            } else {
                insertRow(0, rowData);
            }
        }

        private Vector<String> createNewRowData() {
            Vector<String> data = new Vector<String>(1);
            data.add("Hello-" + (count++));
            return data;
        }

        public boolean isFrozen() {
            return frozen;
        }

        public void setFrozen(boolean frozen) {
            if (frozen == this.frozen) {
                return;
            }
            this.frozen = frozen;
            if (!frozen) {
                flushBuffer();
            }
        }

        private void flushBuffer() {
            for (Vector<String> rowData : buffer) {
                insertRow(0, rowData);
            }
        }
    }

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

        final MyTableModel model = new MyTableModel();
        JTable table = new JTable(model);
        final JScrollPaneExtension scroll = new JScrollPaneExtension(table, model);
        JPanel panel = new JPanel();
        final JButton freeze = new JButton("Freeze");
        freeze.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
                if (model.isFrozen()) {
                    freeze.setText("Freeze model");
                } else {
                    freeze.setText("Continue");
                }
                model.setFrozen(!model.isFrozen());
            }
        });
        final JButton freeze2 = new JButton("Freeze scroll");
        freeze2.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
                if (scroll.isFrozen()) {
                    freeze2.setText("Freeze scroll");
                } else {
                    freeze2.setText("Resume scroll");
                }
                scroll.setFrozen(!scroll.isFrozen());
            }
        });
        scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        frame.add(scroll);
        panel.add(freeze);
        panel.add(freeze2);
        frame.getContentPane().add(panel, BorderLayout.NORTH);
        frame.pack();
        frame.setVisible(true);
        Timer t = new Timer();
        t.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                SwingUtilities.invokeLater(new Runnable() {

                    @Override
                    public void run() {
                        model.insertNewRow();
                    }
                });
            }
        }, new Date(), 300);
    }
}
Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
  • +1 I like the contrast; I've added a cross-reference [here](http://stackoverflow.com/a/7519403/230513). – trashgod May 05 '12 at 11:05
  • Both approaches worked out well. I still want updates added to the table as the user may want to manually scroll up so I went with the overridden scrollPane approach. – chollida May 11 '12 at 12:57
2

You may be able to adapt the approach shown here. It conditions scrolling based on the value of getValueIsAdjusting() in an AdjustmentListener. Click on the thumb to suspend scrolling; release to resume.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045