views:

712

answers:

2

Hi all,

I am attempting to mimic the functionality of Adium and most other chat clients I've seen, wherein the scrollbars advance to the bottom when new messages come in, but only if you're already there. In other words, if you've scrolled a few lines up and are reading, when a new message comes in it won't jump your position to the bottom of the screen; that would be annoying. But if you're scrolled to the bottom, the program rightly assumes that you want to see the most recent messages at all times, and so auto-scrolls accordingly.

I have had a bear of a time trying to mimic this; the platform seems to fight this behavior at all costs. The best I can do is as follows:

In constructor:

JTextArea chatArea = new JTextArea();
JScrollPane chatAreaScrollPane = new JScrollPane(chatArea);

// We will manually handle advancing chat window
DefaultCaret caret = (DefaultCaret) chatArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

In method that handles new text coming in:

boolean atBottom = isViewAtBottom();

// Append the text using styles etc to the chatArea

if (atBottom) {
    scrollViewportToBottom();
} 


public boolean isAtBottom() {
    // Is the last line of text the last line of text visible?
    Adjustable sb = chatAreaScrollPane.getVerticalScrollBar();

    int val = sb.getValue();
    int lowest = val + sb.getVisibleAmount();
    int maxVal = sb.getMaximum();

    boolean atBottom = maxVal == lowest;
    return atBottom;
}


private void scrollToBottom() {
    chatArea.setCaretPosition(chatArea.getDocument().getLength());
}

Now, this works, but it's janky and not ideal for two reasons.

  1. By setting the caret position, whatever selection the user may have in the chat area is erased. I can imagine this would be very irritating if he's attempting to copy/paste.
  2. Since the advancement of the scroll pane occurs after the text is inserted, there is a split second where the scrollbar is in the wrong position, and then it visually jumps towards the end. This is not ideal.

Before you ask, yes I've read this blog post on Text Area Scrolling, but the default scroll to bottom behavior is not what I want.

Other related (but to my mind, not completely helpful in this regard) questions: Setting scroll bar on a jscrollpane Making a JScrollPane automatically scroll all the way down.

Any help in this regard would be very much appreciated.

Edit:

As per Devon_C_Miller's advice, I have an improved way of scrolling to the bottom, solving issue #1.

private void scrollToBottom() {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
       public void run() {
           try {
               int endPosition = chatArea.getDocument().getLength();
               Rectangle bottom = chatArea.modelToView(endPosition);
               chatArea.scrollRectToVisible(bottom);
           }
           catch (BadLocationException e) {
               System.err.println("Could not scroll to " + e);
           }
       }
    });
}

I still have problem #2.

+2  A: 

Take a look at the scrollRectToVisible(Rectangle r) and modelToView(int pos)

That should get you what you're looking for without disturbing the user's selection.

As for the scrollbar, try forcing the scroll and the append into occur on different events. For example:

if (atBottom) {
    // append new line & do scroll
    SwingUtilities.invokerLater(new Runnable(){
        public void run() {
            // append text
        }});
} else {
    // append newline
    // append text
}
Devon_C_Miller
I don't understand what you mean about the different events... I am scrolling as a separate event.
I82Much
The goal of splitting up the scroll and the append is to allow the GUI a chance to repaint between the scroll and the addition of the text. That allows the scrollbar move to the bottom before the text appears.
Devon_C_Miller
I am splitting up the scroll and append, but clearly the append has to occur before the scrolling, otherwise we won't be at the bottom of the view when the text is appended.
I82Much
Take a look at the comments in the code I posted above. Append a newline and scroll in one event, then add the text without a preceding newline in the invokeLater.
Devon_C_Miller
A: 

Check out this example.

Although, I would change the code to use the following which is more efficient:

caret.setDot(textArea.getDocument().getLength());
camickr
That example again discards the selection. Furthermore the scroll bar still jumps.
I82Much
I don't see the "scrollbar jumping". You can always change the code to NOT reset the caret position if a selection is found. Then the viewport won't scroll automatically, but I would think that is the behaviour you want. If a user is currently selecting text the scrolling should not be automatic.
camickr