I have creeated a custom cell renderer for JComboBoxes, based on DefaultListCellRenderer (my one basically shows a color). So far it works nicely.
The only problem is, when the JComboBox is disabled: In this case, I should also paint the color in a less shiny way (to show it is inactive). But how do I get the status in the ListCellRenderer ?
I tried isEnabled() or component.isEnabled(), but this does not seem accessible / give me the actual state of the Combo. In the DefaultListCellRenderer, there is a query to list.isEnabled(), but that does not make sence to me (and it does not work, either).
Any ideas?
CodePudding user response:
I would go a pragmatic way and just pass the combo box to the renderer, e.g.
class ColorIcon implements Icon {
final Color color;
public ColorIcon(Color color) { this.color = color; }
@Override public void paintIcon(Component c, Graphics g, int x, int y) {
g.setColor(color);
g.fillRect(x, y, getIconWidth(), getIconHeight());
}
@Override public int getIconWidth() { return 40; }
@Override public int getIconHeight() { return 12; }
}
Color[] colors = { Color.YELLOW, Color.RED, Color.LIGHT_GRAY, Color.BLUE, Color.GREEN };
JComboBox<Color> cmb = new JComboBox<>(colors);
cmb.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(
JList<?> list, Object value, int index, boolean sel, boolean focus) {
Color color = (Color)value;
Component c = super.getListCellRendererComponent(list, value, index, sel, focus);
setIcon(new ColorIcon(cmb.isEnabled()? color: color.darker()));
setText(String.format("#x", color.getRGB() & 0xFFFFFF));
return c;
}
});
JCheckBox chk = new JCheckBox("Disabled?");
chk.addChangeListener(ev -> cmb.setEnabled(!chk.isSelected()));
JFrame f = new JFrame("Test");
f.add(cmb, BorderLayout.PAGE_START);
f.add(chk, BorderLayout.PAGE_END);
f.pack();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
This implies that you can’t share a single instance of this renderer between different combo boxes, but that’s rarely an issue. In fact, since sharing such a renderer implies that adapting the component to different parents may happen during rendering, sharing could incur higher costs than instantiating a few renderers.
You can still make the logic sharable, e.g.
private static JComboBox<Color> setupColorRender(JComboBox<Color> cmb) {
ListCellRenderer<? super Color> def = cmb.getRenderer();
ListCellRenderer<? super Color> r
= def instanceof JLabel? def: new DefaultListCellRenderer();
JLabel label = (JLabel)def;
cmb.setRenderer((list, color, index, sel, focus) -> {
Component c = r.getListCellRendererComponent(list, color, index, sel, focus);
label.setIcon(new ColorIcon(cmb.isEnabled()? color: color.darker()));
label.setText(String.format("#x", color.getRGB() & 0xFFFFFF));
return c;
});
return cmb;
}
Color[] colors = { Color.YELLOW, Color.RED, Color.LIGHT_GRAY, Color.BLUE, Color.GREEN };
JComboBox<Color> cmb = setupColorRender(new JComboBox<>(colors));
JCheckBox chk = new JCheckBox("Disabled?");
chk.addChangeListener(ev -> cmb.setEnabled(!chk.isSelected()));
JFrame f = new JFrame("Test");
f.add(cmb, BorderLayout.PAGE_START);
f.add(chk, BorderLayout.PAGE_END);
f.pack();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
This setupColorRender
method can be invoked for an arbitrary number of combo boxes. It further uses delegation instead the typical subclassing, which allows to use the original look and feel provided renderer as long as it is still a subclass of JLabel
. This approach has better visual results for some look & feels.
CodePudding user response:
A little bit tricky but possible ;)
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
/**
* <code>ComboTest</code>.
*/
public class ComboTest {
public static void main(String[] args) {
SwingUtilities.invokeLater(new ComboTest()::startUp);
}
private void startUp() {
JFrame frm = new JFrame("Combo test");
JComboBox<String> combo = new JComboBox<>(new String[] {"One", "Two", "Three"});
combo.setRenderer(new EnablementCellRenderer());
JButton b = new JButton("Toggle Enabled");
b.addActionListener(l -> {
combo.setEnabled(!combo.isEnabled());
combo.repaint();
});
frm.add(combo);
frm.add(b, BorderLayout.EAST);
frm.pack();
frm.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frm.setLocationRelativeTo(null);
frm.setVisible(true);
}
private class EnablementCellRenderer extends BasicComboBoxRenderer {
@Override
protected void paintComponent(Graphics g) {
JComboBox<?> combo = getFirstAncestorOfClass(this, JComboBox.class);
setForeground(combo.isEnabled() ? Color.BLUE : Color.GREEN);
super.paintComponent(g);
}
}
/**
* Searches for the first ancestor of the given component which is the instance of the given class.
*
* @param aStart start component to search. If the component is instance of the class - it will be returned.
* @param condition condition used to determine the component.
* @return first ancestor of the given component which is the instance of the given class. Null if no such component found.
*/
@Nullable
public static Container getFirstAncestor(Component aStart, Predicate<Component> condition) {
Container result = null;
Component base = aStart;
while ((result == null) && (base.getParent() != null || getInvoker(base) != null)) {
base = getInvoker(base) == null ? base.getParent() : getInvoker(base);
result = condition.test(base) ? (Container) base : null;
}
return result;
}
/**
* Searches for the first ancestor of the given component which is the instance of the given class.
*
* @param aStart start component to search. If the component is instance of the class - it will be returned.
* @param aClass class of component.
* @return first ancestor of the given component which is the instance of the given class. Null if no such component found.
* @param <E> class of component.
*/
@Nullable
public static <E> E getFirstAncestorOfClass(Component aStart, Class<E> aClass) {
return aClass.cast(getFirstAncestor(aStart, aClass::isInstance));
}
/**
* Gets the invoker of the given component when it's a pop-up menu.
*
* @param c component which invoker must be found.
* @return the invoker when the given component is a pop-up menu or null otherwise.
*/
private static Component getInvoker(Component c) {
return c instanceof JPopupMenu ? ((JPopupMenu) c).getInvoker() : null;
}
}