Search code examples
javaswingswingxswingx-highlighter

How do I use SwingX's Highlighter to return a different renderer component?


I noticed SwingX's Highlighter interface allows the highlighter to return a different component from the one that is being passed in. I can't actually find any examples of this being used, but I thought I would try to use it to create some kind of fake second column.

The intended result is that text in the left column should truncate where the right column starts, so I can't just use a Painter. The right column should render the same width for the whole list, which is an issue I haven't figured out yet but which doesn't seem like it will be hard.

As for right now though, I am finding that the row height gets compressed to be so small, you can't see any of the text.

Here's what I mean:

screenshot

Sample program:

import java.awt.BorderLayout;
import java.awt.Component;

import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.LayoutStyle;
import javax.swing.SwingUtilities;

import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.renderer.JRendererLabel;
import org.jdesktop.swingx.renderer.StringValue;

public class RendererTest implements Runnable
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new RendererTest());
    }

    @Override
    public void run()
    {
        JFrame frame = new JFrame("Highlighter test");
        JXList list = new JXList();
        DefaultListModel<String> listModel = new DefaultListModel<>();
        listModel.addElement("one");
        listModel.addElement("two");
        listModel.addElement("three");
        list.setModel(listModel);
        list.setVisibleRowCount(8);
        list.setPrototypeCellValue("some string");
        list.addHighlighter(new AddSecondColumnHighlighter(v -> ((String) v).toUpperCase()));
        JScrollPane listScroll = new JScrollPane(list);
        frame.setLayout(new BorderLayout());
        frame.add(listScroll, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
    }

    private static class AddSecondColumnHighlighter extends AbstractHighlighter
    {
        private final StringValue secondColumnStringValue;

        public AddSecondColumnHighlighter(StringValue secondColumnStringValue)
        {
            this.secondColumnStringValue = secondColumnStringValue;
        }

        @Override
        protected Component doHighlight(Component component, ComponentAdapter adapter)
        {
            JRendererLabel rightColumn = new JRendererLabel();
            rightColumn.setText(secondColumnStringValue.getString(adapter.getValue()));

            return new FixedSecondColumnRendererLabel(component, rightColumn);
        }
    }

    private static class FixedSecondColumnRendererLabel extends JRendererLabel
    {
        private FixedSecondColumnRendererLabel(Component leadingComponent, Component trailingComponent)
        {
            GroupLayout layout = new GroupLayout(this);
            setLayout(layout);

            layout.setHorizontalGroup(layout.createSequentialGroup()
                                            .addComponent(leadingComponent, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                            .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
                                            .addComponent(trailingComponent));

            layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
                                          .addComponent(leadingComponent)
                                          .addComponent(trailingComponent));
        }
    }

}

I'm wondering if there is a right way to use this bit of the API. I deliberately extended JRendererLabel in case that was the issue, but it seems to be something more subtle...


Solution

  • If you have a look at JRendererLabel, you will see that invalidate, revalidate, validate (and a bunch of other methods) have been set to "no operation", meaning that they no longer make notifications to the layout manager that the component should be laid out. This is done to help improve the performance of "stamping" the renderer onto the component (the JXList).

    Instead, use extend FixedSecondColumnRendererLabel from JPanel.

    Instead of creating a new instance of FixedSecondColumnRendererLabel and JRendererLabel each time the method is called, you should consider optimising it so that you return the same instance, but one which is configured each time the method is called.

    Remember, this method is called for EACH row in your JXList, the more you have, the more times it will be called, the more short lived objects it will create...