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?
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;
}
}