tags:

views:

110

answers:

1

I have a JScrollPane with a JTextArea set as its view port.

I update the (multi line) text shown on the JTextArea continously about once a second. Each time the text updates, JScrollPane goes all the way to the bottom of the text.

Instead, I'd like to figure out the line number that is currently shown as the first line in the original text, and have that line be the first line shown when the text has been updated (or if the new text doesn't have that many lines, then scroll all the way to the bottom).

My first attempt of doing this was to get the current caret position, figure the line based on that, and then set the text area to show that line:

    int currentPos = textArea.getCaretPosition();
    int currentLine = 0;
    try {
        for(int i = 0; i < textArea.getLineCount(); i++) {
            if((currentPos >= textArea.getLineStartOffset(i)) && (currentPos < gameStateTextArea.getLineEndOffset(i))) {
                currentLine = i;
                break;
            }
        }
    } catch(Exception e) { }

    textArea.setText(text);

    int newLine = Math.min(currentLine, textArea.getLineCount());
    int newOffset = 0;
    try {
        newOffset = textArea.getLineStartOffset(newLine);
    } catch(Exception e) { }

    textArea.setCaretPosition(newOffset);

This was almost acceptable for my needs, but requires the user to click inside the text area to change the caret position, so that the scrolling will maintain state (which isn't nice).

How would I do this using the (vertical) scroll position instead ?

+2  A: 

This is pieced together, untested, from the API documentation:

  • use getViewport() on your JScrollPane to get a hold of the viewport.
  • use Viewport.getViewPosition() to get the top-left coordinates. These are absolute, not a percentage of scrolled text.
  • use Viewport.addChangeListener() to be notified when the top-left position changes (among other things). You may want to create a mechanism to distinguish user changes from changes your program makes, of course.
  • use Viewport.setViewPosition() to set the top-left position to where it was before the disturbance.

Update:

  • To stop JTextArea from scrolling, you may want to override its getScrollableTracksViewport{Height|Width}() methods to return false.

Update 2:

The following code does what you want. It's amazing how much trouble I had to go to to get it to work:

  • apparently the setViewPosition has to be postponed using invokeLater because if it's done too early the text update will come after it and nullify its effect.
  • also, for some weird reason perhaps having to do with concurrency, I had to pass the correct value to my Runnable class in its constructor. I had been using the "global" instance of orig and that kept setting my position to 0,0.

public class Sami extends JFrame implements ActionListener {

   public static void main(String[] args) {
      (new Sami()).setVisible(true);
   }

   private JTextArea textArea;
   private JScrollPane scrollPane;
   private JButton moreTextButton = new JButton("More text!");
   private StringBuffer text = new StringBuffer("0 Silly random text.\n");
   private Point orig = new Point(0, 0);

   public Sami() {
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      getContentPane().setLayout(new BorderLayout());
      this.textArea = new JTextArea() {
         @Override
         public boolean getScrollableTracksViewportHeight() {
            return false;
         }
         @Override
         public boolean getScrollableTracksViewportWidth() {
            return false;
         }
      };
      this.scrollPane = new JScrollPane(this.textArea);
      getContentPane().add(this.scrollPane, BorderLayout.CENTER);
      this.moreTextButton.addActionListener(this);
      getContentPane().add(this.moreTextButton, BorderLayout.SOUTH);
      setSize(400, 300);
   }

   @Override
   public void actionPerformed(ActionEvent arg0) {
      int lineCount = this.text.toString().split("[\\r\\n]").length;
      this.text.append(lineCount + "The quick brown fox jumped over the lazy dog.\n");
      Point orig = this.scrollPane.getViewport().getViewPosition();
      // System.out.println("Orig: " + orig);
      this.textArea.setText(text.toString());
      SwingUtilities.invokeLater(new LaterUpdater(orig));
   }

   class LaterUpdater implements Runnable {
      private Point o;
      public LaterUpdater(Point o) {
         this.o = o;
      }
      public void run() {
         // System.out.println("Set to: " + o);
         Sami.this.scrollPane.getViewport().setViewPosition(o);
      }
   } 

}
Carl Smotricz
I tried:Point orig = scrollPane.getViewport().getViewPosition();textArea.setText(text);scrollPane.getViewport().setViewPosition(orig);It just doesn't seem to do anything for me. With that code, the text area gets scrolled all the way to the bottom every time. (And running it inside the swing event dispatching thread).
Sami
[sigh] I can see I should have tested that. I'm going to do that now and get back to you.
Carl Smotricz
Thanks. I played around with this a bit, and it doesn't seem that the overriding actually is needed. I think the Point object returned by getViewPosition() is shared, instead of a copy, so we need to clone it, to get the same effect: final Point orig = (Point) scrollPane.getViewport().getViewPosition().clone(); textArea.setText(text); SwingUtilities.invokeLater() { new Runnable() { scrollPane.getViewport().setViewPosition(orig); } }); .. Said that, this solution works 99% of the times, but once in a while, when doing quick updates it just scroll back all the way down.
Sami
The works 99% time comment is both for the code using clone, and the code in your answer. Anyway, it's already good enough for my practical purposes, so I'll accept the answer later. Anyway, still left with the curiosity on how to make it work 100% of the time.
Sami
I feel I've worked hard enough on this. Good luck!
Carl Smotricz