Fran Fitzpatrick
Fran Fitzpatrick

Reputation: 18512

How to make JTextPane autoscroll only when scroll bar is at bottom and scroll lock is off?

How to make JTextPane autoscroll only when scroll bar is at bottom and scroll lock is off? This shouldn't have anything to do with caret, which is what I seem to be finding all over Google. :(

Upvotes: 14

Views: 12133

Answers (6)

corochann
corochann

Reputation: 1624

After I read Mike Clark and random dude's solution, I end up with below snippet code.

private boolean doAutoScroll = true;
private JTextPane textPane;
private JScrollPane scrollPane;

public void setup() {
    /* Left Panel */
    textPane = new JTextPane();
    textPane.setPreferredSize(new Dimension(600, 400)); // width, height

    /*
     * Not update the cursor position after inserting or appending text to the JTextPane.
     * [NOTE]
     * This breaks normal typing into the JTextPane.
     * This approach assumes that all updates to the JTextPane are programmatic.
     */
    DefaultCaret caret = (DefaultCaret) textPane.getCaret();
    caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

    scrollPane = new JScrollPane(textPane);
    scrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {
        BoundedRangeModel brm = scrollPane.getVerticalScrollBar().getModel();
        @Override
        public void adjustmentValueChanged(AdjustmentEvent e) {
            // Invoked when user select and move the cursor of scroll by mouse explicitly.
            if (!brm.getValueIsAdjusting()) {
                if (doAutoScroll) brm.setValue(brm. getMaximum());
            } else {
                // doAutoScroll will be set to true when user reaches at the bottom of document.
                doAutoScroll = ((brm.getValue() + brm.getExtent()) == brm.getMaximum());
            }
        }
    });

    scrollPane.addMouseWheelListener(new MouseWheelListener() {
        BoundedRangeModel brm = scrollPane.getVerticalScrollBar().getModel();
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            // Invoked when user use mouse wheel to scroll
            if (e.getWheelRotation() < 0) {
                // If user trying to scroll up, doAutoScroll should be false.
                doAutoScroll = false;
            } else {
                // doAutoScroll will be set to true when user reaches at the bottom of document.
                doAutoScroll = ((brm.getValue() + brm.getExtent()) == brm.getMaximum());
            }
        }
    });
}

The difference is that it is additionally using MouseWheelListener to update doAutoScroll flag even if user uses mouse wheel to scroll up and down.

Upvotes: 0

Tim Autin
Tim Autin

Reputation: 6165

I needed to do the same for a logging text area. The solutions I found on the web did not worked for me (they either stop auto scrolling when logging to much messages quickly, or they blocked the scrollbar at bottom even if you scroll up whith your mouse wheel).

I did it this way :

public static void makeTextAreaAutoScroll(JTextArea textArea) {

    // Get the text area's scroll pane :
    final JScrollPane scrollPane = (JScrollPane) (textArea.getParent().getParent());

    // Disable the auto scroll :
    ((DefaultCaret)textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

    // Add a listener to the vertical scroll bar :
    scrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {

        private int _val = 0;
        private int _ext = 0;
        private int _max = 0;

        private final BoundedRangeModel _model = scrollPane.getVerticalScrollBar().getModel();

        @Override
        public void adjustmentValueChanged(AdjustmentEvent e) {

            // Get the new max :
            int newMax = _model.getMaximum();

            // If the new max has changed and if we were scrolled to bottom :
            if (newMax != _max && (_val + _ext == _max) ) {

                // Scroll to bottom :
                _model.setValue(_model.getMaximum() - _model.getExtent());
            }

            // Save the new values :
            _val = _model.getValue();
            _ext = _model.getExtent();
            _max = _model.getMaximum();
        }
    });
}

Just use it this way :

makeTextAreaAutoScroll(yourTextArea);

You can test with this piece of code :

new Timer().schedule(new TimerTask() {

    @Override
    public void run() {

        javax.swing.SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {

                String line = "test " + Math.random();
                yourTextArea.append(yourTextArea.getText().isEmpty() ? line : "\n" + line);
            }
        });
    }
}, 0, 5);

Now your text area should auto scroll if the scroll bar is at bottom, stop auto scrolling if you move the scroll bar (by dragging the bar or by using the wheel), and auto scroll again if you put the scroll bar at bottom again.

Upvotes: 2

BilalDja
BilalDja

Reputation: 1092

Try this :

JTextArea txt = new JTextArea();
JScrollPane jsp = new JScrollPane(history, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
txt.setCaretPosition(txt.getDocument().getLength()); // do this afeter any event

Hope that helps you

Upvotes: 0

Mike Clark
Mike Clark

Reputation: 10136

I think my program below meets your requirements exactly, with one possible caveat: you're not allowed to type in the text area. So this would be good for a log viewer, but not an interactive console. The code runs a little long because I have made it into a ready-to-run demo of the approach. I suggest running the program as-is and checking out the behavior. If the behavior works well for you, then invest a little time in studying the code. I have included comments in the code to highlight some of the more important sections.


Update 2013-07-17: You may also want to check out random dude's solution in his separate answer farther down the page. His approach is more elegant than mine.

Also see Swing: Scroll to bottom of JScrollPane, conditional on current viewport location for a potential solution that does not interfere with the caret position.


SCCE source code follows:

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.Timer;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class ScrollingJTextAreaExample extends JFrame {
    // Worker thread to help periodically append example messages to JTextArea
    Timer timer = new Timer();
    // Merely informative counter, will be displayed with the example messages
    int messageCounter = 0;
    // GUI components
    JScrollPane jScrollPane;
    JTextArea jTextArea;

    public ScrollingJTextAreaExample() {
        initComponents(); // Boiler plate GUI construction and layout

        // Configure JTextArea to not update the cursor position after
        // inserting or appending text to the JTextArea. This disables the
        // JTextArea's usual behavior of scrolling automatically whenever
        // inserting or appending text into the JTextArea: we want scrolling
        // to only occur at our discretion, not blindly. NOTE that this
        // breaks normal typing into the JTextArea. This approach assumes
        // that all updates to the ScrollingJTextArea are programmatic.
        DefaultCaret caret = (DefaultCaret) jTextArea.getCaret();
        caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

        // Schedule a task to periodically append example messages to jTextArea
        timer.schedule(new TextGeneratorTask(), 250, 250);

        // This DocumentListener takes care of re-scrolling when appropriate
        Document document = jTextArea.getDocument();
        document.addDocumentListener(new ScrollingDocumentListener());
    }

    // Boring, vanilla GUI construction and layout code
    private void initComponents() {
        jScrollPane = new javax.swing.JScrollPane();
        jTextArea = new javax.swing.JTextArea();
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jScrollPane.setViewportView(jTextArea);
        getContentPane().add(jScrollPane, java.awt.BorderLayout.CENTER);
        setSize(320, 240);
        setLocationRelativeTo(null);
    }

    // ScrollingDocumentListener takes care of re-scrolling when appropriate
    class ScrollingDocumentListener implements DocumentListener {
        public void changedUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        public void insertUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        public void removeUpdate(DocumentEvent e) {
            maybeScrollToBottom();
        }

        private void maybeScrollToBottom() {
            JScrollBar scrollBar = jScrollPane.getVerticalScrollBar();
            boolean scrollBarAtBottom = isScrollBarFullyExtended(scrollBar);
            boolean scrollLock = Toolkit.getDefaultToolkit()
                    .getLockingKeyState(KeyEvent.VK_SCROLL_LOCK);
            if (scrollBarAtBottom && !scrollLock) {
                // Push the call to "scrollToBottom" back TWO PLACES on the
                // AWT-EDT queue so that it runs *after* Swing has had an
                // opportunity to "react" to the appending of new text:
                // this ensures that we "scrollToBottom" only after a new
                // bottom has been recalculated during the natural
                // revalidation of the GUI that occurs after having
                // appending new text to the JTextArea.
                EventQueue.invokeLater(new Runnable() {
                    public void run() {
                        EventQueue.invokeLater(new Runnable() {
                            public void run() {
                                scrollToBottom(jTextArea);
                            }
                        });
                    }
                });
            }
        }
    }

    class TextGeneratorTask extends TimerTask {
        public void run() {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    String message = (++messageCounter)
                            + " Lorem ipsum dolor sit amet, consectetur"
                            + " adipisicing elit, sed do eiusmod tempor"
                            + " incididunt ut labore et dolore magna aliqua.\n";
                    jTextArea.append(message);
                }
            });
        }
    }

    public static boolean isScrollBarFullyExtended(JScrollBar vScrollBar) {
        BoundedRangeModel model = vScrollBar.getModel();
        return (model.getExtent() + model.getValue()) == model.getMaximum();
    }

    public static void scrollToBottom(JComponent component) {
        Rectangle visibleRect = component.getVisibleRect();
        visibleRect.y = component.getHeight() - visibleRect.height;
        component.scrollRectToVisible(visibleRect);
    }

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new ScrollingJTextAreaExample().setVisible(true);
            }
        });
    }
}

Upvotes: 14

random dude
random dude

Reputation: 475

Little late to this question, but I came up with this solution.

  conversationPane = new JTextPane();
  final JScrollPane conversationScrollPane = new JScrollPane(conversationPane);
  conversationScrollPane.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() {

     BoundedRangeModel brm = conversationScrollPane.getVerticalScrollBar().getModel();
     boolean wasAtBottom = true;

     public void adjustmentValueChanged(AdjustmentEvent e) {
        if (!brm.getValueIsAdjusting()) {
           if (wasAtBottom)
              brm.setValue(brm.getMaximum());
        } else
           wasAtBottom = ((brm.getValue() + brm.getExtent()) == brm.getMaximum());

     }
  });   

Seems to work perfectly for my needs. Little explanation: Essentially if the scroll bar is not being moved by a person and the bar was last at the maximum/bottom then reset it to the maximum. If it's being manually adjusted, then check to see if it was adjusted to be at the bottom.

Upvotes: 13

camickr
camickr

Reputation: 324197

Text Area Scrolling may be of interest.

I have no idea how the scroll lock key affects it. I found the following from the Wikipedia page on Scroll Lock:

Therefore, Scroll Lock can be regarded as a defunct feature in almost all modern programs and operating systems.

So I wouldn't worry about it.

Upvotes: 3

Related Questions