1

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) {}
}
Hakan Dilek
  • 2,178
  • 2
  • 23
  • 35
user2011659
  • 847
  • 1
  • 7
  • 15
  • I'm not an expert here but I'd probably try to build that myself, i.e. wrap the text pane and area with a panel and put that into the scroll pane. – Thomas Oct 12 '21 at 06:57
  • For sanity, I might do something more like [this](https://stackoverflow.com/questions/21766588/make-jscrollpane-control-multiple-components/21767752#21767752) – MadProgrammer Oct 12 '21 at 07:20

1 Answers1

0

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;
    }       
}
Thomas
  • 87,414
  • 12
  • 119
  • 157
  • Putting both components into a JPanel inside the JScrollPane viewport fixes the scrolling issue. But is there now a way to get the JTextArea to expand in y direction to viewport size as before? – user2011659 Oct 12 '21 at 08:13
  • @user2011659 you could play around with methods like `getPreferredSize()` etc. which report size preferences to the layout manager. One option might be to return `true` from `getScrollableTracksViewportHeight()` and `new Dimension(getPreferredSize().width, Integer.MAX_VALUE)` from `getPreferredScrollableViewportSize()`. Then change the constraints for the components in the panel, i.e. add `weighty=1.0` for both and `anchor=GridBagConstraints.NORTH` for at least the numbers component (although adding it for both shouldn't do any harm) (edited the answer to reflect this). – Thomas Oct 12 '21 at 08:40
  • 1
    I only changed `getScrollableTracksViewportHeight()` to return `((JViewport)getParent()).getHeight() > getPreferredSize().height` after sanity check. Now it works and looks good. Thanks. – user2011659 Oct 12 '21 at 09:33