CellConf.java
- package swingtree;
- import org.jspecify.annotations.Nullable;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import swingtree.api.Configurator;
- import swingtree.layout.Size;
- import javax.swing.*;
- import java.awt.*;
- import java.util.List;
- import java.util.*;
- import java.util.function.Consumer;
- import java.util.function.Function;
- import java.util.function.Supplier;
- /**
- * This class models the state of an individual table/tree/list/drop down cell alongside
- * various properties that a cell should have, like for example
- * the value of the cell, its position within the component
- * as well as a {@link CellConf#view()} (renderer/editor) in the form of an AWT {@link Component}
- * which may or may not be replaced or modified.
- * <br>
- * The {@link CellConf} is exposed to the {@link RenderAs#as(Configurator)}
- * method after a {@link CellBuilder#when(Class)} call as part of various
- * cell builder APIs like: <br>
- * <ul>
- * <li>{@link UIForTable#withCell(Configurator)}</li>
- * <li>{@link UIForTable#withCells(Configurator)}</li>
- * <li>{@link UIForTable#withCellForColumn(String, Configurator)} </li>
- * <li>{@link UIForTable#withCellsForColumn(String, Configurator)} </li>
- * <li>{@link UIForTable#withCellForColumn(int, Configurator)} </li>
- * <li>{@link UIForTable#withCellsForColumn(int, Configurator)} </li>
- * <li>{@link UIForList#withCell(Configurator)} </li>
- * <li>{@link UIForList#withCells(Configurator)} </li>
- * </ul>
- * When configuring your cell, you may use methods like
- * {@link CellConf#view(Component)} or {@link CellConf#renderer(Size,Consumer)}
- * to define how the cell should be rendered.
- * <p>
- * Note that the {@link CellConf#isEditing()} flag determines
- * two important modes in which this class is exposed to {@link RenderAs#as(Configurator)}.
- * If the {@code isEditing()} is true, then you are expected to configure a
- * cell editor component for the {@link CellConf#view()} property.
- * If the {@code isEditing()} is false, then you are expected to configure a simple
- * cell renderer component as the {@link CellConf#view()} property.<br>
- * Note that for each state of the {@code isEditing()} flag, the view component
- * is persisted across multiple calls to the {@link RenderAs#as(Configurator)} method.
- * <p>
- * This design allows you to easily define and continuously update both a
- * renderer and an editor for a cell on a single call to the {@link RenderAs#as(Configurator)} method, and then
- * to update the renderer or editor in every subsequent call to the same method.
- *
- * @param <V> The entry type of the entry of this {@link CellConf}.
- */
- public final class CellConf<C extends JComponent, V>
- {
- private static final Logger log = LoggerFactory.getLogger(CellConf.class);
- static <C extends JComponent, V> CellConf<C, V> of(
- @Nullable JList jListIfInvolved,
- @Nullable Component lastRenderer,
- C owner,
- @Nullable V entry,
- boolean isSelected,
- boolean hasFocus,
- boolean isEditing,
- boolean isExpanded,
- boolean isLeaf,
- int row,
- int column,
- Supplier<Component> defaultRenderSource
- ) {
- List<String> toolTips = new ArrayList<>();
- return new CellConf<>(
- jListIfInvolved,
- owner,
- entry,
- isSelected,
- hasFocus,
- isEditing,
- isExpanded,
- isLeaf,
- row,
- column,
- lastRenderer,
- toolTips,
- null,
- defaultRenderSource
- );
- }
- private final @Nullable JList<?> jListIfInvolved;
- private final C parent;
- private final @Nullable V entry;
- private final boolean isSelected;
- private final boolean hasFocus;
- private final boolean isEditing;
- private final boolean isExpanded;
- private final boolean isLeaf;
- private final int row;
- private final int column;
- private final @Nullable Component view;
- private final List<String> toolTips;
- private final @Nullable Object presentationEntry;
- private final Supplier<Component> defaultRenderSource;
- private CellConf(
- @Nullable JList jListIfInvolved,
- C host,
- @Nullable V entry,
- boolean isSelected,
- boolean hasFocus,
- boolean isEditing,
- boolean isExpanded,
- boolean isLeaf,
- int row,
- int column,
- @Nullable Component view,
- List<String> toolTips,
- @Nullable Object presentationEntry,
- Supplier<Component> defaultRenderSource
- ) {
- this.jListIfInvolved = jListIfInvolved;
- this.parent = Objects.requireNonNull(host);
- this.entry = entry;
- this.isSelected = isSelected;
- this.hasFocus = hasFocus;
- this.isEditing = isEditing;
- this.isExpanded = isExpanded;
- this.isLeaf = isLeaf;
- this.row = row;
- this.column = column;
- this.view = view;
- this.toolTips = Objects.requireNonNull(toolTips);
- this.presentationEntry = presentationEntry;
- this.defaultRenderSource = Objects.requireNonNull(defaultRenderSource);
- }
- /**
- * Returns the parent/host of this cell, i.e. the component
- * which contains this cell,
- * like a {@link JComboBox}, {@link JTable} or {@link JList}
- *
- * @return The owner of this cell, typically a table, list or combo box.
- */
- public C getHost() {
- return parent;
- }
- /**
- * Some host components (see {@link #getHost()}, use a
- * {@link JList} in their look and feel to render cells.
- * This is the case for the {@link JComboBox} component, which
- * has a drop-down popup that is rendered through an internal
- * {@link JList} which you can access through this method.<br>
- * <p>
- * But note that this {@link JList} is returned through an {@link Optional}
- * because it may not exist for other host components like a {@link JTable} or {@link JTree}.
- * In case of this cell being directly used for a {@link JList}, through
- * {@link UIForList#withCell(Configurator)} or {@link UIForList#withCells(Configurator)},
- * then both {@link #getHost()} and this return the same {@link JList} instance.
- * </p>
- *
- * @return An optional containing a {@link JList} used for rendering this cell
- * if this is called by a {@link ListCellRenderer}, and an {@link Optional#empty()} otherwise.
- */
- public Optional<JList<?>> getListView() {
- if ( parent instanceof JList )
- return Optional.of((JList<?>)parent);
- else
- return Optional.ofNullable(jListIfInvolved);
- }
- /**
- * Returns the entry of this cell, which is the data
- * that this cell represents. The entry is wrapped in an
- * {@link Optional} to indicate that the entry may be null.
- * A cell entry is typically a string, number or custom user object.
- *
- * @return An optional of the entry of this cell, or an empty optional if the entry is null.
- */
- public Optional<V> entry() {
- return Optional.ofNullable(entry);
- }
- /**
- * Returns the entry of this cell as a string. If the entry
- * is null, then an empty string is returned. This method is
- * useful when you want to display the entry of the cell as a string,
- * and do not have a special meaning assigned to null entries.
- * (Which is the preferred way to handle null entries)
- *
- * @return The entry of this cell as a string, or an empty string if the entry is null.
- * Note that the string representation of the entry is obtained by calling
- * the {@link Object#toString()} method on the entry.
- */
- public String entryAsString() {
- try {
- return entry().map(Object::toString).orElse("");
- } catch (Exception e) {
- log.error("Failed to convert entry to string!", e);
- }
- return "";
- }
- /**
- * The flag returned by this method indicates whether this cell
- * is selected or not. A cell is selected when the user interacts
- * with it, like clicking on it or navigating to it using the keyboard.
- * You may want to use this flag to change the appearance of the cell
- * when it is selected. For example, you may want to highlight the cell
- * by changing its background color.
- *
- * @return True if the cell is selected, false otherwise.
- */
- public boolean isSelected() {
- return isSelected;
- }
- /**
- * Just like any other component, a cell may have focus or not.
- * The focus is typically indicated by a border around the cell.
- * It is an important property to consider when designing your cell
- * renderer, as you may want to change the appearance of the cell
- * when it has focus.
- *
- * @return True if the cell has focus, false otherwise.
- */
- public boolean hasFocus() {
- return hasFocus;
- }
- /**
- * This method returns true if the cell is currently being edited.
- * A cell is typically edited when the user double-clicks on it
- * or presses the F2 key. When a cell is being edited, then the cell
- * renderer wrapped by this cell will be used as an editor.
- * You may want to use this flag to change the appearance of the cell
- * when it is being edited. For example, you may want to show a text
- * field instead of a label when the cell is being edited.
- *
- * @return True if the cell is being edited, false otherwise.
- * Note that you can reliably say that when this flag
- * is true, then the cell builder is being used to construct
- * or maintain an editor.
- */
- public boolean isEditing() {
- return isEditing;
- }
- /**
- * This method returns true if the cell is expanded, i.e. if it
- * is a parent cell in a {@link javax.swing.JTree}.
- * You may want to use this flag to change the appearance of the cell
- * when it is expanded.You may, for example, want to show a different
- * icon when the cell is expanded.
- *
- * @return True if the cell is expanded, false otherwise.
- */
- public boolean isExpanded() {
- return isExpanded;
- }
- /**
- * This method returns true if the cell is a leaf, i.e. if it
- * is a child cell in a {@link javax.swing.JTree}.
- * You may want to use this flag to change the appearance of the cell
- * when it is a leaf. You may, for example, want to show a different
- * icon when the cell is a leaf.
- *
- * @return True if the cell is a leaf, false otherwise.
- */
- public boolean isLeaf() {
- return isLeaf;
- }
- /**
- * Exposes a list of tool tips that should be shown when the user
- * hovers over the cell. The tool tips are strings that provide
- * additional information about the cell to the user.
- *
- * @return An unmodifiable list of tool tips that should be shown when the user hovers over the cell.
- */
- public List<String> toolTips() {
- return Collections.unmodifiableList(toolTips);
- }
- /**
- * Gives you the row index of the cell in the table, list or drop down.
- * It tells you the location of the cell in the vertical direction.
- *
- * @return The row index of the cell in the table, list or drop down.
- */
- public int row() {
- return row;
- }
- /**
- * Gives you the column index of the cell in the table, list or drop down.
- * It tells you the location of the cell in the horizontal direction.
- *
- * @return The column index of the cell in the table, list or drop down.
- */
- public int column() {
- return column;
- }
- /**
- * Returns the renderer/editor of this cell, which is the component
- * that is used to display the cell to the user. The view
- * is typically a label, text field or some other custom component.
- * It is wrapped in an {@link Optional} to clearly indicate
- * that it may be null.<br>
- * Note that in case of the {@link CellConf#isEditing()} method
- * returning true, the view component stored in this cell is used as an editor.
- * If the cell is not being edited, then the component is used as a renderer.<br>
- * Two components are persisted across multiple calls to the
- * {@link CellBuilder}s {@link RenderAs#as(Configurator)} method, one
- * for the renderer and one for the editor. (So technically there are two views)<br>
- * Also note that not all types of components are suitable to
- * be used as editors. For example, a label is not suitable to be used as an editor.
- * Instead, you should use a text field or a combo box as an editor.<br>
- * If a component is not suitable to be used as an editor, then it
- * will simply be ignored in exchange for a default editor.
- *
- * @return An optional of the view of this cell, or an empty optional if the view is null.
- * In case of the {@link CellConf#isEditing()} method returning true,
- * the component stored in this optional is used as an editor.
- * The cell will remember the renderer and editor components across multiple calls
- * to the {@link CellBuilder}s {@link RenderAs#as(Configurator)} method.
- */
- public OptionalUI<Component> view() {
- return OptionalUI.ofNullable(view);
- }
- /**
- * Allows you to configure the view of this cell by providing
- * a configurator lambda, which takes an {@link OptionalUI} of the
- * current renderer and returns a (potentially updated) {@link OptionalUI}
- * of the new renderer. <br>
- * The benefit of using this method is that you can easily initialize
- * the renderer with a new component through the {@link OptionalUI#orGetUi(Supplier)}
- * method, and then update it in every refresh coll inside the
- * {@link OptionalUI#update(java.util.function.Function)} method. <br>
- * This may look like the following:
- * <pre>{@code
- * UI.table()
- * .withCell(cell -> cell
- * .updateView( comp -> comp
- * .update( r -> { r.setText(cell.entryAsString()); return r; } )
- * .orGetUi( () -> UI.textField(cell.entryAsString()).withBackground(Color.CYAN) )
- * )
- * )
- * // ...
- * }</pre>
- * In this example, the view is initialized with a text field
- * if it is not present, and then the text field is continuously updated
- * with the entry of the cell. <br>
- *
- * @param configurator The {@link Configurator} lambda which takes an {@link OptionalUI}
- * of the current view and returns a (potentially updated or initialized)
- * {@link OptionalUI} of the new view.
- * @return An updated cell delegate object with the new view.
- * If the configurator returns an empty optional, then the view
- * of the cell will be reset to null.
- */
- public CellConf<C,V> updateView( Configurator<OptionalUI<Component>> configurator ) {
- OptionalUI<Component> newRenderer = OptionalUI.empty();
- try {
- newRenderer = configurator.configure(view());
- } catch (Exception e) {
- log.error("Failed to configure view!", e);
- }
- return _withRenderer(newRenderer.orElseNullable(null));
- }
- /**
- * Creates an updated cell delegate object with the given component
- * as the view (renderer/editor) of the cell. view is the
- * component that is used to render the cell to the user. It is
- * typically a label, text field or some other custom component.
- * <br>
- * Note that in case of the {@link CellConf#isEditing()} method
- * returning true, this {@link CellConf} is used for constructing
- * or maintaining an editor. If the cell is not being edited, then
- * this {@link CellConf} is used for rendering.<br>
- * Either way, the component is memorized across multiple calls to the
- * {@link CellBuilder}s {@link RenderAs#as(Configurator)} method. <br>
- *
- * A typical usage of this method may look something like this:
- * <pre>{@code
- * UI.table()
- * .withCell(cell -> cell
- * .view(new JLabel("Hello, World!" + cell.row()) )
- * )
- * // ...
- * }</pre>
- * But keep in mind that in this example the label will be recreated
- * on every refresh call, which is not very efficient. It is better
- * to use the {@link CellConf#updateView(Configurator)} method to
- * initialize the view once and then update it in every refresh call.
- *
- * @param component The component to be used as the view of the cell.
- * @return An updated cell delegate object with the new view to
- * serve as the renderer/editor of the cell.
- */
- public CellConf<C, V> view( Component component ) {
- return _withRenderer(component);
- }
- /**
- * Creates an updated cell delegate object with the supplied cell
- * size and painter as the view (renderer/editor) of the cell.
- * The painter is a lambda that takes a {@link Graphics2D} object
- * and paints the cell with it.
- * This method is useful when you want to
- * create a custom cell renderer that paints the cell in a
- * specific way. For example, you may want to paint the cell
- * with a gradient background or a custom border.
- * <br>
- * Note that in case of the {@link CellConf#isEditing()} method
- * returning true, this {@link CellConf} is used for constructing
- * or maintaining an editor. If the cell is not being edited, then
- * this {@link CellConf} is used for rendering.<br>
- * Either way, the component is memorized across multiple calls to the
- * {@link CellBuilder}s {@link RenderAs#as(Configurator)} method.
- *
- * @param cellSize The minimum and preferred size of the cell to be painted.
- * @param painter The lambda that paints the cell with a {@link Graphics2D} object.
- * @return An updated cell delegate object with the new view to
- * serve as the renderer/editor of the cell.
- */
- public CellConf<C, V> renderer( Size cellSize, Consumer<Graphics2D> painter ) {
- Component component = new Component() {
- @Override
- public void paint(Graphics g) {
- super.paint(g);
- painter.accept((Graphics2D) g);
- }
- /*
- The following methods are overridden as a performance measure
- to prune code-paths are often called in the case of renders
- but which we know are unnecessary. Great care should be taken
- when writing your own renderer to weigh the benefits and
- drawbacks of overriding methods like these.
- */
- @Override
- public boolean isOpaque() {
- Color back = getBackground();
- Component p = getParent();
- if (p != null) {
- p = p.getParent();
- }
- // p should now be the JTable.
- boolean colorMatch = (back != null) && (p != null) &&
- back.equals(p.getBackground()) &&
- p.isOpaque();
- return !colorMatch && super.isOpaque();
- }
- @Override
- public void invalidate() {}
- @Override
- public void validate() {}
- @Override
- public void revalidate() {}
- @Override
- public void repaint(long tm, int x, int y, int width, int height) {}
- @Override
- public void repaint() {}
- @Override
- public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { }
- };
- component.setMinimumSize(cellSize.toDimension());
- component.setPreferredSize(cellSize.toDimension());
- component.setSize(cellSize.toDimension());
- return _withRenderer(component);
- }
- /**
- * Creates an updated cell delegate object with the default cell view / renderer
- * component based on the {@link javax.swing.DefaultListCellRenderer},
- * {@link javax.swing.table.DefaultTableCellRenderer} and {@link javax.swing.tree.DefaultTreeCellRenderer}
- * classes.
- *
- * @return An updated cell delegate object with the default view component.
- * This will override any custom view that was previously specified.
- */
- public CellConf<C, V> viewDefault() {
- try {
- return this.view(this.defaultRenderSource.get());
- } catch (Exception e) {
- log.error("Failed to create default renderer!", e);
- }
- return this;
- }
- public CellConf<C, V> _withRenderer(@Nullable Component component ) {
- return new CellConf<>(
- jListIfInvolved,
- parent,
- entry,
- isSelected,
- hasFocus,
- isEditing,
- isExpanded,
- isLeaf,
- row,
- column,
- component,
- toolTips,
- presentationEntry,
- defaultRenderSource
- );
- }
- /**
- * Creates a cell with an additional tool tip to be shown
- * when the user hovers over the cell. The tool tips are strings
- * that provide additional information about the cell to the user.
- *
- * @param toolTip The tool tip to be added to the list of tool tips.
- * @return An updated cell delegate object with the new tool tip.
- */
- public CellConf<C, V> toolTip( String toolTip ) {
- ArrayList<String> newToolTips = new ArrayList<>(toolTips);
- newToolTips.add(toolTip);
- return new CellConf<>(
- jListIfInvolved,
- parent,
- entry,
- isSelected,
- hasFocus,
- isEditing,
- isExpanded,
- isLeaf,
- row,
- column,
- view,
- newToolTips,
- presentationEntry,
- defaultRenderSource
- );
- }
- /**
- * The presentation entry is the first choice of the
- * default cell view to be used for rendering and presentation
- * to the user. If it does not exist then the regular
- * cell entry is used for rendering by the default view.
- * Note that if you supply your own custom view/renderer component,
- * then the presentation entry is ignored.
- *
- * @return An optional of the presentation entry.
- * It may be an empty optional if no presentation entry was specified.
- */
- public Optional<Object> presentationEntry() {
- return Optional.ofNullable(presentationEntry);
- }
- /**
- * The presentation entry is the first choice of the
- * default cell view to be used for rendering and presentation
- * to the user.
- * By default, this entry is null,
- * in which case it does not exist the regular
- * cell entry is used for rendering by the default view.
- * Note that if you supply a presentation value, then SwingTree
- * will try to apply this value to the view component.
- * (Which includes the editor and renderer components)
- *
- * @param toBeShown The object which should be used by the renderer
- * to present to the user, typically a String.
- * @return An updated cell delegate object with the new presentation entry.
- */
- public CellConf<C, V> presentationEntry( @Nullable Object toBeShown ) {
- return new CellConf<>(
- jListIfInvolved,
- parent,
- entry,
- isSelected,
- hasFocus,
- isEditing,
- isExpanded,
- isLeaf,
- row,
- column,
- view,
- toolTips,
- toBeShown,
- defaultRenderSource
- );
- }
- /**
- * The presentation entry is the first choice of the
- * default cell view to be used for rendering and presentation
- * to the user. A common use case is to convert the cell entry to
- * a presentation entry that is more suitable for the default view.
- * This method allows you to convert the cell entry to a presentation
- * entry by applying a function to it. The function takes the cell entry
- * as an argument and returns the presentation entry.
- * Note that if you supply a presentation value, then SwingTree
- * will try to apply this value to the view component.
- * (Which includes the editor and renderer components)
- *
- * @param presenter The function that converts the cell entry to a presentation entry.
- * @return An updated cell delegate object with the new presentation entry.
- * @throws NullPointerException If the presenter function is null.
- */
- public CellConf<C, V> entryToPresentation( Function<@Nullable V, @Nullable Object> presenter ) {
- Objects.requireNonNull(presenter);
- @Nullable V entry = this.entry;
- try {
- return presentationEntry(presenter.apply(entry));
- } catch (Exception e) {
- log.error("Failed to convert entry to presentation entry!", e);
- }
- return this;
- }
- }