CellBuilder.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.api.Configurator;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.CellEditorListener;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import java.awt.*;
import java.util.List;
import java.util.*;
import java.util.function.BiConsumer;
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#withCells(Configurator)},
* {@link UIForCombo#withCells(Configurator)} or {@link UIForTable#withCells(Configurator)},
* where the builder is exposed to the configurator lambda. <p>
* A typical usage of this API may look something like this:
* <pre>{@code
* .withCells( it -> it
* .when( Number.class )
* .asText( cell -> cell.entryAsString()+" km/h" )
* .when( String.class )
* .as( cell -> {
* // do component based rendering:
* return cell.view( new JLabel( cell.entryAsString() ) );
* // or do 2D graphics rendering directly:
* return cell.renderer(Size.of(200,100), g -> {
* // draw something
* g.setColor( UI.color( cell.entryAsString() ) );
* g.fillRect( 0, 0, 200, 100 );
* });
* })
* )
* }</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 CellBuilder<C extends JComponent, E> {
private static final Logger log = org.slf4j.LoggerFactory.getLogger(CellBuilder.class);
private final Class<C> _componentType;
private final Class<E> _elementType;
private final Map<Class<?>, CellView<C>> _rendererLookup = new LinkedHashMap<>(16);
private static class CellView<C extends JComponent> {
@Nullable Component _renderer = null;
@Nullable Component _editor = null;
final List<Configurator<CellConf<C, ?>>> _configurators = new ArrayList<>();
}
static <E> CellBuilder<JList<E>,E> forList(Class<E> elementType) {
return (CellBuilder) new CellBuilder<>(JList.class, elementType);
}
static <C extends JComboBox<E>, E> CellBuilder<C,E> forCombo(Class<E> elementType) {
return (CellBuilder) new CellBuilder<>(JComboBox.class, elementType);
}
static <E> CellBuilder<JTable,E> forTable(Class<E> elementType) {
return (CellBuilder) new CellBuilder<>(JTable.class, elementType);
}
private CellBuilder(Class<C> componentType, Class<E> elementType) {
_componentType = componentType;
_elementType = elementType;
}
private @Nullable Component findRenderer(@Nullable Object value) {
Class type = (value == null ? Object.class : value.getClass());
return _rendererLookup.computeIfAbsent(type, k -> new CellView<>())._renderer;
}
private void safeRenderer(@Nullable Object value, @Nullable Component renderer) {
Class type = (value == null ? Object.class : value.getClass());
_rendererLookup.computeIfAbsent(type, k -> new CellView<>())._renderer = renderer;
}
private @Nullable Component findEditor(@Nullable Object value) {
Class type = (value == null ? Object.class : value.getClass());
return _rendererLookup.computeIfAbsent(type, k -> new CellView<>())._editor;
}
private void safeEditor(@Nullable Object value, @Nullable Component editor) {
Class type = (value == null ? Object.class : value.getClass());
_rendererLookup.computeIfAbsent(type, k -> new CellView<>())._editor = editor;
}
private void _checkTypeValidity( @Nullable Object encounteredValue ) {
if ( encounteredValue != null ) {
if ( !_elementType.isAssignableFrom(encounteredValue.getClass()) )
log.debug(
"Encountered an unusual cell entry in component '"+_componentType.getSimpleName()+"'. " +
"Expected type '"+_elementType.getSimpleName()+"', but got '"+encounteredValue.getClass().getSimpleName()+"'."
);
}
}
/**
* 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<CellConf<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<CellConf<C, V>> valueInterpreter
) {
NullUtil.nullArgCheck(valueType, "valueType", Class.class);
NullUtil.nullArgCheck(predicate, "predicate", Predicate.class);
NullUtil.nullArgCheck(valueInterpreter, "valueInterpreter", Configurator.class);
List<Configurator<CellConf<C, ?>>> found = _rendererLookup.computeIfAbsent(valueType, k -> new CellView<>())._configurators;
found.add(cell -> {
if (predicate.test(cell))
return valueInterpreter.configure((CellConf<C, V>) cell);
else
return cell;
});
}
public <T extends JComponent> Component _updateAndGetComponent(
Function<@Nullable Object, Component> defaultRenderer,
BiConsumer<@Nullable Component, CellConf<?,?>> saveComponent,
CellConf<T, Object> cell
) {
@Nullable Object value = cell.entry().orElse(null);
List<Configurator<CellConf<C, ?>>> interpreter = _find(value, _rendererLookup);
if ( interpreter.isEmpty() )
return defaultRenderer.apply(value);
else {
/*
If a view is persisted from previous rendering, initialize with what is most
like what the user would expect. This is however mainly to avoid
rendering state left over from previous rendering.
*/
cell = _initializeViewIfPresent(cell);
for ( Configurator<CellConf<C,?>> configurator : interpreter ) {
CellConf newCell = cell;
try {
newCell = configurator.configure(newCell);
} catch (Exception e) {
log.error(
"Failed to configure cell renderer for " +
"component '"+cell.getHost().getClass().getSimpleName()+"'.",
e
);
}
if ( newCell != null )
cell = newCell;
}
Component choice;
Optional<Object> presentationEntry = cell.presentationEntry();
if (cell.view().isPresent()) {
choice = cell.view().orElseThrow();
saveComponent.accept(choice, cell);
} else if (presentationEntry.isPresent()) {
choice = defaultRenderer.apply(presentationEntry.get());
saveComponent.accept(null, cell);
} else {
choice = defaultRenderer.apply(value);
saveComponent.accept(null, cell);
}
if (!cell.toolTips().isEmpty() && choice instanceof JComponent)
((JComponent) choice).setToolTipText(String.join("; ", cell.toolTips()));
return choice;
}
}
private CellConf _initializeViewIfPresent(CellConf<?, Object> cell) {
if ( cell.view().isPresent() ) {
Component view = cell.view().orElseThrow();
@Nullable Object value = cell.entry().orElse(null);
view.setEnabled(true);
view.setVisible(true);
if ( view instanceof AbstractButton ) {
AbstractButton button = (AbstractButton) view;
button.setSelected(false);
if ( value instanceof Boolean )
button.setSelected((Boolean) value);
else if ( value instanceof String )
button.setText((String) value);
else if ( value instanceof Icon )
button.setIcon((Icon) value);
} else if ( view instanceof JComboBox ) {
JComboBox<?> comboBox = (JComboBox<?>) view;
if ( value != null )
comboBox.setSelectedItem(value);
} else if ( view instanceof JTextComponent) {
JTextComponent textField = (JTextComponent) view;
if ( value != null )
textField.setText(value.toString());
} else if ( view instanceof JLabel ) {
JLabel label = (JLabel) view;
if ( value != null )
label.setText(value.toString());
}
}
return cell;
}
class SimpleTableCellRenderer implements TableCellRenderer, TableCellEditor, TreeCellRenderer, TreeCellEditor
{
private final DefaultTableCellRenderer _defaultRenderer = new DefaultTableCellRenderer();
private final DefaultTreeCellRenderer _defaultTreeRenderer = new DefaultTreeCellRenderer();
private final InternalCellEditor _basicEditor;
SimpleTableCellRenderer(Class<? extends JComponent> hostType) {
_basicEditor = new InternalCellEditor(hostType);
}
public @Nullable Component getEditorComponent() {
return _basicEditor.getComponent();
}
private @Nullable Component _loadEditor(@Nullable Object value) {
@Nullable Component editor = findEditor(value);
if ( editor != null )
editor = _setEditorComponent(editor);
return editor;
}
private @Nullable Component _setEditorComponent(@Nullable Component editor) {
if ( _basicEditor.getComponent() != editor ) {
if (editor instanceof JCheckBox) {
_basicEditor.setEditor((JCheckBox) editor);
} else if (editor instanceof JComboBox) {
_basicEditor.setEditor((JComboBox<?>) editor);
} else if (editor instanceof JTextField) {
_basicEditor.setEditor((JTextField) editor);
}
}
return _basicEditor.getComponent();
}
private void _setEditor(
@Nullable Component newEdior,
@Nullable Object entryFromModel,
CellConf<?,?> currentCell
) {
newEdior = _setEditorComponent(newEdior);
safeEditor(entryFromModel, newEdior);
try {
// Apply user values to editor:
Optional<Object> presentationEntry = currentCell.presentationEntry();
if ( presentationEntry.isPresent() )
_basicEditor.setEntry(presentationEntry.orElse(null), entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
else if ( currentCell.view().isEmpty() )
_basicEditor.setEntry(currentCell.entry().orElse(null), entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
} catch (Exception e) {
log.error("Failed to populate cell editor!", e);
}
}
private void _setRenderer(
@Nullable Component newRenderer,
@Nullable Object entryFromModel,
CellConf<?,?> currentCell
) {
safeRenderer(entryFromModel, newRenderer);
try {
Optional<Object> presentationEntry = currentCell.presentationEntry();
if ( presentationEntry.isPresent() || currentCell.view().isEmpty() ) {
@Nullable Object toBePresented = presentationEntry.orElse(currentCell.entry().orElse(null));
if ( newRenderer instanceof AbstractButton ) {
AbstractButton button = (AbstractButton) newRenderer;
if ( toBePresented instanceof Boolean )
button.setSelected((Boolean) toBePresented);
else if ( toBePresented instanceof String )
button.setText((String) toBePresented);
else if ( toBePresented instanceof Icon )
button.setIcon((Icon) toBePresented);
} else if ( newRenderer instanceof JComboBox ) {
JComboBox<?> comboBox = (JComboBox<?>) newRenderer;
comboBox.setSelectedItem(toBePresented);
} else if ( newRenderer instanceof JTextComponent ) {
JTextComponent textField = (JTextComponent) newRenderer;
textField.setText(toBePresented == null ? "" : toBePresented.toString());
} else if ( newRenderer instanceof JLabel ) {
JLabel label = (JLabel) newRenderer;
label.setText(toBePresented == null ? "" : toBePresented.toString());
}
}
} catch (Exception e) {
log.error("Failed to populate cell editor!", e);
}
}
private Component _fit( JTable table, int row, int column, Component view ) {
try {
boolean isDefaultEditor = _basicEditor.getComponent() == view && _basicEditor.hasDefaultComponent();
boolean isDefaultRenderer = view instanceof InternalLabelForRendering ||
view.getClass() == DefaultListCellRenderer.class ||
view instanceof DefaultTableCellRenderer ||
view instanceof DefaultTreeCellRenderer;
if ( !isDefaultRenderer && !isDefaultEditor ) {
/*
If you want the table to fit the cell size to the content,
then you have to use a custom view / editor!
*/
Dimension minSize = view.getMinimumSize();
TableColumn currentColumn = table.getColumnModel().getColumn(column);
if (currentColumn.getMinWidth() < minSize.width)
currentColumn.setMinWidth(minSize.width);
if (table.getRowHeight(row) < minSize.height)
table.setRowHeight(row, minSize.height);
}
} catch (Exception e) {
log.error("Failed to fit cell size", e);
}
return view;
}
@Override
public Component getTableCellRendererComponent(
final JTable table,
final @Nullable Object entryFromModel,
final boolean isSelected,
final boolean hasFocus,
final int row,
final int column
) {
_checkTypeValidity(entryFromModel);
return _fit(table, row, column,
_updateAndGetComponent(
localEntry -> _defaultRenderer.getTableCellRendererComponent(table, localEntry, isSelected, hasFocus, row, column),
(choice, newRenderer) -> _setRenderer(choice, entryFromModel, newRenderer),
CellConf.of(
null, findRenderer(entryFromModel),
table, entryFromModel, isSelected, hasFocus, false, false, false, row, column,
() -> _defaultRenderer.getTableCellRendererComponent(table, entryFromModel, isSelected, hasFocus, row, column)
)
)
);
}
@Override
public Component getTableCellEditorComponent(
final JTable table,
final @Nullable Object entryFromModel,
final boolean isSelected,
final int row,
final int column
) {
_checkTypeValidity(entryFromModel);
_basicEditor.ini(table, row, column);
_basicEditor.updateForTable(table, column);
_basicEditor.setEntry(entryFromModel, entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
return _fit(table, row, column,
_updateAndGetComponent(
localEntry -> _basicEditor.getTableCellEditorComponent(table, localEntry, isSelected, row, column),
(choice, newEditor) -> _setEditor(choice, entryFromModel, newEditor),
CellConf.of(
null, _loadEditor(entryFromModel),
table, entryFromModel, isSelected, true, true, false, false, row, column,
() -> _basicEditor.getTableCellEditorComponent(table, entryFromModel, isSelected, row, column)
)
)
);
}
@Override
public Component getTreeCellRendererComponent(
final JTree tree,
final @Nullable Object entryFromModel,
final boolean selected,
final boolean expanded,
final boolean leaf,
final int row,
final boolean hasFocus
) {
_checkTypeValidity(entryFromModel);
String entryAsString = tree.convertValueToText(entryFromModel, selected, expanded, leaf, row, false);
_basicEditor.ini(tree, row, 0);
_basicEditor.setEntry(entryAsString, entryFromModel, entryFromModel == null ? Object.class : entryFromModel.getClass());
return _updateAndGetComponent(
localValue -> _defaultTreeRenderer.getTreeCellRendererComponent(tree, localValue, selected, expanded, leaf, row, hasFocus),
(choice, newRenderer) -> _setRenderer(choice, entryFromModel, newRenderer),
CellConf.of(
null, findRenderer(entryFromModel),
tree, entryFromModel, selected, hasFocus, false, expanded, leaf, row, 0,
() -> _defaultTreeRenderer.getTreeCellRendererComponent(tree, entryFromModel, selected, expanded, leaf, row, hasFocus)
)
);
}
@Override
public Component getTreeCellEditorComponent(
final JTree tree,
final @Nullable Object entryFromModel,
final boolean isSelected,
final boolean expanded,
final boolean leaf,
final int row
) {
_checkTypeValidity(entryFromModel);
_basicEditor.ini(tree, row, 0);
return _updateAndGetComponent(
localEntry -> _basicEditor.getTreeCellEditorComponent(tree, localEntry, isSelected, expanded, leaf, row),
(choice, newEditor) -> _setEditor(choice, entryFromModel, newEditor),
CellConf.of(
null, _loadEditor(entryFromModel),
tree, entryFromModel, isSelected,
true, true, expanded, leaf, row, 0,
() -> _basicEditor.getTreeCellEditorComponent(tree, entryFromModel, isSelected, expanded, leaf, row)
)
);
}
@Override
public @Nullable Object getCellEditorValue() {
return _basicEditor.getCellEditorValue();
}
@Override
public boolean isCellEditable(EventObject anEvent) {
return _basicEditor.isCellEditable(anEvent);
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
return _basicEditor.shouldSelectCell(anEvent);
}
@Override
public boolean stopCellEditing() {
return _basicEditor.stopCellEditing();
}
@Override
public void cancelCellEditing() {
_basicEditor.cancelCellEditing();
}
@Override
public void addCellEditorListener(CellEditorListener l) {
_basicEditor.addCellEditorListener(l);
}
@Override
public void removeCellEditorListener(CellEditorListener l) {
_basicEditor.removeCellEditorListener(l);
}
}
private class SimpleListCellRenderer<O extends C> implements ListCellRenderer<Object>
{
private final O _component;
private final ListCellRenderer<Object> _defaultRenderer;
private SimpleListCellRenderer(O component) {
_component = Objects.requireNonNull(component);
if ( component instanceof JComboBox )
_defaultRenderer = new BasicComboBoxRenderer.UIResource();
else
_defaultRenderer = new DefaultListCellRenderer.UIResource();
}
@Override
public Component getListCellRendererComponent(
final JList list,
final Object value,
final int row,
final boolean isSelected,
final boolean hasFocus
) {
_checkTypeValidity(value);
List<Configurator<CellConf<C, ?>>> interpreter = _find(value, _rendererLookup);
if (interpreter.isEmpty())
return _defaultRenderer.getListCellRendererComponent(list, value, row, isSelected, hasFocus);
else {
CellConf<O, Object> cell = CellConf.of(
list,
findRenderer(value),
_component, value, isSelected,
hasFocus, false, false, false, row, 0,
()->_defaultRenderer.getListCellRendererComponent(list, value, row, isSelected, hasFocus)
);
for ( Configurator<CellConf<C,?>> configurator : interpreter ) {
CellConf newCell = cell;
try {
newCell = configurator.configure(newCell);
} catch (Exception e) {
log.error(
"Failed to configure cell renderer for " +
"component '"+_component.getClass().getSimpleName()+"'.",
e
);
}
if ( newCell != null )
cell = newCell;
}
Component choice;
Optional<Object> presentationEntry = cell.presentationEntry();
if (cell.view().isPresent()) {
choice = cell.view().orElseThrow();
safeRenderer(value, choice);
} else if (presentationEntry.isPresent()) {
choice = _defaultRenderer.getListCellRendererComponent(list, presentationEntry.get(), row, isSelected, hasFocus);
safeRenderer(value, null);
} else {
choice = _defaultRenderer.getListCellRendererComponent(list, value, row, isSelected, hasFocus);
safeRenderer(value, null);
}
if (!cell.toolTips().isEmpty() && choice instanceof JComponent)
((JComponent) choice).setToolTipText(String.join("; ", cell.toolTips()));
return choice;
}
}
Optional<ComboBoxEditor> establishEditor() {
if ( !( _component instanceof JComboBox ) )
return Optional.empty();
JComboBox<?> comboBox = (JComboBox<?>) _component;
CellConf<JComboBox<?>, Object> cell = CellConf.of(
null, null, comboBox, null, false, false, true, false, false, 0, 0, () -> null
);
List<Configurator<CellConf<C, ?>>> interpreter = _findAll(_rendererLookup);
if (interpreter.isEmpty())
return Optional.empty();
else {
for ( Configurator<CellConf<C,?>> configurator : interpreter ) {
CellConf<JComboBox<?>,Object> newCell = cell;
try {
newCell = configurator.configure((CellConf)newCell);
} catch (Exception e) {
log.error(
"Failed to establish cell editor through cell configurator " +
"for component '"+_component.getClass().getSimpleName()+"'.",
e
);
}
try {
if ( newCell != null )
cell = newCell.updateView(v -> v.update(c -> c instanceof JTextField ? c : null ) );
} catch (Exception e) {
log.error(
"Failed to establish cell editor through cell configurator " +
"for component '"+_component.getClass().getSimpleName()+"'.",
e
);
}
}
if (!cell.view().isPresent())
return Optional.empty();
Component choice = cell.view().orElseThrow();
if ( !(choice instanceof JTextField) )
return Optional.empty();
JTextField textField = (JTextField) choice;
return Optional.of(new InternalComboBoxCellEditor(textField));
}
}
}
private static <C extends JComponent> List<Configurator<CellConf<C, ?>>> _find(
@Nullable Object value,
Map<Class<?>, CellView<C>> rendererLookup
) {
Class<?> type = (value == null ? Object.class : value.getClass());
List<Configurator<CellConf<C, ?>>> cellRenderer = new ArrayList<>();
for (Map.Entry<Class<?>, CellView<C>> e : rendererLookup.entrySet()) {
if (e.getKey().isAssignableFrom(type))
cellRenderer.addAll(e.getValue()._configurators);
}
// We reverse the cell renderers, so that the most un-specific one is first
Collections.reverse(cellRenderer);
return cellRenderer;
}
private static <C extends JComponent> List<Configurator<CellConf<C,?>>> _findAll(
Map<Class<?>, CellView<C>> rendererLookup
) {
List<Configurator<CellConf<C, ?>>> cellRenderer = new ArrayList<>();
for (CellView<C> e : rendererLookup.values()) {
cellRenderer.addAll(e._configurators);
}
// We reverse the cell renderers, so that the most un-specific one is first
Collections.reverse(cellRenderer);
return cellRenderer;
}
SimpleTableCellRenderer getForTable() {
_addDefaultRendering();
if (JTable.class.isAssignableFrom(_componentType)) {
SimpleTableCellRenderer renderer = new SimpleTableCellRenderer(_componentType);
return renderer;
} else
throw new IllegalArgumentException("Renderer was set up to be used for a JTable!");
}
TreeCellRenderer getForTree() {
_addDefaultRendering();
if (JTree.class.isAssignableFrom(_componentType))
return new SimpleTableCellRenderer(_componentType);
else
throw new IllegalArgumentException("Renderer was set up to be used for a JTree!");
}
/**
* 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#withCell(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.
*/
void buildForList( C list ) {
_addDefaultRendering();
if (JList.class.isAssignableFrom(_componentType)) {
SimpleListCellRenderer<C> renderer = new SimpleListCellRenderer<>(list);
JList<E> jList = (JList<E>) list;
jList.setCellRenderer(renderer);
} 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() + "')"
);
}
void buildForCombo(C comboBox, boolean establishEditorAlso) {
_addDefaultRendering();
if (JComboBox.class.isAssignableFrom(_componentType)) {
SimpleListCellRenderer<C> renderer = new SimpleListCellRenderer<>(comboBox);
JComboBox<E> combo = (JComboBox<E>) comboBox;
combo.setRenderer(renderer);
if (establishEditorAlso) {
renderer.establishEditor().ifPresent(combo::setEditor);
}
} 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.entryAsString()));
}
static class InternalLabelForRendering extends DefaultListCellRenderer {
InternalLabelForRendering(String text) {
setText(text);
setOpaque(true);
}
}
static <C extends JComponent, V> Configurator<CellConf<C, V>> _createDefaultTextRenderer(
Function<CellConf<C, V>, String> renderer
) {
Function<CellConf<C, V>, String> exceptionSafeRenderer = cell -> {
try {
return renderer.apply(cell);
} catch (Exception e) {
log.error("Failed to convert cell to displayable String!", e);
return "";
}
};
return cell -> {
if ( cell.isEditing() )
return cell;
Component existing = cell.view().orElseNullable(null);
InternalLabelForRendering l = (existing instanceof InternalLabelForRendering) ? (InternalLabelForRendering) existing : null;
if ( existing != null && l == null )
return cell; // The user has defined a custom renderer, so we don't touch it.
if ( l == null )
l = new InternalLabelForRendering(exceptionSafeRenderer.apply(cell));
else
l.setText(exceptionSafeRenderer.apply(cell));
Color bg = null;
Color fg = null;
if ( cell.getHost() instanceof JComboBox && cell.getListView().isPresent() ) {
JList<?> list = cell.getListView().get();
if (cell.isSelected()) {
bg = list.getSelectionBackground();
fg = list.getSelectionForeground();
}
else {
bg = list.getBackground();
fg = list.getForeground();
}
} else if ( cell.getHost() instanceof JList ) {
JList<?> jList = (JList<?>) cell.getHost();
bg = jList.getSelectionBackground();
fg = jList.getSelectionForeground();
if ( bg == null )
bg = UIManager.getColor("List.selectionBackground");
if ( fg == null )
fg = UIManager.getColor("List.selectionForeground");
} else if ( cell.getHost() instanceof JTable ) {
JTable jTable = (JTable) cell.getHost();
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 )
bg = cell.getHost().getBackground();
if ( fg == null )
fg = cell.getHost().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 ) _setBackgroundColor(l, bg);
if ( fg != null ) _setForegroundColor(l, fg);
}
else {
Color normalBg = cell.getHost().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.row() % 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 )
_setBackgroundColor( l, normalBg );
if ( fg != null )
_setForegroundColor( l, cell.getHost().getForeground() );
}
// TODO:
//l.setFont(cell.getHost().getFont()); // Is this a good idea?
if ( l.isEnabled() != cell.getHost().isEnabled() )
l.setEnabled(cell.getHost().isEnabled());
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 && border != l.getBorder() )
l.setBorder(border);
return cell.view(l);
};
}
private static void _setBackgroundColor( JComponent comp, @Nullable Color color ) {
if ( color == null ) {
if ( comp.isBackgroundSet() )
comp.setBackground(null);
else
return; // Already null!
}
else
if ( !Objects.equals(comp.getBackground(), color) )
comp.setBackground( color );
}
private static void _setForegroundColor( JComponent comp, @Nullable Color color ) {
if ( color == null ) {
if ( comp.isForegroundSet() )
comp.setForeground(null);
else
return; // Already null!
}
else
if ( !Objects.equals(comp.getForeground(), color) )
comp.setForeground( color );
}
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);
}
}