javaswingjscrollpanejtextpanejslider

Keeping a JScrollPane's (JTextPane) scroll bar at the bottom when resizing text?


Very weird problem: I have a JTextPane within a JScrollPane, and a JSlider for resizing the text. If I increase the text size, the scroll bar moves up (normal). My goal is to keep the scroll bar at the bottom, if it was at the bottom before resizing the text. The weird thing is, I can ONLY get it to work if I throw in a JOptionPane (dialog) towards the end of the ChangeListener. If I don't include the dialog (comment out line 92), the scroll bar still moves up when increasing text size, apparently ignoring line 100.

Another thing: if I click to change the text size, the dialog window freaks out, popping up multiple times and screwing up the slider. However, if I use Tab to select the slider and then arrow keys to move it up and down, the dialog window acts normally, and the scroll bar will do what I want it to.

So, actually two mysteries here:

  1. Why does line 100, which sends the scroll bar to the bottom, only seem to work if it's preceded by a dialog popup (line 92)?
  2. Why does the dialog/slider get all screwy when using a mouse to move the slider, but not when using Tab+arrow keys?

This is driving me bonkers. Good luck and thanks!

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;



public class ScrollTest extends JFrame {
   
   private JPanel mainPanel;
   private JTextPane textPane;
   private JScrollPane scrollPane;
   private JScrollBar scrollBar;
   private JSlider textSlider;
   private JFrame dialogFrame;
   
   public static void main(String[] args) {
   
      java.awt.EventQueue.invokeLater(
         new Runnable() {
            public void run() {
            
               ScrollTest test = new ScrollTest();
               test.pack();
               test.setLocationRelativeTo(null);
               test.setVisible(true);
            }
         });
   }
   
   public ScrollTest() {
   
      super("Scroll Test");
      this.setDefaultCloseOperation(EXIT_ON_CLOSE);
      
      mainPanel = new JPanel();
      mainPanel.setPreferredSize(new Dimension(250, 100));
      this.add(mainPanel);
      
      textPane = new JTextPane();
      textPane.setEditable(false);
      textPane.setText("hello\nhello\nhello\nhello\nhello\nhello\nhello\nhello\nhello\nhello");
      textPane.setFont(new Font("Courier New", Font.BOLD, 10));
      
      scrollPane = new JScrollPane(textPane);
      scrollPane.setPreferredSize(new Dimension(80, 80));
      mainPanel.add(scrollPane);
      
      scrollBar = scrollPane.getVerticalScrollBar();
      
      textSlider = new JSlider(10, 16, 10);
      textSlider.setMajorTickSpacing(2);
      textSlider.setPreferredSize(new Dimension(120, 40));
      textSlider.setPaintLabels(true);
      textSlider.setSnapToTicks(true);
      mainPanel.add(textSlider);
      
      dialogFrame = new JFrame();
      
      textSlider.addChangeListener(
         new ChangeListener() { 
            public void stateChanged(ChangeEvent e) {
               
               /**
                *  Determine whether the scroll bar is at the bottom.
                *  Must be done this way because getValue() returns 
                *  the position at the top of the knob.
                */
               int value = scrollBar.getValue();
               int max = scrollBar.getMaximum() - scrollBar.getVisibleAmount();
               boolean startedAtBottom = value == max;
               
               /**
                *  Resize the text.
                *  Increasing the text size moves the scroll bar up.
                */
               switch (textSlider.getValue()) {
                  case 10: textPane.setFont(textPane.getFont().deriveFont(10f));
                     break;
                  case 12: textPane.setFont(textPane.getFont().deriveFont(12f));
                     break;
                  case 14: textPane.setFont(textPane.getFont().deriveFont(14f));
                     break;
                  case 16: textPane.setFont(textPane.getFont().deriveFont(16f));
                     break;               
               }
               
               /**
                *  IMPORTANT: line 92 seems to determines whether line 100
                *  works properly (for some reason).
                *  Comment out line 92 to test.
                */
               JOptionPane.showMessageDialog(dialogFrame, "test", "dialog", JOptionPane.PLAIN_MESSAGE);
               
               /**
                *  Send the scroll bar to the bottom ONLY if it was at 
                *  the bottom before the text was resized.
                *  If line 92 is commented out, this doesn't work when 
                *  increasing text size.
                */
               if (startedAtBottom) scrollBar.setValue(scrollBar.getMaximum());
            }
         });
   }
}

Solution

  • I modified your code to create the following GUI.

    Scroll Test

    The major changes I made are in the stateChanged method. I added the test for the slider to stop moving and the JFrame pack. The JFrame pack makes sure all the Swing components have resized properly.

    Here's the complete runnable code.

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Font;
    
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JScrollBar;
    import javax.swing.JScrollPane;
    import javax.swing.JSlider;
    import javax.swing.JTextPane;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    
    public class ScrollTest implements Runnable {
    
        public static void main(String[] args) {
            EventQueue.invokeLater(new ScrollTest());
        }
        
        private JFrame frame;
    
        private JScrollBar scrollBar;
        
        private JTextPane textPane;
    
        @Override
        public void run() {
            frame = new JFrame("Scroll Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            frame.add(createMainPanel(), BorderLayout.CENTER);
    
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    
        private JPanel createMainPanel() {
            JPanel mainPanel = new JPanel();
            mainPanel.setPreferredSize(new Dimension(250, 100));
    
            textPane = new JTextPane();
            textPane.setEditable(false);
            textPane.setText("hello\nhello\nhello\nhello\nhello\nhello\nhello\nhello\nhello\nhello");
            textPane.setFont(new Font("Courier New", Font.BOLD, 10));
    
            JScrollPane scrollPane = new JScrollPane(textPane);
            scrollPane.setPreferredSize(new Dimension(80, 80));
            mainPanel.add(scrollPane);
    
            scrollBar = scrollPane.getVerticalScrollBar();
    
            JSlider textSlider = new JSlider(10, 16, 10);
            textSlider.setMajorTickSpacing(2);
            textSlider.setPreferredSize(new Dimension(120, 40));
            textSlider.setPaintLabels(true);
            textSlider.setSnapToTicks(true);
            textSlider.addChangeListener(new FontSizeListener());
            mainPanel.add(textSlider);
    
            return mainPanel;
        }
    
        public class FontSizeListener implements ChangeListener {
    
            @Override
            public void stateChanged(ChangeEvent event) {
                JSlider slider = (JSlider) event.getSource();
                
                if (!slider.getValueIsAdjusting()) {
                    /**
                     * Determine whether the scroll bar is at the bottom. Must be done this way
                     * because getValue() returns the position at the top of the knob.
                     */
                    int value = scrollBar.getValue();
                    int max = scrollBar.getMaximum() - scrollBar.getVisibleAmount();
                    boolean startedAtBottom = value == max;
    
                    /**
                     * Resize the text. Increasing the text size moves the scroll bar up.
                     */
                    textPane.setFont(textPane.getFont().deriveFont((float) slider.getValue()));
                    frame.pack();
    
                    /**
                     * Send the scroll bar to the bottom ONLY if it was at the bottom before the
                     * text was resized. If line 92 is commented out, this doesn't work when
                     * increasing text size.
                     */
                    if (startedAtBottom) {
                        max = scrollBar.getMaximum() + scrollBar.getVisibleAmount();
                        scrollBar.setValue(max);
                    }
                }
                
            }
    
        }
    
    }