Home > Software design >  JScrollPane dynamic RowHeader out of sync when resizing
JScrollPane dynamic RowHeader out of sync when resizing

Time:10-13

I want to implement a TextArea with a sidebar that shows the index of the leftmost character in each line of a wrapped text. This is implemented with a JScrollPane using RowHeader showing a JTextPane for the numbers and Viewport showing a JTextArea for the sequence.

All works well except that the RowHeader and Viewport are out of sync when resizing horizontally. In the following SSCCE it also happens at startup.

The picture illustrates this: The cursor of the JTextArea is at the first position while the lower end of JTextPane is shown.

I know it has something to do with the JTextPane changing its content dynamically because it works with static content.

Is it possible to prevent this odd behaviour?

enter image description here

package main;

import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Collections;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Utilities;

public class SequenceAreaExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setSize(150, 200);

        SequenceArea sequenceArea = new SequenceArea();
        frame.add(sequenceArea);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        
        frame.setVisible(true);
        sequenceArea.setText(String.join("", Collections.nCopies(200, "a")));
    }
}

class SequenceArea extends JScrollPane implements ComponentListener {
    private static final long serialVersionUID = 1L;

    private JTextArea text;
    private JTextPane numbers;

    // FIXME text and numbers are out of sync when resizing horizontally
    SequenceArea() {
        text = new JTextArea();
        text.setLineWrap(true);
        text.setEnabled(true);
        text.addComponentListener(this);

        numbers = new JTextPane();
        numbers.setEnabled(false);

        setViewportView(text);
        setRowHeaderView(numbers);
    }

    void updateNumbers() {
        StringBuilder newNumbers = new StringBuilder();

        int length = text.getText().length();
        int index = 0;

        while (index < length) {
            try {
                int start = Utilities.getRowStart(text, index);
                int end = Utilities.getRowEnd(text, index);

                newNumbers.append(start);
                newNumbers.append('\n');

                index = end   1;
            } catch (BadLocationException e) {
                break;
            }
        }

        numbers.setText(newNumbers.toString());
    }

    void setText(String t) {
        text.setText(t);
    }

    @Override
    public void componentResized(ComponentEvent e) {
        updateNumbers();
    }

    @Override
    public void componentMoved(ComponentEvent e) {}

    @Override
    public void componentShown(ComponentEvent e) {}

    @Override
    public void componentHidden(ComponentEvent e) {}
}

CodePudding user response:

As per my comment I tried to build an alternative solution with the row header being part of the viewport and it seems to work fine so far. (Btw, kudos for adding a SSCCE right at the start)

JPanel panel = new ScrollablePanel();        
panel.setLayout(new GridBagLayout());

//can be reused as constraints are only read when adding components
GridBagConstraints constraints = new GridBagConstraints();

//components should take all vertical space if possible.
constraints.weighty = 1.0;

//components should expand even if it doesn't need more space
constraints.fill = GridBagConstraints.BOTH;

//add the numbers component 
panel.add(numbers, constraints );

//add specific constraints for the text component        
//text takes as much of the width as it can get
constraints.weightx = 1.0;

//numbers component seems to have some insets so add 2px at the top to get better alignment - could be done differently as well
constraints.insets = new Insets(2, 0, 0, 0);
panel.add(text, constraints);
    
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

    
setViewportView(panel);

For this to work properly you also need to use a panel that's aware of scrolling and able to adjust its dimensions to the viewport:

class ScrollablePanel extends JPanel implements Scrollable {
    public Dimension getPreferredScrollableViewportSize() {
        //the panel prefers to take as much height as possible
        return new Dimension(getPreferredSize().width, Integer.MAX_VALUE);
    }

    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 1;
    }

    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 1;
    }

    public boolean getScrollableTracksViewportWidth() {
        return true;
    }

    public boolean getScrollableTracksViewportHeight() {
        return true;
    }       
}
  • Related