3

I'm currently writing a simple chat in Java and currently I'm stuck on this problem: I want my output JTextPane to behave like you would expect it to from a good chat, ie by default the text scrolls automatically when new text arrives (using outputfield.setCaretPosition(outputdocument.getLength())), but when the user scrolls up this should be disabled and of course re-enabled when the user scrolls to the bottom again.

I tried toying around with the ScrollBars and all, but I can't seem to find a way to detect whether the ScrollBar is at the bottom or not.

I create the scrollable output area simply by creating a JTextPane, a JScrollPane and put one into the other.

JTextPane outputpane = new JTextPane
...
JScrollPane outputscroll = new JScrollPane(outputpane);
outputscroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

Edit: Here's the code I use to create the components and display new messages:

// lots of imports


public class RoomFrame extends JFrame implements ListSelectionListener, ActionListener, Comparable, Runnable
{   
public static final String DEFAULT_FONT = "Courier";
public static final int DEFAULT_FONT_SIZE = 12;

private JTextPane mOutputField;
private JScrollPane mOutputScroll;

private StyledDocument mOutputDocument;


public RoomFrame(...)
{
    super(...);

    createGUI();
    setOutputStyles();
    setVisible(true);

    new Thread(this).start();
}

// ========================================================

private void createGUI()
{
    Color borderhighlightouter = new Color(220, 220, 220);
    Color borderhighlightinner = new Color(170, 170, 170);
    Color bordershadowouter = new Color(120, 120, 120);
    Color bordershadowinner = new Color(170, 170, 170);

    setLayout(new GridBagLayout());
    setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    setSize(500, 400);


    // -----------------------------
    // Output
    mOutputField = new JTextPane();
    mOutputField.setEditable(false);
    mOutputField.setBackground(new Color(245, 245, 245));
    mOutputDocument = mOutputField.getStyledDocument();

    mOutputScroll = new JScrollPane(mOutputField);

    mOutputScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    mOutputScroll.setPreferredSize(new Dimension(250, 250));
    mOutputScroll.setMinimumSize(new Dimension(50, 50));
    mOutputScroll.setOpaque(false);
    mOutputScroll.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, 0, 1, 0), BorderFactory.createBevelBorder(BevelBorder.LOWERED, borderhighlightouter, borderhighlightinner, bordershadowouter, bordershadowinner)));

    getContentPane().add(mOutputScroll);
}

private void setOutputStyles()
{
    Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);

    Style regular = mOutputDocument.addStyle("regular", def);
    StyleConstants.setFontFamily(def, DEFAULT_FONT);
    StyleConstants.setFontSize(def, DEFAULT_FONT_SIZE);
    StyleConstants.setFirstLineIndent(def, 100.0f);

    Style nickname = mOutputDocument.addStyle("username", def);
    StyleConstants.setBold(nickname, true);
    StyleConstants.setForeground(nickname, new Color(50, 50, 220));

    Style highlight = mOutputDocument.addStyle("highlight", def);
    StyleConstants.setBold(highlight, true);
    StyleConstants.setBackground(highlight, new Color(150, 0, 0));

    Style join = mOutputDocument.addStyle("join", def);
    StyleConstants.setBold(join, true);
    StyleConstants.setForeground(join, new Color(20, 100, 20));

    Style leave = mOutputDocument.addStyle("leave", def);
    StyleConstants.setBold(leave, true);
    StyleConstants.setForeground(leave, new Color(100, 100, 20));

    Style topic = mOutputDocument.addStyle("topic", def);
    StyleConstants.setBold(topic, true);
    StyleConstants.setUnderline(topic, true);

    Style error = mOutputDocument.addStyle("error", def);
    StyleConstants.setBold(error, true);
    StyleConstants.setForeground(error, new Color(255, 0, 0));

    Style kick = mOutputDocument.addStyle("kick", def);
    StyleConstants.setBold(kick, true);
    StyleConstants.setForeground(kick, new Color(150, 0, 0));
}

private final boolean shouldScroll()
{
    int min = mOutputScroll.getVerticalScrollBar().getValue() + mOutputScroll.getVerticalScrollBar().getVisibleAmount();
    int max = mOutputScroll.getVerticalScrollBar().getMaximum();

    return min == max;
}

// ========================================================

public void displayMessage(String message)
{
    displayMessage(message, "");
}

public void displayMessage(String message, String style)
{
    displayMessage(message, style, true);
}

public void displayMessage(String message, String style, boolean addnewline)
{
    String newline = (addnewline ? "\n" : "");

    style = (style.equals("") ? "regular" : style);
    message = message.replace("\n", " ");

    // check for highlight

    try
    {
        mOutputDocument.insertString(mOutputDocument.getLength(),
                                        String.format("%s%s", message, newline),
                                        mOutputDocument.getStyle(style));
    }
    catch (Exception e) {}

    // if (shouldScroll())
    //  mOutputField.setCaretPosition(mOutputDocument.getLength());
}

public void run()
{
    while (true)
    {
        if (shouldScroll())
        {
            SwingUtilities.invokeLater(
                new Runnable()
                {
                    public void run()
                    {
                        mOutputScroll.getVerticalScrollBar().setValue(mOutputScroll.getVerticalScrollBar().getMaximum());
                    }
                });
        }

        try { Thread.sleep(500); }
        catch (InterruptedException e) { break; }
    }
}

public void valueChanged(ListSelectionEvent event)
{

}

public void actionPerformed(ActionEvent event)
{

}

public final int compareTo(Object o) { return this.toString().compareTo(o.toString()); }
}


Edit: Thanks to fireshadow52's link to another similar question I finally got it to work exactly how I want it to:

private boolean isViewAtBottom()
{
    JScrollBar sb = mOutputScroll.getVerticalScrollBar();
    int min = sb.getValue() + sb.getVisibleAmount();
    int max = sb.getMaximum();
    System.out.println(min + " " + max);
    return min == max;
}

private void scrollToBottom()
{
    SwingUtilities.invokeLater(
        new Runnable()
        {
            public void run()
            {
                mOutputScroll.getVerticalScrollBar().setValue(mOutputScroll.getVerticalScrollBar().getMaximum());
            }
        });
}

public void displayMessage(String message, String style, boolean prependnewline)
{
    String newline = (prependnewline ? "\n" : "");
    boolean scroll = isViewAtBottom() && prependnewline;

    style = (style.equals("") ? "regular" : style);
    message = message.replace("\n", " ");

    try
    {
        mOutputDocument.insertString(mOutputDocument.getLength(),
                                        String.format("%s%s", newline, message),
                                        mOutputDocument.getStyle(style));
    }
    catch (Exception e) {}

    if (scroll)
        scrollToBottom();
}

Again, thanks for all your help!

Community
  • 1
  • 1
Szernex
  • 165
  • 2
  • 12
  • are you meaning automatically scroll_to_bottom ??? – mKorbel Jan 09 '12 at 13:56
  • 1
    as you mentioned, setCaretPosition(Component.getDocument().getLength()); would do the job. Simply call this everytime there is new data coming in. Otherwise first i tried to use this techique: http://stackoverflow.com/questions/8445413/detecting-mouseclick-event-on-jscrollpane-in-java-swing, it was fine, but not as good as a simple line of text you already mentioned above – Johnydep Jan 09 '12 at 14:05
  • I mean that the text should only scroll when the ScrollBar is at the bottom. If you just use setCaretPosition(Component.getDocument().getLength()); it will always scroll to the bottom even if the user scrolled up – Szernex Jan 09 '12 at 14:10
  • Have a look at [this question](http://stackoverflow.com/questions/2670124/swing-scroll-to-bottom-of-jscrollpane-conditional-on-current-viewport-location). – fireshadow52 Jan 09 '12 at 17:08
  • Omg thank you! That finally works! – Szernex Jan 09 '12 at 18:54

3 Answers3

2

If you're only wanting to scroll when you're at the bottom then this should help.

Use this method to check and see if the scrollbar is at the end (or bottom), and if so, scroll down automatically using setCaretPosition(Component.getDocument().getLength());:

public boolean shouldScroll() {
    int minimumValue = scrollPane.getVerticalScrollBar().getValue() + scrollPane.getVerticalScrollBar().getVisibleAmount();
    int maximumValue = scrollPane.getVerticalScrollBar().getMaximum();
    return maximumValue == minimumValue;
}

I found similar results when using Google which led me to a method similar to this one which worked as requested.

Edit: Make sure that it is done within invokeLater() as it needs to be updated before scrolling is done.

A full example of what I use:

public class ScrollTest extends Thread {

    private JFrame frame;

    private JTextArea textArea;

    private JScrollPane scrollPane;

    public static void main(String[] arguments) {
            new ScrollTest().run();
    }

    public ScrollTest() {
            textArea = new JTextArea(20, 20);
            scrollPane = new JScrollPane(textArea);

            frame = new JFrame("Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(scrollPane);
            frame.pack();
            frame.setVisible(true);
    }

    public void run() {
            while (true) {
                    textArea.append("" + Math.random() + "\n");
                    if (shouldScroll()) {
                            SwingUtilities.invokeLater(new Runnable() {

                                    @Override
                                    public void run() {
                                            scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
                                    }

                            });
                    }
                    try {
                            Thread.sleep(500);
                    } catch (Exception exception) {
                            exception.printStackTrace();
                    }
            }
    }

    public boolean shouldScroll() {
            int minimumValue = scrollPane.getVerticalScrollBar().getValue() + scrollPane.getVerticalScrollBar().getVisibleAmount();
            int maximumValue = scrollPane.getVerticalScrollBar().getMaximum();
            return maximumValue == minimumValue;
    }

}

  • This doesn't seem to work either. If I use it like `if (shouldScroll()) mOutputField.setCaretPosition(mOutputDocument.getLength());` it won't scroll at all. Do I have to manually set the maximum value of the scroll bar? Edit: Uhm, where should I use invokeLater()? I'm still new to swing. – Szernex Jan 09 '12 at 14:34
  • I have updated my post to include the example I worked with, which works fine. –  Jan 09 '12 at 14:42
  • Now it won't display any text. I guess that's probably because .) I use a JTextPane and .) a method displayMessage(...) which uses `mOutputDocument.insertString(mOutputDocument.getLength(), String.format("%s%s", message, newline), mOutputDocument.getStyle(style));` to display the new text? Edit: Okay, the problem was because it wasn't running in a thread. Now it displays the text but still doesn't scroll like it should... – Szernex Jan 09 '12 at 14:53
  • I would assume that it is because you used JTextPane. I replaced `textArea.append(...)` with `textArea.getDocument().insertString(textArea.getDocument().getLength(), "" + Math.random() + "\n", null);` and it is still working fine. Do you mind posting _exactly_ what you are using? –  Jan 09 '12 at 14:58
  • I edited the post. It contains only the things related to the output of new text. – Szernex Jan 09 '12 at 15:11
  • I must be completely lost because it is scrolling exactly how mine does. Let me see if I understand this properly. If you are scrolling up (to read some text, let's say) it should not scroll you down automatically. If you are scrolled down to the bottom it should automatically scroll you down. Is that right? –  Jan 09 '12 at 15:23
  • Exactly that's the behaviour I want. I just can't get it to work with neither of the answers here :( – Szernex Jan 09 '12 at 15:31
  • I'm not sure why... I used [this example](http://pastebin.com/raw.php?i=SEzRzk0s) exactly and it works just as you want. My only guess is that you have to send the messages within the thread, have you tried that? –  Jan 09 '12 at 15:38
  • Yeah, when I call displayMessage() just like you do in your example it works as I want it to. Now I only have to get it to work if displayMessage() is called from outside the thread... – Szernex Jan 09 '12 at 15:46
  • After futher testing I've found that it sets even if it should not scroll, but it is not being documented that it is scrolling, it is just automatic. I'll keep looking but hopefully someone with more experience will be able to find the problem. –  Jan 09 '12 at 16:10
  • Yeah, thanks, I really appreciate it. I'll keep on trying and looking myself, but still you were a great help. – Szernex Jan 09 '12 at 16:43
1

JScrollBar has getValue() method. Just warp it in SwingUtilities.invokeLater() to be called after text appending.

StanislavL
  • 56,971
  • 9
  • 68
  • 98
1

You can use an AdjustmentListener to condtion scrolling, as shown in this example.

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