ScrollableComponentDelegate.java

package swingtree;

import swingtree.api.Configurator;
import swingtree.api.ScrollIncrementSupplier;
import swingtree.layout.Bounds;
import swingtree.layout.Size;

import javax.swing.JComponent;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import java.awt.Component;
import java.util.Objects;

/**
 * This class is an immutable builder which defines the {@link javax.swing.Scrollable} behavior of a component
 * within a {@link JScrollPane}.
 * So it is in essence a config object that provides information to a scrolling container
 * like {@link JScrollPane}.
 * <p>
 * Instances of this class are exposed to the {@link swingtree.api.Configurator} lambda
 * of the {@link UI#scrollPane(Configurator)} factory method, where you can configure the scrollable behavior
 * according to your needs. <br>
 * This includes setting the preferred size, unit increment, block increment, and whether the component should
 * fit the width or height of the viewport. <br>
 * Here an example demonstrating how the API of this class
 * is typically used:
 * <pre>{@code
 * UI.panel()
 * .withBorderTitled("Scrollable Panel")
 * .add(
 *     UI.scrollPane(conf -> conf
 *         .prefSize(400, 300)
 *         .unitIncrement(20)
 *         .blockIncrement(50)
 *         .fitWidth(true)
 *         .fitHeight(false)
 *     )
 * )
 * }</pre>
 * <p>
 * Note that the provided {@link Configurator} will be called
 * for every call to a method of the underlying {@link javax.swing.Scrollable}
 * component implementation, so the settings you provide can
 * also change dynamically based on the context captured by the lambda.<br>
 * <p>
 * Also note that this configuration object also exposes some context
 * information you may find useful when defining its properties like {@link #fitWidth(boolean)},
 * {@link #fitHeight(boolean)}, {@link #unitIncrement(int)}, and so on...<br>
 * Like for example the current {@link #view()}, which implements the {@link javax.swing.Scrollable}
 * and wraps your {@link #content} component. You can also access the {@link #viewport()} as
 * well as the overarching {@link #scrollPane()} overall!<br>
 * Again, the configurator you pass to {@link UI#scrollPane(Configurator)} will be
 * called eagerly, so everything you define in there will be completely dynamic,
 * which means your scroll behaviour can dynamically react to the involved components.
 */
public final class ScrollableComponentDelegate
{
    static ScrollableComponentDelegate of(
        JScrollPane scrollPane,
        JComponent content,
        Size preferredSize,
        int unitIncrement, int blockIncrement
    ) {
        Component view     = scrollPane.getViewport().getView();
        JViewport viewport = scrollPane.getViewport();
        boolean fitWidth  = viewport.getWidth()  >  view.getPreferredSize().width;
        boolean fitHeight = viewport.getHeight() > view.getPreferredSize().height;
        return new ScrollableComponentDelegate(
                    scrollPane, content, view, preferredSize,
                    (a,b,c)->unitIncrement, (a,b,c)->blockIncrement,
                    fitWidth, fitHeight
                );
    }

    private final JScrollPane             _scrollPane;
    private final JComponent              _content;
    private final Component               _view;
    private final Size                    _preferredSize;
    private final ScrollIncrementSupplier _unitIncrement;
    private final ScrollIncrementSupplier _blockIncrement;
    private final boolean                 _fitWidth;
    private final boolean                 _fitHeight;


    private ScrollableComponentDelegate(
        JScrollPane             scrollPane,
        JComponent              content,
        Component               view,
        Size                    preferredSize,
        ScrollIncrementSupplier unitIncrement,
        ScrollIncrementSupplier blockIncrement,
        boolean                 fitWidth,
        boolean                 fitHeight
    ) {
        _scrollPane     = scrollPane;
        _content        = content;
        _view           = view;
        _preferredSize  = preferredSize;
        _unitIncrement  = unitIncrement;
        _blockIncrement = blockIncrement;
        _fitWidth       = fitWidth;
        _fitHeight      = fitHeight;
    }

    /**
     * Creates an updated scrollable config with the
     * preferred size of the viewport for a view component.
     * For example, the preferred size of a <code>JList</code> component
     * is the size required to accommodate all the cells in its list.
     * However, the value of <code>preferredScrollableViewportSize</code>
     * is the size required for <code>JList.getVisibleRowCount</code> rows.
     * A component without any properties that would affect the viewport
     * size should just return <code>getPreferredSize</code> here.
     *
     * @param width  The preferred width of a <code>JViewport</code> whose view a
     *               <code>Scrollable</code> configured by this config object.
     * @param height The preferred height of a <code>JViewport</code> whose view a
     *               <code>Scrollable</code> configured by this config object.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated preferred size.
     * @see JViewport#getPreferredSize
     */
    public ScrollableComponentDelegate prefSize( int width, int height ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, Size.of(width, height), _unitIncrement, _blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Creates an updated scrollable config with the
     * preferred size of the viewport for a view component.
     * For example, the preferred size of a <code>JList</code> component
     * is the size required to accommodate all the cells in its list.
     * However, the value of <code>preferredScrollableViewportSize</code>
     * is the size required for <code>JList.getVisibleRowCount</code> rows.
     * A component without any properties that would affect the viewport
     * size should just return <code>getPreferredSize</code> here.
     *
     * @param preferredSize The preferred size of the component.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated preferred size.
     * @see JViewport#getPreferredSize
     * @throws NullPointerException If the preferred size is null, use {@link Size#unknown()}
     *                              to indicate that the preferred size is unknown.
     */
    public ScrollableComponentDelegate prefSize( Size preferredSize ) {
        Objects.requireNonNull(preferredSize);
        if ( preferredSize.equals(Size.unknown()) )
            return this;
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, preferredSize, _unitIncrement, _blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Creates an updated scrollable config with the specified unit increment.
     * The unit increment is the amount to scroll when the user requests a unit scroll.
     * For example, this could be the amount to scroll when the user presses the arrow keys.
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one new row
     * or column, depending on the value of orientation.  Ideally,
     * components should handle a partially exposed row or column by
     * returning the distance required to completely expose the item.
     * <p>
     * Scrolling containers, like JScrollPane, will use this increment value
     * each time the user requests a unit scroll.
     *
     * @param unitIncrement The unit increment value.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated unit increment.
     * @see JScrollBar#setUnitIncrement
     */
    public ScrollableComponentDelegate unitIncrement( int unitIncrement ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, (a, b, c)->unitIncrement, _blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Creates an updated scrollable config with the specified unit increment supplier,
     * (see {@link ScrollIncrementSupplier}) which takes the visible rectangle,
     * orientation and direction as arguments and returns the unit increment for the given context. <br>
     * The unit increment is the amount to scroll when the user requests a unit scroll.
     * For example, this could be the amount to scroll when the user presses the arrow keys.
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one new row
     * or column, depending on the value of orientation.  Ideally,
     * components should handle a partially exposed row or column by
     * returning the distance required to completely expose the item.
     * <p>
     * Scrolling containers, like JScrollPane, will use this increment value
     * each time the user requests a unit scroll.
     *
     * @param unitIncrement A {@link ScrollIncrementSupplier} that returns the unit increment for the given context.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated unit increment supplier.
     * @see JScrollBar#setUnitIncrement
     */
    public ScrollableComponentDelegate unitIncrement( ScrollIncrementSupplier unitIncrement ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, unitIncrement, _blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Creates an updated scrollable config with the specified block increment.
     * The block increment is the amount to scroll when the user requests a block scroll.
     * For example, this could be the amount to scroll when the user presses the page up or page down keys.
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one block
     * of rows or columns, depending on the value of orientation.
     * <p>
     * Scrolling containers, like JScrollPane, will use this increment value
     * each time the user requests a block scroll.
     *
     * @param blockIncrement The block increment value.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated block increment.
     * @see JScrollBar#setBlockIncrement
     */
    public ScrollableComponentDelegate blockIncrement( int blockIncrement ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, _unitIncrement, (a, b, c)->blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Creates an updated scrollable config with the specified block increment supplier,
     * (see {@link ScrollIncrementSupplier}) which takes the visible rectangle,
     * orientation and direction as arguments and returns the block increment for the given context. <br>
     * The block increment is the amount to scroll when the user requests a block scroll.
     * For example, this could be the amount to scroll when the user presses the page up or page down keys.
     * Components that display logical rows or columns should compute
     * the scroll increment that will completely expose one block
     * of rows or columns, depending on the value of orientation.
     * <p>
     * Scrolling containers, like JScrollPane, will use this increment value
     * each time the user requests a block scroll.
     *
     * @param blockIncrement A {@link ScrollIncrementSupplier} that returns the block increment for the given context.
     * @return A new instance of {@link ScrollableComponentDelegate} with the updated block increment supplier.
     * @see JScrollBar#setBlockIncrement
     */
    public ScrollableComponentDelegate blockIncrement( ScrollIncrementSupplier blockIncrement ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, _unitIncrement, blockIncrement, _fitWidth, _fitHeight
            );
    }

    /**
     * Set this to true if a viewport should always force the width of this
     * <code>Scrollable</code> to match the width of the viewport.
     * For example a normal
     * text view that supported line wrapping would return true here, since it
     * would be undesirable for wrapped lines to disappear beyond the right
     * edge of the viewport.  Note that returning true for a Scrollable
     * whose ancestor is a JScrollPane effectively disables horizontal
     * scrolling.
     * <p>
     * Scrolling containers, like JViewport, will use this method each
     * time they are validated.
     *
     * @return A new scroll config with the desired width fitting mode, which,
     *          if true, makes the viewport force the Scrollables width to match its own.
     */
    public ScrollableComponentDelegate fitWidth( boolean fitWidth ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, _unitIncrement, _blockIncrement, fitWidth, _fitHeight
        );
    }

    /**
     * Set this to true if a viewport should always force the height of this
     * Scrollable to match the height of the viewport.  For example a
     * columnar text view that flowed text in left to right columns
     * could effectively disable vertical scrolling by returning
     * true here.
     * <p>
     * Scrolling containers, like JViewport, will use this method each
     * time they are validated.
     *
     * @return A new scroll config with the desired width fitting mode, which,
     *          if true, makes a viewport force the Scrollables height to match its own.
     */
    public ScrollableComponentDelegate fitHeight( boolean fitHeight ) {
        return new ScrollableComponentDelegate(
                _scrollPane, _content, _view, _preferredSize, _unitIncrement, _blockIncrement, _fitWidth, fitHeight
        );
    }

    /**
     * Returns the scroll pane that contains the scrollable component
     * this configuration is for.
     *
     * @return The scroll pane that contains the scrollable component.
     */
    public JScrollPane scrollPane() {
        return _scrollPane;
    }

    /**
     * Returns the viewport of the scroll pane that contains the {@link javax.swing.Scrollable} component
     * this configuration is for.
     *
     * @return The viewport of the scroll pane that contains the scrollable component.
     */
    public JViewport viewport() {
        return _scrollPane.getViewport();
    }

    /**
     * Returns the user provided content component that is contained in the scroll pane
     * and which is wrapped by a view component implementing the {@link javax.swing.Scrollable}
     * interface configured by this {@link ScrollableComponentDelegate}.
     * <b>
     *     The content component is effectively the component supplied to
     *     the {@link UIForAnyScrollPane#add(Component[])} method.
     *     (Please note that a scroll pane can only ever hold a single content component)
     * </b>
     *
     * @return The content component that is contained within the scroll pane
     *         and wrapped by the {@link #view()} component.
     */
    public JComponent content() {
        return _content;
    }

    /**
     * Returns the view component implementing the {@link javax.swing.Scrollable} interface and which
     * is placed directly in the scroll panes {@link #viewport()} through {@link JViewport#setView(Component)}.<br>
     * This is the main UI component that is configured by this {@link ScrollableComponentDelegate}.
     *
     * @return The view component that is contained within the scroll pane.
     */
    public Component view() {
        return _view;
    }

    // Not part of the public API below:

    Size preferredSize() {
        return _preferredSize;
    }

    int unitIncrement(
        Bounds   viewRectangle,
        UI.Align orientation,
        int      direction
    ) {
        return _unitIncrement.get(viewRectangle, orientation, direction);
    }

    int blockIncrement(
        Bounds   viewRectangle,
        UI.Align orientation,
        int      direction
    ) {
        return _blockIncrement.get(viewRectangle, orientation, direction);
    }

    boolean fitWidth() {
        return _fitWidth;
    }

    boolean fitHeight() {
        return _fitHeight;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" +
                "preferredSize="  + _preferredSize  + ", " +
                "unitIncrement="  + _unitIncrement  + ", " +
                "blockIncrement=" + _blockIncrement + ", " +
                "fitWidth="       + _fitWidth       + ", " +
                "fitHeight="      + _fitHeight      +
            "]";
    }

    @Override
    public boolean equals( Object obj ) {
        if ( this == obj )
            return true;
        if ( !(obj instanceof ScrollableComponentDelegate) )
            return false;
        ScrollableComponentDelegate that = (ScrollableComponentDelegate) obj;
        return _preferredSize.equals(that._preferredSize) &&
               _unitIncrement.equals(that._unitIncrement) &&
               _blockIncrement.equals(that._blockIncrement) &&
               _fitWidth       == that._fitWidth &&
               _fitHeight      == that._fitHeight;
    }

    @Override
    public int hashCode() {
        return Objects.hash(_preferredSize, _unitIncrement, _blockIncrement, _fitWidth, _fitHeight);
    }

}