1

NOTE: I am reasking this question because I need an answer to it, and the answer there has a broken link.

I am writing a simple chat program as part of a project for school, and I wanted the user to be able to automatically track messages as they come if they are at the bottom, but not be bothered by having to scroll up again if they are looking at something higher up. Currently, my code is just scroll to the bottom no matter what. Basic code is below, and I really don't know how to convert it to an SSCCE. If anyone can, please feel free to do so.

JTextArea jta = new JTextArea(50, 50);
JScrollPane scroll = new JScrollPane(jta);
try {
    int iter = 0;
    while (true) { //Not a for loop because it should not end after x iterations
        Thread.sleep(500);
        jta.append("Test text #" + iter);
        iter++;
        //Check if they are at the bottom and if so scroll down to the new bottom.
    }
} catch (InterruptedException ex) {
    System.out.println("Error!");
}
Community
  • 1
  • 1
Nic
  • 6,211
  • 10
  • 46
  • 69
  • It would be better to leave a comment under the answer with the broken link, asking of the answerer could fix the link/provide another example. – 11684 Feb 26 '13 at 15:25
  • If you are going to share your findings *later*, why did you post a question *now*? By the way, did you try to get the idea behind the posted code? – skuntsel Feb 26 '13 at 15:26
  • for better help sooner post an [SSCCE](http://sscce.org/), short, runnable, compilable, not code snipped caused locking Event Dispatch THread, more in Oracles tutorial Concurency in Swing – mKorbel Feb 26 '13 at 15:31
  • Ah, I just saw you indeed left a comment under the original answer. But I think the author of that answer deserves a bit more time than 17 minutes. There is no guarantee at all he/she has even been online since then, let alone found a new post with a solution. – 11684 Feb 26 '13 at 15:32
  • Sidenote: the creating of the `JTextArea` and `JScrollPane` must happen on the Event Dispatch Thread (EDT). Having a `while(true)` loop on the EDT will block this thread which will make sure you never see the text you append. Better to use a `javax.swing.Timer` for this code. It is also no longer documented that `append` is thread-safe, which makes the Swing `Timer` an even better choice – Robin Feb 26 '13 at 15:57
  • @11684 I did, but I figured that it would also be nice to find a solution that is more recent that August of 2010. – Nic Feb 26 '13 at 17:45
  • @skuntsel When I wrote this question, I simply did not have time to write up a full SSCCE, otherwise I would have. I would have placed a comment where I needed the scrolling to happen. – Nic Feb 26 '13 at 17:48
  • @11684 Oh, and I wanted to try to reach as many people as possible, and not just that one person who may not even use that account any more. – Nic Feb 26 '13 at 17:54
  • @Robin For this bit of code, it was all I could think of. My actual program has a much better way to deal with the problems you pointed out, but for this I thought it would be better to just post a simple bit of code so that people get what I mean. – Nic Feb 26 '13 at 17:57

3 Answers3

3

Not sure this is the nicest way to do it, but here is some code that stops scrolling down whenever the user scrolls up. When he scrolls back to the bottom, auto-scrolling starts again.

The idea is to check wheter the user has moved up or not by comparing the height of the textarea and the location and height of the visible rectangle of the scrollpane. If it matches, then it means that the scroll is at the bottom and the user wants autoamtic scrolling. In the other case, we then force the visible rectangle to remain the same everytime the textarea changes.

Small SSCCE:

import java.awt.BorderLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class TestScrollbars {

    protected void initUI() {
        final JFrame frame = new JFrame();
        frame.setTitle(TestScrollbars.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final JTextArea chat = new JTextArea(10, 40);
        chat.setLineWrap(true);
        chat.setEditable(false);
        chat.setWrapStyleWord(false);
        final JScrollPane scrollPane = new JScrollPane(chat);
        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        frame.setLayout(new BorderLayout());
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
        Timer t = new Timer(200, new ActionListener() {

            private int i = 1;

            @Override
            public void actionPerformed(ActionEvent e) {
                final Rectangle visibleRect = chat.getVisibleRect();
                boolean scroll = chat.getHeight() <= visibleRect.y + visibleRect.height;
                chat.append("Hello line " + i++ + "\n");
                if (!scroll) {
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            chat.scrollRectToVisible(visibleRect);
                        }
                    });
                }
            }
        });
        t.start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestScrollbars().initUI();
            }
        });
    }

}
Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
  • I actually have this implemented in a Thread, so the `Timer` is a bit redundant. However, this looks like what I want. Ill be right back with the test results... – Nic Feb 26 '13 at 17:50
  • Yep, this looks like exactly what I want! Thanks! I'm going to need to edit it a bit to make sure it work in my program, obviously, but other than that it's perfect. – Nic Feb 26 '13 at 17:51
  • @NickHartley The `Timer` is only there to simulate your loop/incoming data. Also, the Swing `Timer` executes everything on the EDT, avoiding the need to protected UI-calls (although in this case, it is not necessary, as `JTextArea.append()` is Thread-safe). – Guillaume Polet Feb 26 '13 at 18:01
  • I figured it was to simulate a loop, seeing as it repeats over and over. I didn't know, however, that the Timer operated on the EDT... That may come in handy in the future. Again, thanks for the SSCCE and the information. :) – Nic Feb 26 '13 at 18:12
  • No offense, but I think that people who come to this question in the future will want to see a premade class that does it all for them *and* provides an explanation, as was made by camickr below. I just wish I could accept two answers, because this one is great as well. – Nic Feb 27 '13 at 17:46
2

Edit:

I replaced the following code with a more flexible version that will work on any component in a JScrollPane. Check out: Smart Scrolling.

Here is some reusable code that can be used by any scrollpane containing a JTextArea or a JTextPane:

import java.awt.*;
import java.awt.event.*;
import java.util.Date;
import javax.swing.*;
import javax.swing.text.*;

public class ScrollControl implements AdjustmentListener
{
    private JScrollBar scrollBar;
    private JTextComponent textComponent;
    private int previousExtent = -1;

    public ScrollControl(JScrollPane scrollPane)
    {
        Component view = scrollPane.getViewport().getView();

        if (! (view instanceof JTextComponent))
            throw new IllegalArgumentException("Scrollpane must contain a JTextComponent");

        textComponent = (JTextComponent)view;

        scrollBar = scrollPane.getVerticalScrollBar();
        scrollBar.addAdjustmentListener( this );
    }

    @Override
    public void adjustmentValueChanged(final AdjustmentEvent e)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                checkScrollBar(e);
            }
        });
    }

    private void checkScrollBar(AdjustmentEvent e)
    {
        //  The scroll bar model contains information needed to determine the
        //  caret update policy.

        JScrollBar scrollBar = (JScrollBar)e.getSource();
        BoundedRangeModel model = scrollBar.getModel();
        int value = model.getValue();
        int extent = model.getExtent();
        int maximum = model.getMaximum();
        DefaultCaret caret = (DefaultCaret)textComponent.getCaret();

        //  When the size of the viewport changes there is no need to change the
        //  caret update policy.

        if (previousExtent != extent)
        {
            //  When the height of a scrollpane is decreased the scrollbar is
            //  moved up from the bottom for some reason. Reposition the
            //  scrollbar at the bottom

            if (extent < previousExtent
            &&  caret.getUpdatePolicy() == DefaultCaret.UPDATE_WHEN_ON_EDT)
            {
                scrollBar.setValue( maximum );
            }

            previousExtent = extent;
            return;
        }

        //  Text components will not scroll to the bottom of a scroll pane when
        //  a bottom inset is used. Therefore the location of the scrollbar,
        //  the height of the viewport, and the bottom inset value must be
        //  considered when determining if the scrollbar is at the bottom.

        int bottom = textComponent.getInsets().bottom;

        if (value + extent + bottom < maximum)
        {
            if (caret.getUpdatePolicy() != DefaultCaret.NEVER_UPDATE)
                caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
        }
        else
        {
            if (caret.getUpdatePolicy() != DefaultCaret.UPDATE_WHEN_ON_EDT)
            {
                caret.setDot(textComponent.getDocument().getLength());
                caret.setUpdatePolicy(DefaultCaret.UPDATE_WHEN_ON_EDT);
            }
        }
    }

    private static void createAndShowUI()
    {
        JPanel center = new JPanel( new GridLayout(1, 2) );
        String text = "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n";

        final JTextArea textArea = new JTextArea();
        textArea.setText( text );
        textArea.setEditable( false );
        center.add( createScrollPane( textArea ) );
        System.out.println(textArea.getInsets());

        final JTextPane textPane = new JTextPane();
        textPane.setText( text );
        textPane.setEditable( false );
        center.add( createScrollPane( textPane )  );
        textPane.setMargin( new Insets(5, 3, 7, 3) );
        System.out.println(textPane.getInsets());

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(center, BorderLayout.CENTER);
        frame.setSize(500, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        Timer timer = new Timer(2000, new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                try
                {
                    Date now = new Date();
                    textArea.getDocument().insertString(textArea.getDocument().getLength(), "\n" + now.toString(), null);
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), "\n" + now.toString(), null);
                }
                catch (BadLocationException e1) {}
            }
        });
        timer.start();
    }

    private static JComponent createScrollPane(JComponent component)
    {
        JScrollPane scrollPane = new JScrollPane(component);
        new ScrollControl( scrollPane );

        return scrollPane;
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowUI();
            }
        });
    }
}

Edit:

Updated the code so that it will also work if the height of the scrollpane is decreased. For some reason the default behaviour is to move the scrollbar up one row which means the scrollbar will no longer remain at the bottom when new text is added.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • Great work making this, but how would I use it? Would I just create an object of this class and the class does everything else? Or would I have to do something else for it to work? – Nic Feb 27 '13 at 14:47
  • This seems like another great solution to my problem, though, and if I can figure it out I may accept this one instead. It's just a shame I can't accept both... – Nic Feb 27 '13 at 14:49
  • Once you create the JTextArea and add it to a JScrollPane, its one line of code to use it: **new ScrollControl( scrollPane );** All the Timer code was just to prove that the class works. In your application you just add text to the text area as you currently do and the scrolling will happen automatically. – camickr Feb 27 '13 at 16:23
  • Also, you may want to check back on Monday. I plan on releasing a more general version of this class that should work on any scrollpane. That is the scrollpane could contian JTextArea, JTextPane, JList, JTable... – camickr Feb 27 '13 at 16:28
  • Wow. That's pretty awesome, making a class that only needs a single line of code AND works with almost everything that can be scrolled... By the way, you can use the "`" mark to show code, like it says in the help thing. It just makes it easier to tell that it's code, and not an emphasized normal statement. – Nic Feb 27 '13 at 17:45
  • One more thing: When you say `The timer code was just to prove the class works`, you mean that I can delete `createAndShowUI()`, `createScrollPane(JComponent component)` and `main(String[] args)`? – Nic Feb 27 '13 at 18:02
  • Hmmmmm... When I added this to my code, it acted like there was nothing. It always stayed put. I'll double check for remnants of my old system, but is there a way I should be adding text to my JTextArea? Should I use `append()` (as I am now) or something else? – Nic Feb 27 '13 at 18:22
  • Using 'append()' is fine. The code was tested on Windows 7 using JDK7. Does my sample code work for you? If the sample works, then I don't see why it shouldn't work in your program. – camickr Feb 27 '13 at 18:38
  • It should be working fine, then... I'll do another check through my code to make sure that I'm not messing anything up by accident. Though the computer I'm using to test this is JDK6 with Windows XP seeing as it is a school computer. I'll do another test at my home computer, but your example works fine. Again, I'll do a double-check that no relic code is hanging around, then I'll get back. – Nic Feb 28 '13 at 14:35
  • Oh, and you may want to add some credit for yourself - If you want, I can edit in the javadoc comment I put in to remind myself who exactly made this for me and how to use it. – Nic Feb 28 '13 at 14:37
  • OK, I figured out a way to make it work for me, but it may not be thread-safe. I just changed all of the `DefaultCaret.UPDATE_WHILE_ON_EDT` to `DefaultCaret.ALWAYS_UPDATE` when it was setting the update policy. Now it works perfectly. – Nic Feb 28 '13 at 14:52
  • No that is the wrong solution if you need to do that. All updates to GUI components should be done on the EDT. If it doesn't work for you then means you are invoking the append() method incorrectly. You need to wrap the append() method in a SwingUtilities.invokeLater() if your current code is not executing on the EDT. – camickr Feb 28 '13 at 17:00
  • Ah, I forgot about the need to use `SwingUtilities.invokeLater()`... I'll add that and put back `DefaultCaret.UPADTE_WHILE_ON_EDT`. – Nic Feb 28 '13 at 17:49
  • @NickHartley, new version has been release if you are interested. See the edit at the top of the posting. – camickr Mar 03 '13 at 20:09
  • Thanks! For now I personally don't need it, but great job making that. Two small suggestions, though: Make it possible to monitor both horizontal and vertical (Maybe with SmartScroller.BOTH?) and mention that you need to make sure everything that changes the scroll happens on the EDT. The second may seem self-explanatory or obvious, but I didn't know until you told me that I had to use `SwingUtilities.invokeLater()` around my `append()` call. – Nic Mar 04 '13 at 18:03
  • @NickHartley, if you want both you need to create two SmartScroller's because each keep some internal state properties. Changes should always be made on the EDT for good Swing programming practices, however scrolling in this implementation isn't dependent on this requirement because it doesn't use a caret update policy. – camickr Mar 04 '13 at 19:12
  • Ah, I see. And I didn't look through the code, so I didn't realize that it didn't have the caret update policy like the previous version, although I guess that should have been obvious. And while it's a good programming practice, people have bad habits and you should at least make a not if it's ever needed. – Nic Mar 05 '13 at 21:34
0

The old Apple MacOS way was the possibility to have (several) automatic split panes which could be torn upwards from below, and again dropped.

They would create several views (text boxes) on the same StyledDocument, of which only the bottom one would scroll automatically.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • I am supposed to make this for all platforms, not just any specific one, which is the reason I chose to use Java. – Nic Feb 26 '13 at 17:47