1

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...

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
Hakanai
  • 12,010
  • 10
  • 62
  • 132

1 Answers1

2

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...

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I was worried about using `JPanel` because they suggest in the comments that there are performance problems if you *don't* override all those methods, and we have previously witnessed when making a custom check box renderer, similar performance issues happening with check boxes. I will definitely do the instance sharing, though. – Hakanai Dec 02 '14 at 00:55
  • Yeah, that's a definite issue, but in your case, you need this functionality, as each time you change the `String`/text, you need the layout to be revalidated, but you can't do this until the component is actually sized for painting...You could override the `setBounds` method and force a call to `doLayout` each time as a compromise... – MadProgrammer Dec 02 '14 at 00:57
  • That might be the way to do it actually. Or I maintain my own layoutDirty status and do it from paintComponent where I know for sure the component is being painted. – Hakanai Dec 02 '14 at 00:58
  • Technically, components aren't always painted by the container, so I'd stick with `setBounds` personally... – MadProgrammer Dec 02 '14 at 00:59
  • How do I know they won't change Swing in the future to call a different method before `paintComponent`? :( – Hakanai Dec 02 '14 at 01:04
  • Because if they did, it would break the entire API. One of things that must be done before you component can be stamped, is it must be sized, this is done by calling `setBounds`. `setBounds` normally calls `invalidate` and causing the change of updates that will eventually call `doLayout`...so, when `setBounds` is called, call `super.setBounds` and `doLayout` yourself... – MadProgrammer Dec 02 '14 at 01:07
  • Actually, what `setBounds()` actually does is calls `reshape()`... so changing the method which is called has presumably already occurred in the past (JDK 1.1). Looks like `reshape()` is what I actually want to override, then, because I can't trust every possible user of the component to avoid deprecated APIs. :( – Hakanai Dec 02 '14 at 01:15
  • That's try, as `reshape` has been deprecated, which is presumably the change around about the time `Swing` was been introduced. The fact is, there's no way to 100% future proof. What is known, is changing the state of the components within in any `paint` method is a MAJOR no-no – MadProgrammer Dec 02 '14 at 01:17