line wrapping JTextArea as ListCellRenderer?
Hi all,
Short background info: I am the main author of the free flashcard program Pauker (http://pauker.sourceforge.net/). Right now we optionally use the automatic line wrapping feature of JTextArea when entering new cards (see http://pauker.sourceforge.net/screenshots.php?lang=en&pic=NewCardDialog). OK, the line wrapping is not visible at this screenshot :-)
Per users request I wanted to use line wrapping when displaying the cards of a batch (see http://pauker.sourceforge.net/screenshots.php?lang=en&pic=MainWindow).
But it seems next to impossible to use the line wrapping feature when JTextArea is used as a ListCellRenderer. I searched through the bug database and forum but didn't find a solution.
I reduced the problem to the following test case:
import java.awt.*;
import javax.swing.*;
publicclass ScrollTestextends JFrame{
public ScrollTest(){
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// set renderer
JList list =new JList();
list.setCellRenderer(new JTextAreaRenderer(list));
// fill list with example data
DefaultListModel listModel =new DefaultListModel();
String element =
"This text is so long that it must be (automatically) " +
"wrapped by the JTextArea. But somehow it does'nt work...";
for (int i = 0; i < 3; i++){
listModel.addElement(element);
}
list.setModel(listModel);
// add to JFrame
JScrollPane scrollPane =new JScrollPane(list);
getContentPane().add(scrollPane);
pack();
}
publicstaticvoid main(String args[]){
EventQueue.invokeLater(new Runnable(){
publicvoid run(){
new ScrollTest().setVisible(true);
}
});
}
privateclass JTextAreaRendererextends JTextAreaimplements ListCellRenderer{
private Color selectionBackground;
private Color background;
public JTextAreaRenderer(JList list){
selectionBackground = list.getSelectionBackground();
background = list.getBackground();
setLineWrap(true);
setWrapStyleWord(true);
}
public Component getListCellRendererComponent(JList list, Object object,
int index,boolean isSelected,boolean cellHasFocus){
setText((String)object);
setBackground(isSelected ? selectionBackground : background);
returnthis;
}
}
}
The problem is that despite using setLineWrap(true) the renderer doesn't create a line wrapped text area but a single line of text which is even cut off (no scrollbars) ...
Is there a simple solution or do I have to file a bug report?
This helped....
private class JTextAreaRenderer extends JTextArea implements ListCellRenderer {
private Color selectionBackground;
private Color background;
public JTextAreaRenderer(JList list) {
selectionBackground = list.getSelectionBackground();
background = list.getBackground();
setLineWrap(true);
setWrapStyleWord(true);
setPreferredSize(new Dimension (50,100));
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setText((String)object);
setBackground(isSelected ? selectionBackground : background);
return this;
}
}
The JList sets the cell dimensions (including cell height) based on the preferred size of the component returned by the cell renderer. You need to set the preferred size appropriately based on the text length.
Thank you for your response. Setting the preferred size in the renderer's constructor is no solution. The text length is unknown at construction. The text to render is arbitrary (user defined).
I've never tried using a JTextArea in a List since I believe each row in the list is rendered at the same height so I'm not sure how to deal with that.
Anyway, this link shows how to use a text area as a TableCellRenderer. Maybe you can get some ideas from here:
http://javaspecialists.co.za/archive/newsletter.do?issue=106&locale=en_US
So why not overload the setText method to set the preferredSize and then call the super.setText?
Overloading setText seemed to work. The constants about line size and height are off, but that would just take some tweeking.
public class tester extends JFrame {
public tester() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// set renderer
JList list = new JList();
list.setCellRenderer(new JTextAreaRenderer(list));
// fill list with example data
DefaultListModel listModel = new DefaultListModel();
String[] element = new String[]
{"This text is so long that it must be (automatically) " +
"wrapped by the JTextArea. But somehow it does'nt work...",
"This text is shorter than the one before",
"Really Short"};
for (int i = 0; i < 3; i++) {
listModel.addElement(element[i%element.length]);
}
list.setModel(listModel);
// add to JFrame
JScrollPane scrollPane = new JScrollPane(list);
getContentPane().add(scrollPane);
pack();
}
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new tester().setVisible(true);
}
});
}
private class JTextAreaRenderer extends JTextArea implements ListCellRenderer {
private Color selectionBackground;
private Color background;
int charsPerLine = 10;
int lineHeight = 25;
public JTextAreaRenderer(JList list) {
selectionBackground = list.getSelectionBackground();
background = list.getBackground();
setLineWrap(true);
setWrapStyleWord(true);
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setText((String)object);
setBackground(isSelected ? selectionBackground : background);
return this;
}
public void setText(String text) {
super.setText(text);
super.setPreferredSize(new Dimension (100,
lineHeight * (text.length() / charsPerLine)));
}
}
}
Thank you for the link. After looking through the sources I think that the "magic" of Dr. Heinz M. Kabutz's Multi-line JTable cells in is the line:table.setRowHeight(row, height_wanted);Unfortunately, there is no JList.setCellHeight() method...
> Unfortunately, there is no JList.setCellHeight() method... You can always use a JTable with a single column instead of the JList.
John, thanks for another stab at the problem. Overloading setText() seems to help initial list rendering. But it is still no practical solution as the cell sizes are not updated when you resize the JFrame/JList.
> You can always use a JTable with a single column
> instead of the JList.
OK, I need some time (read: days) to try and test if a single column JTable provides everything we need.
I haven't been a Swing basher before, but isn't it a sign of bad design when I have to abuse other classes to mimic the behavior of a simple List?
> but isn't it a sign of bad design when I have to abuse other classes to mimic the behavior of a simple List?
Well, I guess your definition and my definition of a simple list is different. I would say it's you who is trying to bend the definition of a "simple list".
A JTable was designed to be more flexible to handle rows and columns and different widths and heights. Why should all that excess code be duplicated in a JList as well? The idea is to pick the component thats right for the job.
You could always just use a single line JList to display the initial part of the text and then use a tooltip to display the entire text message. This is a common design in GUI's.
> I would say it's you who is trying to bend the definition
> of a "simple list".
It's not only my definition that a list is a component for one-dimensional arrays and a table is for two-dimensional arrays.
> A JTable was designed to be more flexible...
That exactly seems to be the problem. JList seems to be too inflexible. If I need a table for displaying a list something is very wrong by design.
> It's not only my definition that a list is a component for one-dimensional arrays and a table is for two-dimensional arrays.
No. A list is for displaying data in cells of equal sizes and it gives you the ability to select these cells. Plain and simple. If you requirement is to display vertical components, then you could always do this with a BoxLayout and add individual textAreas on top of one another.
Also, you're not thinking outside the box. I've used a list for renderering two columns of data. This example uses a JCombobox, but the idea is the same.
http://forum.java.sun.com/thread.jspa?forumID=57&threadID=692641
> JList seems to be too inflexible. If I need a table for displaying a list something is very wrong by design
No, the design of your application is wrong.
A JList is designed to display simple data. The size of each row is fixed, therefore the size of the list is easily calculated by multiplying the row height by the number of rows. This makes it easy to calculate the size, do scrolling and other things as well when you can make some assumptions. So in general you have a component that is very efficient.
Why does Swing have a JTextField a JTextArea and a JTextPane. If you just need the ability to enter text you couuld always use a JTextPane. But again you have a lot of extra overhead that is not required in the simple case. So your application is made more efficient by using the simpler JTextField whenever possible if you don't require all the fancy styled text features of JTextPane.
The same analogy can be drawn for JList and JTable, JList for simple cases that handles 90% of peoples requirements and JTable when you need the additional flexibility.
Again you can always change you design. Just display a standard 2 lines of code in the text area. Then if the cell has more data you give the use a way to see the extra data, like a tooltip. This is standard feature in most application. Take for example, windows explorer. Data is displayed in multiple columns. The column width is set so that is probably handles 90% of the width of your file names. It wouldn't make sense to set the width to the largest file names because you could have one that is 100 characters which would mean that all the other columns would not be visible. So the tradeoff is to set a reasonable width and then use a tooltip.
Same in your case you are trying to handle 100% of the cases with a simple component. You can't always fit a square peg into a round hole.
> No. A list is for displaying data in cells of equal sizes...
Wrong. A JList displays per default cells of arbitrary sizes. You can change this only by calling JList.setFixedCellHeight(int height)
or JList.setFixedCellWidth(int width)
Therefore I do not follow your reasoning that my application design is broken and still think that JList is doing something wrong or is missing some flexibility.
> you could always do this with a BoxLayout and add individual textAreas...
This would be even worse. Some Pauker lessons contain more than 12.000 cards. Displaying the summary would then allocate 12.000 textAreas instead of using the renderer 12.000 times like a "rubber stamp" to paint each visible cell. I don't like OutOfMemoryError's.
> Wrong. A JList displays per default cells of arbitrary sizes. You can change this only by calling
I give up!! Yes you can specify the cell size, but it is the same for every cell in the list...
> Therefore I do not follow your reasoning that my application design is broken
I did not say it was broken. I said it could be improved so that you could still use a text area as a renderer to display a 2-3 line summary (or whatever you think is reasonable) of the data and provide some mechanism to display the entire text if the user decides then need to see the entire text.
> This would be even worse. Some Pauker lessons contain more than 12.000 cards.
Again, I get back to design. Why would you display 12,000 entries in a single JList? It would take the user forever to scroll through the entire list to find the relevant information that they want.
> Yes you can specify the cell size, but it is the same for every cell in the list...
You are wrong again. I created a special screentshot just for you. Please take a look at http://pauker.sourceforge.net/JList.png. The card list at the right bottom is a standard JList. Please note that all cell sizes are different.
> It would take the user forever to scroll through the entire list to find the relevant information that they want.
There is a search function implemented. You can see it in the screenshot :-)
By the way, I don't think the idea with the tooltip is a good design. It would need a lot of mouse movement to show all information. Automatic line wrapping in combination with scrolling (if a certain minimum list width is reached) would be better.
Can you let us know how you got the individual cells to be different heights?
I took your test case from the first post and modified the renderer to resize
properly, but the best I can do is get the cells the same size. I modified the
test string to concatenate so it increases in size for each cell. This results
in the first cell being as tall as the third, which is too tall for the first but
just right for the third.
If I could make it so each cell just fit the text, that would be perfect, but the
best I can do at present is get each cell the height of the tallest one.
Thanks in advance for pointing out how you did this!
: jay
JayDSa at 2007-7-20 20:50:54 >

OK, here comes a short example for a standard JList with different cell sizes:
import java.awt.*;
import javax.swing.*;
public class Example extends JFrame {
public Example() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// set renderer
JList list = new JList();
list.setCellRenderer(new JTextAreaRenderer(list));
// fill list with example data
DefaultListModel listModel = new DefaultListModel();
listModel.addElement("One line");
listModel.addElement("Two\nlines");
listModel.addElement("Even\nthree\nlines");
list.setModel(listModel);
// add to JFrame
JScrollPane scrollPane = new JScrollPane(list);
getContentPane().add(scrollPane);
setSize(100, 150);
}
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new Example().setVisible(true);
}
});
}
private class JTextAreaRenderer extends JTextArea implements ListCellRenderer {
private Color selectionBackground;
private Color background;
public JTextAreaRenderer(JList list) {
selectionBackground = list.getSelectionBackground();
background = list.getBackground();
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setText((String)object);
setBackground(isSelected ? selectionBackground : background);
return this;
}
}
}
This is mostly correct. It occasionally gives some extra lines, but I attribute
that to the difference between what FontMetrics reports for the String width
and what it actually gets rendered with.
import java.awt.*;
import java.awt.event.*;
import java.util.StringTokenizer;
import javax.swing.*;
public class ScrollTest extends JFrame {
public static void main( String args[] ) {
EventQueue.invokeLater( new Runnable() {
public void run() {
new ScrollTest().setVisible( true );
}
} );
}
public ScrollTest() {
setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE );
// set renderer
JList list = new JList();
list.setCellRenderer( new JTextAreaRenderer( list ) );
// fill list with example data
DefaultListModel listModel = new DefaultListModel();
StringBuffer[] sb = new StringBuffer[5];
sb[0] = new StringBuffer(
"This text is so long that it must be (automatically) " ).append(
"wrapped by the JTextArea. And somehow it (mostly) works..." );
sb[1] = new StringBuffer().append( sb[0] ).append( sb[0] );
sb[2] = new StringBuffer().append( sb[0] ).append( sb[1] );
sb[3] = sb[1];
sb[4] = sb[0];
for ( int k = 0; k < 5; k++ ) {
listModel.addElement( sb[k].toString() );
}
list.setModel( listModel );
// add to JFrame
JScrollPane scrollPane = new JScrollPane( list );
getContentPane().add( scrollPane );
pack();
}
public static int getStringWidth( Component c, String s ) {
if ( s == null )
return 0;
else {
// This seems to generally return a longer width than is correct
FontMetrics f = c.getFontMetrics( c.getFont() );
return f.stringWidth( s );
}
}
private class JTextAreaRenderer extends JTextArea implements ListCellRenderer, ComponentListener {
private Color selectionBackground;
private Color background;
private int oldWidth;
private int defaultHeight;
boolean forcedResize;
public JTextAreaRenderer( final JList list ) {
selectionBackground = list.getSelectionBackground();
background = list.getBackground();
setLineWrap( true );
setWrapStyleWord( true );
list.addHierarchyListener( new HierarchyListener() {
public void hierarchyChanged( HierarchyEvent e ) {
list.removeHierarchyListener( this );
if ( list.getParent() instanceof JViewport ) {
// Add resize listener so height gets adjusted
list.getParent().addComponentListener( JTextAreaRenderer.this );
// Get rid of initial horizontal scrollbar
Runnable doRun = new Runnable() {
public void run() {
list.getParent().invalidate();
list.getParent().repaint();
}
};
SwingUtilities.invokeLater( doRun );
} else {
// Add resize listener so height gets adjusted
list.addComponentListener( JTextAreaRenderer.this );
}
}
} );
// Won't have a preferred height without text
setText( "M" );
defaultHeight = getPreferredSize().height;
}
public Component getListCellRendererComponent( final JList list, Object object, int index, boolean isSelected, boolean cellHasFocus ) {
String text = (String) object;
setText( text );
setBackground( isSelected ? selectionBackground : background );
if ( list.isShowing() ) {
if ( !forcedResize ) {
// Highly dependent on BasicListUI implementation
forcedResize = true;
int fixed = list.getFixedCellHeight();
list.setFixedCellHeight( fixed + 1 );
list.setFixedCellHeight( fixed );
}
int neededHeight = 0;
int currentWidth = list.getWidth();
if ( list.getParent() instanceof JViewport ) {
currentWidth = list.getParent().getWidth();
JScrollBar vert = ( (JScrollPane) list.getParent().getParent() ).getVerticalScrollBar();
if ( vert.isShowing() )
currentWidth -= vert.getWidth();
}
int numLines = 0;
// Insets
Insets insets = getInsets();
currentWidth += insets.left + insets.right;
// Find number of rows necessary for each line
if ( text != null ) {
StringTokenizer tokenizer = new StringTokenizer( text, "\n" );
int count = tokenizer.countTokens();
for ( int k = 0; k < count; k++ ) {
String token = tokenizer.nextToken();
int lineWidth = getStringWidth( list, token );
numLines += lineWidth / currentWidth + 1;
}
}
// Make sure we've got a good height
if ( numLines <= 1 )
neededHeight = defaultHeight;
else
neededHeight = numLines * defaultHeight;
neededHeight += insets.top + insets.bottom;
Dimension d = new Dimension( currentWidth, neededHeight );
setPreferredSize( d );
}
return this;
}
public void componentResized( ComponentEvent e ) {
JComponent resized = (JComponent) e.getSource();
if ( resized.isShowing() ) {
int newWidth = resized.getWidth();
if ( newWidth != oldWidth ) {
oldWidth = newWidth;
forcedResize = false;
}
}
}
public void componentMoved( ComponentEvent e ) {}
public void componentShown( ComponentEvent e ) {}
public void componentHidden( ComponentEvent e ) {}
}
}
JayDSa at 2007-7-20 20:50:54 >

Sorry for the delay and thank you for the (mostly) working test case. I hope to find some time this weekend to study your source in detail and to understand all your "black Swing magic".
Hi Jay,
I finally found some time to take a look at your code. I understand most of it but not the following two parts:
1. Why do you use a HierarchyListener instead of directly using addComponentListener()?
2. What happens at the following lines?list.setFixedCellHeight( fixed + 1 );
list.setFixedCellHeight( fixed );
I find it very confusing to set a fixed cell height when all I want is the opposite. How did you find this solution? Is this somewhere documented?
I noticed that you try to estimate the number of necessary lines. But there is a simpler and more exact way - the javax.swing.text.View class. I took some of your ideas and implemented a new solution. My version looks a bit better now but the code still sucks. Isn't there a cleaner way without all this ugly hacks?
Here comes the code:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.text.View;
public class FixedScrollTest2 extends JFrame {
public FixedScrollTest2() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// prepare model
DefaultListModel listModel = new DefaultListModel();
StringBuffer[] sb = new StringBuffer[5];
sb[0] = new StringBuffer(
"This text is so long that it must be (automatically) wrapped " +
"by the JTextArea. With the help of some really ugly hacks it " +
"somewhat works...");
sb[1] = new StringBuffer().append(sb[0]).append(sb[0]);
sb[2] = new StringBuffer().append(sb[0]).append(sb[1]);
sb[3] = sb[1];
sb[4] = sb[0];
for ( int k = 0; k < 5; k++ ) {
listModel.addElement(sb[k].toString());
}
// assemble list, model, scrollpane & renderer
JList list = new JList();
list.setModel(listModel);
JScrollPane scrollPane = new JScrollPane(list);
JTextAreaRenderer renderer = new JTextAreaRenderer(scrollPane, list);
list.setCellRenderer(renderer);
scrollPane.addComponentListener(renderer);
getContentPane().add(scrollPane);
setSize(300, 300);
setLocationRelativeTo(null);
}
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new FixedScrollTest2().setVisible(true);
}
});
}
/**
* List cell renderer that uses the list's width to alter its preferred size
*/
private class JTextAreaRenderer
extends JTextArea
implements ListCellRenderer, ComponentListener {
private JList list;
private int listWidth;
private Color selectedColor;
private Color backgroundColor;
private View rootView;
private int rendererLeftRightInsets;
private int topDownInsets;
boolean forcedResize;
private JScrollPane scrollPane;
private int scrollPaneLeftRightInsets;
private JScrollBar verticalScrollBar;
public JTextAreaRenderer(JScrollPane scrollPane, JList list) {
this.scrollPane = scrollPane;
this.list = list;
selectedColor = list.getSelectionBackground();
backgroundColor = list.getBackground();
setLineWrap(true);
setWrapStyleWord(true);
setBorder(new LineBorder(Color.black));
rootView = getUI().getRootView(this);
Insets insets = getInsets();
rendererLeftRightInsets = insets.left + insets.right;
topDownInsets = insets.top + insets.bottom;
Insets scrollPaneInsets = scrollPane.getBorder().getBorderInsets(scrollPane);
scrollPaneLeftRightInsets = scrollPaneInsets.left + scrollPaneInsets.right;
verticalScrollBar = scrollPane.getVerticalScrollBar();
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setBackground(isSelected ? selectedColor : backgroundColor);
String text = (String)object;
setText(text);
if (!forcedResize) {
// Highly dependent on BasicListUI implementation
forcedResize = true;
int fixed = list.getFixedCellHeight();
// ? looks like a really bad hack, what happens here ?
list.setFixedCellHeight(fixed + 1);
list.setFixedCellHeight(fixed);
}
float yAxisSpan = rootView.getPreferredSpan(View.Y_AXIS);
Dimension preferredSize = new Dimension(
listWidth, (int)yAxisSpan + topDownInsets);
setPreferredSize(preferredSize);
return this;
}
public void componentResized(ComponentEvent componentEvent) {
listWidth = scrollPane.getWidth() - scrollPaneLeftRightInsets;
if (verticalScrollBar.isShowing()) {
listWidth -= verticalScrollBar.getWidth();
}
int viewWidth = listWidth - rendererLeftRightInsets;
rootView.setSize(viewWidth, Float.MAX_VALUE);
forcedResize = false;
// Without the following lines we often get a horizontal scrollbar
// when reducing the width of the list. But even with those lines it
// looks horrible because we get a temporary "flickering" horizontal
// scrollbar...
list.getParent().invalidate();
list.getParent().repaint();
}
public void componentMoved( ComponentEvent e ) {}
public void componentShown( ComponentEvent e ) {}
public void componentHidden( ComponentEvent e ) {}
}
}
1. I used the HierarchyListener so that I could get the horizontal scrollbar removed. Without that runnable in there, when the window first showed, it always had a horizontal scrollbar which was unnecessary.
The only reason for adding the component listener there was to make it a bit more flexible: the listener has to be on the viewport if the list is in a scrollpane, otherwise it must be on the list.
2. These two lines simply change the fixedCellHeight and restore it back to what it was. If you look at BasicListUI (as the comment notes, the code is highly dependent on that implementation), you should notice that the height of each row is cached. Updating the fixedCellHeight is one way to force the cache to be recalculated, which is necessary when the viewport is resized. This isn't documented anywhere, and will possibly break is some other ListUI is used, especially if it doesn't inherit from BasicListUI.
I would venture the only cleaner way to do this would be to subclass JList and BasicListUI and expose individual row heights as JTable does.
: jay
JayDSa at 2007-7-20 20:50:55 >

Hi Jay,
> I would venture the only cleaner way to do this would
> be to subclass JList and BasicListUI and expose
> individual row heights as JTable does.
I finally found some time to do what you suggested. It works much better now. There is only one problem left: When I switch between linewrapping and no linewrapping the behavior is very strange: It works only every second time. Do you know why or how to fix this?
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicListUI;
import javax.swing.text.View;
public class FixedScrollTest3 extends JFrame {
private boolean lineWrap;
public FixedScrollTest3() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// prepare model
DefaultListModel listModel = new DefaultListModel();
StringBuffer[] sb = new StringBuffer[5];
sb[0] = new StringBuffer(
"This text is so long that it must be (automatically) wrapped " +
"by the JTextArea. By subclassing JList and BasicListUI it " +
"somewhat works...");
sb[1] = new StringBuffer().append(sb[0]).append(sb[0]);
sb[2] = new StringBuffer().append(sb[0]).append(sb[1]);
sb[3] = sb[1];
sb[4] = sb[0];
for ( int k = 0; k < 5; k++ ) {
listModel.addElement(sb[k].toString());
}
// assemble list, model, scrollpane & renderer
final UpdateLayoutList list = new UpdateLayoutList();
list.setModel(listModel);
final JScrollPane scrollPane = new JScrollPane(list);
final JTextAreaRenderer renderer = new JTextAreaRenderer(scrollPane, list);
list.setCellRenderer(renderer);
scrollPane.addComponentListener(renderer);
getContentPane().add(scrollPane , java.awt.BorderLayout.CENTER);
final JToggleButton jToggleButton = new JToggleButton("linewrap");
jToggleButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
lineWrap = jToggleButton.isSelected();
renderer.setLineWrap(lineWrap);
if (lineWrap) {
scrollPane.addComponentListener(renderer);
} else {
renderer.setPreferredSize(null);
scrollPane.removeComponentListener(renderer);
}
list.getParent().doLayout();
list.doLayout();
}
});
getContentPane().add(jToggleButton, java.awt.BorderLayout.SOUTH);
setSize(450, 450);
setLocationRelativeTo(null);
}
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new FixedScrollTest3().setVisible(true);
}
});
}
private class JTextAreaRenderer
extends JTextArea
implements ListCellRenderer, ComponentListener {
private JList list;
private int listWidth;
private Color selectedColor;
private Color backgroundColor;
private View rootView;
private int rendererLeftRightInsets;
private int topDownInsets;
private JScrollPane scrollPane;
private int scrollPaneLeftRightInsets;
private JScrollBar verticalScrollBar;
public JTextAreaRenderer(JScrollPane scrollPane, JList list) {
this.scrollPane = scrollPane;
this.list = list;
selectedColor = list.getSelectionBackground();
backgroundColor = list.getBackground();
setWrapStyleWord(true);
setBorder(new LineBorder(Color.black));
rootView = getUI().getRootView(this);
Insets insets = getInsets();
rendererLeftRightInsets = insets.left + insets.right;
topDownInsets = insets.top + insets.bottom;
Insets scrollPaneInsets = scrollPane.getBorder().getBorderInsets(scrollPane);
scrollPaneLeftRightInsets = scrollPaneInsets.left + scrollPaneInsets.right;
verticalScrollBar = scrollPane.getVerticalScrollBar();
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setBackground(isSelected ? selectedColor : backgroundColor);
String text = (String)object;
setText(text);
if (lineWrap) {
float yAxisSpan = rootView.getPreferredSpan(View.Y_AXIS);
Dimension preferredSize = new Dimension(
listWidth, (int)yAxisSpan + topDownInsets);
setPreferredSize(preferredSize);
}
return this;
}
public void componentResized(ComponentEvent componentEvent) {
listWidth = scrollPane.getWidth() - scrollPaneLeftRightInsets;
if (verticalScrollBar.isShowing()) {
listWidth -= verticalScrollBar.getWidth();
}
int viewWidth = listWidth - rendererLeftRightInsets;
rootView.setSize(viewWidth, Float.MAX_VALUE);
}
public void componentMoved( ComponentEvent e ) {}
public void componentShown( ComponentEvent e ) {}
public void componentHidden( ComponentEvent e ) {}
}
private class UpdateLayoutList extends JList {
public UpdateLayoutList() {
setUI(new UpdateLayoutListUI());
}
public void doLayout() {
((UpdateLayoutListUI) getUI()).updateLayoutState();
super.doLayout();
}
public boolean getScrollableTracksViewportWidth() {
return lineWrap;
}
}
private static class UpdateLayoutListUI extends BasicListUI {
public void updateLayoutState() {
super.updateLayoutState();
}
}
}
You should return to the link camickr posted and examine the algorithm closely. That technique can be ported from tables to lists and it does everything you want.
Check out this line:
setSize(columnModel.getColumn(column).getWidth(), 100000);
int height_wanted = (int) getPreferredSize().getHeight();
This always figures out the correct height to use when there is line wrapping.
http://javaspecialists.co.za/archive/newsletter.do?issue=106&locale=en_US
Hi James,
thank you for your response. Did you try the code above? It works in almost
every aspect:
If I switch to line wrapping (i.e. select the toggle button at the bottom) and
resize the window line wrapping works like a charm and there is no horizontal
scrollbar. If I switch to no line wrapping (i.e. deselect the toggle button) and
resize the window there is no line wrapping anymore and a horizontal scrollbar
appears.
The remaining problem is that when I do not resize the frame before switching
the wrapping mode it sometimes (every second time) does not seem to work
instantly but I have to resize the frame to see the effect. The following two
lines don't seem to be enough to always enforce the update:
list.getParent().doLayout();
list.doLayout();
I don't see how your recommendation to use the JTable code is helpful here.
Transferred to a real life situation our conversation seems to go like this:
- Dear toolkit provider. You provided me with this wonderful hammer. But it
seems to be too light to drive the nails into this wall here. Do you have a
larger hammer?
- Dear customer. The design of your wall is wrong. And by the way, you should
use our excellent shovel for your larger nails. See, our shovel is much heavier
and has this nice long shaft.
- Dear toolkit provider. I don't need no stinking long shaft shovel. All I want
is a heavier hammer.
- Dear customer. A hammer is only for small nails. For larger nails you need to
use a shovel.
- Dear toolkit provider. Look, I just enlarged your hammer a bit and now I can
drive this nails into my wall. But your hammer design has a problem. Every
second time I pound on a nail the hammer looses its head. Can you fix that?
- Dear customer. Please refer to our first answer. You should use the shovel
design for your larger hammer.
Sorry, couldn't resist :-)
So my question remains: What is missing besides
list.getParent().doLayout();
list.doLayout();
to enforce the layout update?
Well, it worked for me if I just inverted the order. That is, rather than doinglist.getParent().doLayout();list.doLayout();I didlist.doLayout();list.getParent().doLayout();and it switched back and forth just fine as I toggled the button.:
JayDSa at 2007-7-20 20:50:55 >

Hi Jay,
Great, it works! I am very curios: Why does it work this way and how did you find this out again? Try & error? Documentation? Source code? Brilliancy? :-)
Now that I thought that finally all pieces of the puzzle are here I noticed another small problem: When the text is really short and line wrapping is not used the cells width is too small. Here comes the example:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicListUI;
import javax.swing.text.View;
public class FixedScrollTest4 extends JFrame {
private boolean lineWrap;
public FixedScrollTest4() {
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// prepare model
DefaultListModel listModel = new DefaultListModel();
StringBuffer[] sb = new StringBuffer[5];
sb[0] = new StringBuffer("short example text ");
sb[1] = new StringBuffer().append(sb[0]).append(sb[0]);
sb[2] = new StringBuffer().append(sb[0]).append(sb[1]);
sb[3] = sb[1];
sb[4] = sb[0];
for ( int k = 0; k < 5; k++ ) {
listModel.addElement(sb[k].toString());
}
// assemble list, model, scrollpane & renderer
final UpdateLayoutList list = new UpdateLayoutList();
list.setModel(listModel);
final JScrollPane scrollPane = new JScrollPane(list);
final JTextAreaRenderer renderer = new JTextAreaRenderer(scrollPane, list);
list.setCellRenderer(renderer);
scrollPane.addComponentListener(renderer);
getContentPane().add(scrollPane , java.awt.BorderLayout.CENTER);
final JToggleButton jToggleButton = new JToggleButton("linewrap");
jToggleButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
lineWrap = jToggleButton.isSelected();
renderer.setLineWrap(lineWrap);
if (lineWrap) {
scrollPane.addComponentListener(renderer);
} else {
renderer.setPreferredSize(null);
scrollPane.removeComponentListener(renderer);
}
list.doLayout();
list.getParent().doLayout();
}
});
getContentPane().add(jToggleButton, java.awt.BorderLayout.SOUTH);
setSize(450, 450);
setLocationRelativeTo(null);
}
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
public void run() {
new FixedScrollTest4().setVisible(true);
}
});
}
private class JTextAreaRenderer
extends JTextArea
implements ListCellRenderer, ComponentListener {
private int listWidth;
private Color selectedColor;
private Color backgroundColor;
private View rootView;
private int rendererLeftRightInsets;
private int topDownInsets;
private JScrollPane scrollPane;
private int scrollPaneLeftRightInsets;
private JScrollBar verticalScrollBar;
public JTextAreaRenderer(JScrollPane scrollPane, JList list) {
this.scrollPane = scrollPane;
selectedColor = list.getSelectionBackground();
backgroundColor = list.getBackground();
setWrapStyleWord(true);
setBorder(new LineBorder(Color.black));
rootView = getUI().getRootView(this);
Insets insets = getInsets();
rendererLeftRightInsets = insets.left + insets.right;
topDownInsets = insets.top + insets.bottom;
Insets scrollPaneInsets = scrollPane.getBorder().getBorderInsets(scrollPane);
scrollPaneLeftRightInsets = scrollPaneInsets.left + scrollPaneInsets.right;
verticalScrollBar = scrollPane.getVerticalScrollBar();
}
public Component getListCellRendererComponent(JList list, Object object,
int index, boolean isSelected, boolean cellHasFocus) {
setBackground(isSelected ? selectedColor : backgroundColor);
String text = (String)object;
setText(text);
if (lineWrap) {
float yAxisSpan = rootView.getPreferredSpan(View.Y_AXIS);
Dimension preferredSize = new Dimension(
listWidth, (int)yAxisSpan + topDownInsets);
setPreferredSize(preferredSize);
}
return this;
}
public void componentResized(ComponentEvent componentEvent) {
listWidth = scrollPane.getWidth() - scrollPaneLeftRightInsets;
if (verticalScrollBar.isShowing()) {
listWidth -= verticalScrollBar.getWidth();
}
int viewWidth = listWidth - rendererLeftRightInsets;
rootView.setSize(viewWidth, Float.MAX_VALUE);
}
public void componentMoved( ComponentEvent e ) {}
public void componentShown( ComponentEvent e ) {}
public void componentHidden( ComponentEvent e ) {}
}
private class UpdateLayoutList extends JList {
public UpdateLayoutList() {
setUI(new UpdateLayoutListUI());
}
public void doLayout() {
((UpdateLayoutListUI) getUI()).updateLayoutState();
super.doLayout();
}
public boolean getScrollableTracksViewportWidth() {
return lineWrap;
}
}
private static class UpdateLayoutListUI extends BasicListUI {
public void updateLayoutState() {
super.updateLayoutState();
}
}
}
Is there also a simple solution? And please, dont say JTable.
1) Trial and error. This was the third or fourth thing I tried. Sort of makes
sense, in a justification after the fact kind of way: the list's layout has to
change before its parent, otherwise the parent won't know to layout the list
in a different manner.
2) I made the following change to UpdateLayoutList:
public boolean getScrollableTracksViewportWidth() {
return lineWrap || getPreferredSize().width <= getParent().getWidth();
}
Seems to work fine now.
: jay
JayDSa at 2007-7-20 20:50:55 >

Hai Every body
I was not much familer in swing .I am searching for this concept how to use the jTextArea in Jlist here i had seen the code and procedure its very nice Thanks a lot for all . I was very much helped in this topic
Here i am facing one problem in that if we want to change the colors for each jTextArea the how when i change the color
like
this.renderer.setForeground(Color.RED);
it changes for all the text in the list if i want to change for only one string which i passed last
how?
Thanks a lot for u reply
Hai Every body
I was not much familer in swing .I am searching for this concept how to use the jTextArea in Jlist here i had seen the code and procedure its very nice Thanks a lot for all . I was very much helped in this topic
Here i am facing one problem in that if we want to change the colors for each jTextArea the how when i change the color
like
this.renderer.setForeground(Color.RED);
it changes for all the text in the list if i want to change for only one string which i passed last to that list
how?
Thanks a lot for u reply
You need to change the color in the renderer's getListCellRenderer method.Of course, if you just change the foreground to RED, it will be RED in everycell. Presumably, you would change the color depending on some othervalue in the model or some such.
JayDSa at 2007-7-20 20:50:59 >

Hi Jay,
thank you very much for your constructive help! It works now perfectly.
It would be cool if one can achieve this functionality without all the hassle we went through here. What do you think, does all this justify an entry in the Java bug database? A request for enhancement?
Ronny
Hai Jay
Thanks for you r reply if i want to change the font color dynamically how we can change
my application need is first text i send to the list should be one color and second text when i send should be diffrent color
please Tell me how to change the font color its very urgent for me
As an example, I modified the end of the getListCellRenderer method by adding two lines:
setPreferredSize(preferredSize);
}
Color[] colors = { Color.RED, Color.GREEN, Color.BLUE };
setForeground( colors[ index % 3 ] );
return this;
}
As I said, it's up to you how you determine what color to change the text; in this case, it's simple arithmetic.
JayDSa at 2007-7-20 20:50:59 >

Hai Jay
In my application iam setting the string(messg) to the list model (while iam sending the text messg only i want to change the color for this text only )
this.listModel.addElement(messg);
after that what i have to write(to change color of this text only)
this.jList1.setModel(listModel);
You have told to change getListCellRendererComponent() method
but the text is changing with specified colors in that method
so if i want to change the color dynamically which i send
how we can do
i want to tell the color also dynamilcally for that
Please sorry for any mistakes if had written wrong
Thank You
At this point, I'd recommend creating a new thread and posting a [url http://homepage1.nifty.com/algafield/sscce.html]Short, Self Contained, Compilable and Executable, Example Program [/url] that demonstrates the problem you are trying to solve.
That said, you could probably change the renderer like the following:
setPreferredSize(preferredSize);
}
if ( index == list.getModel().getSize()-1 && lastItemColor != null )
setForeground( lastItemColor );
return this;
}
private Color lastItemColor;
public void setLastItemColor( Color c ) {
lastItemColor = c;
}
JayDSa at 2007-7-20 20:50:59 >
