2

My question is very similar to this one:

Set the vertical scroll to current position after revalidate

The difference is that I do not want to scroll back to 0 but to the position I saved from before updating the child components as follows:

final int oldPos = scrollPanel.getVerticalScrollBar().getValue();
removeAll(); // Removes all components from the JPanel
add(mList()); // Adds a bunch of content
revalidate();
SwingUtilities.invokeLater(new Runnable()
{
    @Override
    public void run()
    {
        ml.getVerticalScrollBar().setValue(oldPos);
    }
});

This code gets triggered in a mouse motion listener of another componenent so it is called quite frequently.

Even if initially the jscrollpane is scrolled to the top sometimes the oldPos is not zero which results in the pane scrolling down despite setting the value.

Even if I force scroll to the top and just log the oldPos value, it is not always 0:

final int oldPos = scrollPanel.getVerticalScrollBar().getValue();
removeAll();
add(mList());
revalidate();
SwingUtilities.invokeLater(new Runnable()
{
    @Override
    public void run()
    {
        System.out.println(oldPos); // Sometimes this is > 2000
        ml.getVerticalScrollBar().setValue(0); // scroll to the top
    }
});

I guess this happens if the oldPos = ... code is executed before the invokeLater block of the previous execution is invoked. How can I fix this?

Update:

I created an sscce as suggested by @mKorbel:

Update 2: I updated the sscce to allow switching between jTextArea/jLabel, enabling/disabling the rebinding of the listener and switching between jTextArea and a custom class inheriting from jTextArea overriding the scrollRectToVisible method.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;

public class Main {
  static class MyTextArea extends JTextArea {
    @Override
    public void scrollRectToVisible(final Rectangle aRect) {
      // supress scrollToRect in textarea
    }
  }

  static final Box inner = Box.createVerticalBox();

  // if the jtextarea should contain text
  private static boolean textEnabled = true;
  private static boolean usejLabel = false;
  private static boolean useRebinding = false;
  private static boolean useLock = true;
  private static boolean customTextarea = false;

  private static boolean _locked = false;

  public static void main(final String[] args) {
    final JFrame frame = new JFrame();

    final JPanel insideScroll = insideScroll();
    final JScrollPane scrollpane = new JScrollPane(insideScroll);
    scrollpane.setAutoscrolls(false);
    scrollpane
        .setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    final JPanel rightPanel = new JPanel();
    rightPanel.setPreferredSize(new Dimension(300, 300));

    final MouseMotionAdapter listener = new MouseMotionAdapter() {

      @Override
      public void mouseDragged(final MouseEvent e) {
        super.mouseDragged(e);
        final boolean rebind = useRebinding;

        final MouseMotionAdapter self = this;
        if (rebind) {
          rightPanel.removeMouseMotionListener(self);
        }
        final int pos = scrollpane.getVerticalScrollBar().getValue();
        if (!useLock || !_locked) {
          if (useLock) {
            _locked = true;
          }
          update();
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              if (rebind) {
                rightPanel.addMouseMotionListener(self);
              }
              if (useLock) {
                _locked = false;
              }
            }
          });
        }

      }
    };
    rightPanel.addMouseMotionListener(listener);

    // Add labels describing the problem
    rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.PAGE_AXIS));
    final JButton toggleButton = new JButton("Toggle text");
    final JButton toggleButtonLabel = new JButton("Toggle JLable/JTextArea");
    final JButton toggleButtonRebind = new JButton("Turn rebinding on");
    final JButton toggleButtonCustomTextArea = new JButton(
        "Toggle Custom Textarea");
    rightPanel.add(toggleButton);
    rightPanel.add(toggleButtonLabel);
    rightPanel.add(toggleButtonRebind);
    rightPanel.add(toggleButtonCustomTextArea);
    rightPanel
        .add(new JLabel(
            "<html>If the text is disabled, you can press/drag<br> your mouse on on right side of the<br> window and the scrollbar will<br> stay in its position."));

    rightPanel
        .add(new JLabel(
            "<html><br/>If the text is enabled, the scrollbar<br> will jump around when dragging<br> on the right side."));

    rightPanel
        .add(new JLabel(
            "<html><br/>The problem does not occur when using JLabels instead of JTextArea"));

    // enable/disable the text when the button is clicked.
    toggleButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent e) {
        textEnabled = !textEnabled;
        update();
      }
    });

    toggleButtonLabel.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent e) {
        usejLabel = !usejLabel;
        update();
      }
    });
    toggleButtonRebind.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent e) {
        useRebinding = !useRebinding;
        toggleButtonRebind.setText(useRebinding ? "Turn rebinding off"
            : "Turn rebinding on");
        update();
      }
    });
    toggleButtonCustomTextArea.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent e) {
        customTextarea = !customTextarea;
        update();
      }
    });

    final JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
        scrollpane, rightPanel);

    frame.add(split);

    // initialize the scrollpane content
    update();

    frame.pack();
    frame.setVisible(true);

    frame.setLocationRelativeTo(null);

  }

  // initializes the components inside the scrollpane
  private static JPanel insideScroll() {
    final JPanel panel = new JPanel();
    panel.setLayout(new BorderLayout());
    panel.add(inner, BorderLayout.NORTH);

    return panel;
  }

  // replaces all components inside the scrollpane
  private static void update() {
    inner.removeAll();

    for (int i = 0; i < 30; i++) {
      inner.add(buildRow(i));
    }

    inner.revalidate();
  }

  // build a single component to be inserted into the scrollpane
  private static JPanel buildRow(final int i) {
    final JPanel row = new JPanel();

    final Color bg = i % 2 == 0 ? Color.DARK_GRAY : Color.LIGHT_GRAY;

    row.setBackground(bg);
    row.setPreferredSize(new Dimension(300, 80));
    row.setLayout(new BorderLayout());

    row.add(textarea(bg), BorderLayout.CENTER);

    return row;
  }

  // build the textarea to be inserted into the cells in the scroll pane
  private static Component textarea(final Color bg) {
    final String text = String.format("%d", (int) (1000 * Math.random()))
        + " Lorem ipsum dolor si amet. Lorem ipsum dolor si amet. Lorem ipsum dolor si amet";
    if (usejLabel) {
      final JLabel textarea = new JLabel();

      textarea.setBackground(bg);
      if (textEnabled) {
        textarea.setText(text);
      }

      return textarea;
    } else {
      final JTextArea textarea;
      if (customTextarea) {
        textarea = new MyTextArea();
        textarea.setDisabledTextColor(Color.cyan);

      } else {
        textarea = new JTextArea();
        textarea.setDisabledTextColor(Color.black);

      }

      textarea.setEnabled(false);
      textarea.setLineWrap(true);
      textarea.setBackground(bg);
      textarea.setEditable(false);
      if (textEnabled) {
        textarea.setText(text);
      }

      return textarea;
    }

  }
}

By doing so I realised that the problem only occurs if the jscroll contains jTextarea elements which have their text set to a string. In my real application I need the textareas for the automatic line wrap but even with disabled line wrap the problem occurs.

Community
  • 1
  • 1
Laszlo Korte
  • 1,179
  • 8
  • 17
  • How can I fix this? == for better help sooner post an SSCCE/ MVCE, short, runnable, compilable – mKorbel Jul 12 '15 at 22:06
  • Consider using [`scrollRectToVisible`](http://docs.oracle.com/javase/7/docs/api/javax/swing/JComponent.html#scrollRectToVisible(java.awt.Rectangle)) on the component you want to scroll – MadProgrammer Jul 12 '15 at 22:07
  • @MadProgrammer How would I get the rectangle? – Laszlo Korte Jul 12 '15 at 22:12
  • You have a race condition between your `MouseMotionListener` and your `SwingUtilities.invokeLater`, which allows the `MouseMotionLIstener` to see the `inside` panel while it's empty... – MadProgrammer Jul 13 '15 at 00:29
  • @MadProgrammer That's what I thought, too. But if I replace the JTextArea with a JLabel it just works fine. But assuming it's really the race condition which occurs only when the textarea is used: What can I do to prevent it? – Laszlo Korte Jul 13 '15 at 07:51
  • I remove the listener when it was first called and reapplied it in the `invokeLater`. This seemed to get rid of the majority of the issue... – MadProgrammer Jul 13 '15 at 07:53
  • rebinding the listener fix *most* of the problem indeed. But sometimes the scrolling did still occur. I wrote an answer below: the real problem was the textarea calling scrollRectToVisible by itself. – Laszlo Korte Jul 13 '15 at 09:31

1 Answers1

2

I found a solution. The whole problem is not the JScrollPane itself but the JTextArea calling scrollRectToVisible on itself if the text does not fit. This call propagates through the view hierarchy up to the JScrollPane causing it to scroll to the textarea.

In my case in which I have 30+ JTextareas with constantly changing text, this occurs quite offen.

My solution is to create a custom class inheriting from JTextArea and overriding the scrollRectToVisible method with an empty implementation.

Here is the working example:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;

public class Main {
  // custom Textarea class
  static class MyTextArea extends JTextArea {
    @Override
    public void scrollRectToVisible(final Rectangle aRect) {
      // supress scrollToRect in textarea
    }
  }

  static final Box inner = Box.createVerticalBox();

  public static void main(final String[] args) {
    final JFrame frame = new JFrame();

    final JPanel insideScroll = insideScroll();
    final JScrollPane scrollpane = new JScrollPane(insideScroll);
    scrollpane.setAutoscrolls(false);
    scrollpane
    .setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    final JPanel rightPanel = new JPanel();
    rightPanel.setPreferredSize(new Dimension(300, 300));

    final MouseMotionAdapter listener = new MouseMotionAdapter() {

      @Override
      public void mouseDragged(final MouseEvent e) {
        super.mouseDragged(e);

        update();
      }
    };
    rightPanel.addMouseMotionListener(listener);

    // Add labels describing the problem
    rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.PAGE_AXIS));

    final JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
        scrollpane, rightPanel);

    frame.add(split);

    // initialize the scrollpane content
    update();

    frame.pack();
    frame.setVisible(true);

    frame.setLocationRelativeTo(null);

  }

  // initializes the components inside the scrollpane
  private static JPanel insideScroll() {
    final JPanel panel = new JPanel();
    panel.setLayout(new BorderLayout());
    panel.add(inner, BorderLayout.NORTH);

    return panel;
  }

  // replaces all components inside the scrollpane
  private static void update() {
    inner.removeAll();

    for (int i = 0; i < 30; i++) {
      inner.add(buildRow(i));
    }

    inner.revalidate();
  }

  // build a single component to be inserted into the scrollpane
  private static JPanel buildRow(final int i) {
    final JPanel row = new JPanel();

    final Color bg = i % 2 == 0 ? Color.DARK_GRAY : Color.LIGHT_GRAY;

    row.setBackground(bg);
    row.setPreferredSize(new Dimension(300, 80));
    row.setLayout(new BorderLayout());

    row.add(textarea(bg), BorderLayout.CENTER);

    return row;
  }

  // build the textarea to be inserted into the cells in the scroll pane
  private static Component textarea(final Color bg) {
    final String text = String.format("%d", (int) (1000 * Math.random()))
        + " Lorem ipsum dolor si amet. Lorem ipsum dolor si amet. Lorem ipsum dolor si amet";

    final JTextArea textarea = new MyTextArea();
    textarea.setDisabledTextColor(Color.cyan);

    textarea.setEnabled(false);
    textarea.setLineWrap(true);
    textarea.setBackground(bg);
    textarea.setEditable(false);
    textarea.setText(text);

    return textarea;
  }

}

Update: I just came across this other thread with a better solution for disabling the auto scrolling of the JTextArea - without subclassing:

Java / Swing : JTextArea in a JScrollPane, how to prevent auto-scroll?

Community
  • 1
  • 1
Laszlo Korte
  • 1,179
  • 8
  • 17