RenderBuilder.java
package swingtree;
import org.jspecify.annotations.Nullable;
import swingtree.api.Configurator;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.Color;
import java.awt.Component;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* A builder type for creating cell renderer for a list, combo box or table
* using a fluent API, typically through methods like {@link UIForList#withRenderer(Configurator)},
* {@link UIForCombo#withRenderer(Configurator)} or {@link UIForTable#withRenderer(Configurator)},
* where the builder is exposed to the configurator lambda. <p>
* A typical usage of this API may look something like this:
* <pre>{@code
* .withRenderer( it -> it
* .when( Number.class )
* .asText( cell -> cell.valueAsString().orElse("")+" km/h" )
* .when( String.class )
* .as( cell -> {
* // do component based rendering:
* cell.setRenderer( new JLabel( cell.valueAsString().orElse("") ) );
* // or do 2D graphics rendering directly:
* cell.setRenderer( g -> {
* // draw something
* g.setColor( UI.color( cell.valueAsString().orElse("") ) );
* g.fillRect( 0, 0, cell.getComponent().getWidth(), cell.getComponent().getHeight() );
* });
* })
* )
* }</pre>
* <p>
* <b>Please take a look at the <a href="https://globaltcad.github.io/swing-tree/">living swing-tree documentation</a>
* where you can browse a collection of examples demonstrating how to use the API of this class.</b>
*
* @param <C> The type of the component which is used to render the cell.
* @param <E> The type of the value of the cell.
*/
public final class RenderBuilder<C extends JComponent, E> {
private final Class<C> _componentType;
private final Class<E> _elementType;
private final Map<Class<?>, List<Configurator<CellDelegate<C, ?>>>> _rendererLookup = new LinkedHashMap<>(16);
static <E> RenderBuilder<JList<E>,E> forList(Class<E> elementType) {
return (RenderBuilder) new RenderBuilder<>(JList.class, elementType);
}
static <C extends JComboBox<E>, E> RenderBuilder<C,E> forCombo(Class<E> elementType) {
return (RenderBuilder) new RenderBuilder<>(JComboBox.class, elementType);
}
static <E> RenderBuilder<JTable,E> forTable(Class<E> elementType) {
return (RenderBuilder) new RenderBuilder<>(JTable.class, elementType);
}
private RenderBuilder(Class<C> componentType, Class<E> elementType) {
_componentType = componentType;
_elementType = elementType;
}
/**
* Use this to specify for which type of cell value you want custom rendering next.
* The object returned by this method allows you to specify how to render the values.
*
* @param valueType The type of cell value, for which you want custom rendering.
* @param <T> The type parameter of the cell value, for which you want custom rendering.
* @return The {@link RenderAs} builder API step which expects you to provide a lambda for customizing how a cell is rendered.
*/
public <T extends E> RenderAs<C, E, T> when( Class<T> valueType ) {
NullUtil.nullArgCheck(valueType, "valueType", Class.class);
return when(valueType, cell -> true);
}
/**
* Use this to specify a specific type for which you want custom rendering
* as well as a predicate which tests if a cell value should be rendered.
* The object returned by this method allows you to specify how to render the values
* using methods like {@link RenderAs#as(Configurator)} or {@link RenderAs#asText(Function)}.
*
* @param valueType The type of cell value, for which you want custom rendering.
* @param valueValidator A predicate which should return true if the cell value should be rendered.
* @param <T> The type parameter of the cell value, for which you want custom rendering.
* @return The {@link RenderAs} builder API step which expects you to provide a lambda for customizing how a cell is rendered.
*/
public <T extends E> RenderAs<C, E, T> when(
Class<T> valueType,
Predicate<CellDelegate<C, T>> valueValidator
) {
NullUtil.nullArgCheck(valueType, "valueType", Class.class);
NullUtil.nullArgCheck(valueValidator, "valueValidator", Predicate.class);
return new RenderAs<>(this, valueType, valueValidator);
}
<V> void _store(
Class valueType,
Predicate predicate,
Configurator<CellDelegate<C, V>> valueInterpreter
) {
NullUtil.nullArgCheck(valueType, "valueType", Class.class);
NullUtil.nullArgCheck(predicate, "predicate", Predicate.class);
NullUtil.nullArgCheck(valueInterpreter, "valueInterpreter", Configurator.class);
List<Configurator<CellDelegate<C, ?>>> found = _rendererLookup.computeIfAbsent(valueType, k -> new ArrayList<>());
found.add(cell -> {
if (predicate.test(cell))
return valueInterpreter.configure((CellDelegate<C, V>) cell);
else
return cell;
});
}
private class SimpleTableCellRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(
JTable table,
@Nullable Object value,
boolean isSelected,
boolean hasFocus,
final int row,
int column
) {
List<Configurator<CellDelegate<C, ?>>> interpreter = _find(value, _rendererLookup);
if (interpreter.isEmpty())
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
else {
List<String> toolTips = new ArrayList<>();
CellDelegate<JTable, Object> cell = new CellDelegate<>(
table, value, isSelected,
hasFocus, row, column, null, toolTips, null,
()->SimpleTableCellRenderer.super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
);
for ( Configurator<CellDelegate<C,?>> configurator : interpreter ) {
CellDelegate newCell = configurator.configure((CellDelegate)cell);
if ( newCell != null )
cell = newCell;
}
Component choice;
if (cell.renderer().isPresent())
choice = cell.renderer().get();
else if (cell.presentationValue().isPresent())
choice = super.getTableCellRendererComponent(table, cell.presentationValue().get(), isSelected, hasFocus, row, column);
else
choice = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (!toolTips.isEmpty() && choice instanceof JComponent)
((JComponent) choice).setToolTipText(String.join("; ", toolTips));
return choice;
}
}
}
private class SimpleListCellRenderer<O extends C> extends DefaultListCellRenderer {
private final O _component;
private SimpleListCellRenderer(O component) {
_component = Objects.requireNonNull(component);
}
@Override
public Component getListCellRendererComponent(
final JList list,
final Object value,
final int row,
final boolean isSelected,
final boolean hasFocus
) {
List<Configurator<CellDelegate<C, ?>>> interpreter = _find(value, _rendererLookup);
if (interpreter.isEmpty())
return super.getListCellRendererComponent(list, value, row, isSelected, hasFocus);
else {
List<String> toolTips = new ArrayList<>();
CellDelegate<O, Object> cell = new CellDelegate<>(
_component, value, isSelected,
hasFocus, row, 0, null, toolTips, null,
()->SimpleListCellRenderer.super.getListCellRendererComponent(list, value, row, isSelected, hasFocus)
);
for ( Configurator<CellDelegate<C,?>> configurator : interpreter ) {
CellDelegate newCell = configurator.configure((CellDelegate)cell);
if ( newCell != null )
cell = newCell;
}
Component choice;
if (cell.renderer().isPresent())
choice = cell.renderer().get();
else if (cell.presentationValue().isPresent())
choice = super.getListCellRendererComponent(list, cell.presentationValue().get(), row, isSelected, hasFocus);
else
choice = super.getListCellRendererComponent(list, value, row, isSelected, hasFocus);
if (!toolTips.isEmpty() && choice instanceof JComponent)
((JComponent) choice).setToolTipText(String.join("; ", toolTips));
return choice;
}
}
}
private static <C extends JComponent> List<Configurator<CellDelegate<C, ?>>> _find(
@Nullable Object value,
Map<Class<?>, List<Configurator<CellDelegate<C, ?>>>> rendererLookup
) {
Class<?> type = (value == null ? Object.class : value.getClass());
List<Configurator<CellDelegate<C, ?>>> cellRenderer = new ArrayList<>();
for (Map.Entry<Class<?>, List<Configurator<CellDelegate<C, ?>>>> e : rendererLookup.entrySet()) {
if (e.getKey().isAssignableFrom(type))
cellRenderer.addAll(e.getValue());
}
// We reverse the cell renderers, so that the most specific one is first
Collections.reverse(cellRenderer);
return cellRenderer;
}
DefaultTableCellRenderer getForTable() {
_addDefaultRendering();
if (JTable.class.isAssignableFrom(_componentType))
return new SimpleTableCellRenderer();
else
throw new IllegalArgumentException("Renderer was set up to be used for a JTable!");
}
/**
* Like many things in the SwingTree library, this class is
* essentially a convenient builder for a {@link ListCellRenderer}.
* This internal method actually builds the {@link ListCellRenderer} instance,
* see {@link UIForList#withRenderer(swingtree.api.Configurator)} for more details
* about how to use this class as pat of the main API.
*
* @param list The list for which the renderer is to be built.
* @return The new {@link ListCellRenderer} instance specific to the given list.
*/
ListCellRenderer<E> buildForList( C list ) {
_addDefaultRendering();
if (JList.class.isAssignableFrom(_componentType))
return (ListCellRenderer<E>) new SimpleListCellRenderer<>(list);
else if (JComboBox.class.isAssignableFrom(_componentType))
throw new IllegalArgumentException(
"Renderer was set up to be used for a JList! (not " + _componentType.getSimpleName() + ")"
);
else
throw new IllegalArgumentException(
"Renderer was set up to be used for an unknown component type! (cannot handle '" + _componentType.getSimpleName() + "')"
);
}
/**
* Like many things in the SwingTree library, this class is
* essentially a convenient builder for a {@link ListCellRenderer}.
* This internal method actually builds the {@link ListCellRenderer} instance,
* see {@link UIForList#withRenderer(swingtree.api.Configurator)} for more details
* about how to use this class as pat of the main API.
*
* @param comboBox The combo box for which the renderer is to be built.
* @return The new {@link ListCellRenderer} instance specific to the given combo box.
*/
ListCellRenderer<E> buildForCombo(C comboBox) {
_addDefaultRendering();
if (JComboBox.class.isAssignableFrom(_componentType))
return (ListCellRenderer<E>) new SimpleListCellRenderer<>(comboBox);
else
throw new IllegalArgumentException(
"Renderer was set up to be used for a JComboBox! (not " + _componentType.getSimpleName() + ")"
);
}
private void _addDefaultRendering() {
// We use the default text renderer for objects
_store(Object.class, cell -> true, _createDefaultTextRenderer(cell -> cell.valueAsString().orElse("")));
}
static <C extends JComponent, V> Configurator<CellDelegate<C, V>> _createDefaultTextRenderer(
Function<CellDelegate<C, V>, String> renderer
) {
return cell -> {
JLabel l = new JLabel(renderer.apply(cell));
l.setOpaque(true);
Color bg = null;
Color fg = null;
if ( cell.getComponent() instanceof JList ) {
JList<?> jList = (JList<?>) cell.getComponent();
bg = jList.getSelectionBackground();
fg = jList.getSelectionForeground();
if ( bg == null )
bg = UIManager.getColor("List.selectionBackground");
if ( fg == null )
fg = UIManager.getColor("List.selectionForeground");
}
if ( cell.getComponent() instanceof JTable ) {
JTable jTable = (JTable) cell.getComponent();
bg = jTable.getSelectionBackground();
fg = jTable.getSelectionForeground();
if ( bg == null )
bg = UIManager.getColor("Table.selectionBackground");
if ( fg == null )
fg = UIManager.getColor("Table.selectionForeground");
}
if ( bg == null && cell.getComponent() != null )
bg = cell.getComponent().getBackground();
if ( fg == null && cell.getComponent() != null )
fg = cell.getComponent().getForeground();
if ( bg == null )
bg = UIManager.getColor( "ComboBox.selectionBackground" );
if ( fg == null )
fg = UIManager.getColor( "ComboBox.selectionForeground" );
if ( bg == null )
bg = UIManager.getColor( "List.dropCellBackground" );
if ( fg == null )
fg = UIManager.getColor( "List.dropCellForeground" );
if ( bg == null )
bg = UIManager.getColor( "ComboBox.background" );
if ( fg == null )
fg = UIManager.getColor( "ComboBox.foreground" );
// Lastly we make sure the color is a user color, not a LaF color:
if ( bg != null ) // This is because of a weired JDK bug it seems!
bg = new Color( bg.getRGB() );
if ( fg != null )
fg = new Color( fg.getRGB() );
if (cell.isSelected()) {
if ( bg != null ) l.setBackground(bg);
if ( fg != null ) l.setForeground(fg);
}
else {
Color normalBg = Color.WHITE;
if ( cell.getComponent() != null )
normalBg = cell.getComponent().getBackground();
// We need to make sure the color is a user color, not a LaF color:
if ( normalBg != null )
normalBg = new Color( normalBg.getRGB() ); // This is because of a weired JDK bug it seems!
if ( cell.getRow() % 2 == 1 ) {
// We determine if the base color is more bright or dark,
// and then we set the foreground color accordingly
double brightness = (0.299 * normalBg.getRed() + 0.587 * normalBg.getGreen() + 0.114 * normalBg.getBlue()) / 255;
if ( brightness < 0.5 )
normalBg = brighter(normalBg);
else
normalBg = darker(normalBg);
}
if ( bg != null ) l.setBackground( normalBg );
if ( fg != null && cell.getComponent() != null )
l.setForeground( cell.getComponent().getForeground() );
}
// TODO:
//l.setEnabled(cell.getComponent().isEnabled());
//l.setFont(cell.getComponent().getFont());
Border border = null;
if ( cell.hasFocus() ) {
if ( cell.isSelected() )
border = UIManager.getBorder( "List.focusSelectedCellHighlightBorder" );
if ( border == null )
border = UIManager.getBorder( "List.focusCellHighlightBorder" );
}
else
border = UIManager.getBorder( "List.cellNoFocusBorder" );
if ( border != null ) l.setBorder(border);
return cell.withRenderer(l);
};
}
private static Color darker( Color c ) {
final double PERCENTAGE = (242*3.0)/(255*3.0);
return new Color(
(int)(c.getRed()*PERCENTAGE),
(int)(c.getGreen()*PERCENTAGE),
(int)(c.getBlue()*PERCENTAGE)
);
}
private static Color brighter( Color c ) {
final double FACTOR = (242*3.0)/(255*3.0);
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
int alpha = c.getAlpha();
int i = (int)(1.0/(1.0-FACTOR));
if ( r == 0 && g == 0 && b == 0) {
return new Color(i, i, i, alpha);
}
if ( r > 0 && r < i ) r = i;
if ( g > 0 && g < i ) g = i;
if ( b > 0 && b < i ) b = i;
return new Color(Math.min((int)(r/FACTOR), 255),
Math.min((int)(g/FACTOR), 255),
Math.min((int)(b/FACTOR), 255),
alpha);
}
}