UIForAnyScrollPane.java

package swingtree;

import net.miginfocom.swing.MigLayout;
import org.jspecify.annotations.Nullable;
import sprouts.Val;
import swingtree.components.JBox;
import swingtree.layout.AddConstraint;

import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.Scrollable;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.Objects;

/**
 *  Defines an abstract builder for
 *  constructing a scroll pane or any subclass of {@link JScrollPane}.
 *
 * @param <I> The concrete type of the builder instance, which is
 *           important as a return type for the builder methods.
 * @param <P> The type of the scroll pane or any subclass of {@link JScrollPane}.
 */
public abstract class UIForAnyScrollPane<I, P extends JScrollPane> extends UIForAnySwing<I, P>
{
    @Override
    protected void _addComponentTo(P thisComponent, JComponent addedComponent, @Nullable AddConstraint constraints) {
        if ( constraints != null ) {
            if ( addedComponent instanceof Scrollable ) {
                ThinScrollableDelegateBox thinDelegationBox = new ThinScrollableDelegateBox((Scrollable) addedComponent);
                thinDelegationBox.add(addedComponent, constraints.toConstraintForLayoutManager());
            } else {
                // The user wants to add a component to the scroll pane with a specific constraint.
                // Swing does not support any constraints for scroll panes, but we are not Swing, we are SwingTree!
                ThinDelegationBox thinDelegationBox = new ThinDelegationBox(addedComponent);
                thinDelegationBox.add(addedComponent, constraints.toConstraintForLayoutManager());
                addedComponent = thinDelegationBox;
                //  ^ So we improve this situation by wrapping the component in a mig layout panel, supporting constraints.

                // Let's strip it of any visible properties, since it should serve merely as a container.
                addedComponent.setBorder(null);
                addedComponent.setOpaque(false);
                addedComponent.setBackground(null);
            }
        }
        thisComponent.setViewportView(addedComponent);
    }

    /**
     *  Use this to set the scroll bars policy for both horizontal and vertical scroll bars.<br>
     *  The scroll policy can be one of the following:
     *  <ul>
     *      <li>{@link swingtree.UI.Active#NEVER}: The scrolls bar will never be displayed.</li>
     *      <li>{@link swingtree.UI.Active#ALWAYS}: The scrolls bar will always be displayed.</li>
     *      <li>{@link swingtree.UI.Active#AS_NEEDED}:
     *          The two scroll bars will only be displayed when needed,
     *          i.e. when the content is too large to fit in the viewport
     *          and scrolling is required.
     *      </li>
     *  </ul>
     *
     * @param scrollPolicy The scroll policy to use.
     * @return The next builder instance, to allow for method chaining.
     * @throws NullPointerException If the argument is null.
     */
    public final I withScrollBarPolicy( UI.Active scrollPolicy ) {
        Objects.requireNonNull(scrollPolicy);
        return _with( thisComponent -> {
                    _setVerticalScrollBarPolicy(thisComponent, scrollPolicy);
                    _setHorizontalScrollBarPolicy(thisComponent, scrollPolicy);
               })
               ._this();
    }

    /**
     *  Use this to set the scroll bars policy for the vertical scroll bar,
     *  which controls when the vertical scroll bar should be displayed or not.<br>
     *  The scroll policy can be one of the following:
     *  <ul>
     *      <li>{@link swingtree.UI.Active#NEVER}: The vertical scroll bar will never be displayed.</li>
     *      <li>{@link swingtree.UI.Active#ALWAYS}: The vertical scroll bar will always be displayed.</li>
     *      <li>{@link swingtree.UI.Active#AS_NEEDED}:
     *          The vertical scroll bar will only be displayed when needed,
     *          i.e. when the content is too large to fit in the viewport
     *          and scrolling is required.
     *      </li>
     *  </ul>
     *
     * @param scrollBarPolicy The scroll policy to determine when the vertical scroll bar should be displayed.
     * @return This builder node, to allow for method chaining.
     * @throws NullPointerException If the argument is null.
     */
    public final I withVerticalScrollBarPolicy( UI.Active scrollBarPolicy ) {
        Objects.requireNonNull(scrollBarPolicy);
        return _with( thisComponent -> {
                    _setVerticalScrollBarPolicy(thisComponent, scrollBarPolicy);
                })
                ._this();
    }

    private void _setVerticalScrollBarPolicy( P thisComponent, UI.Active scrollBarPolicy ) {
        switch ( scrollBarPolicy ) {
            case NEVER:     thisComponent.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); break;
            case ALWAYS:    thisComponent.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); break;
            case AS_NEEDED: thisComponent.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); break;
        }
    }

    /**
     *  Use this to dynamically set the scroll bars policy for the vertical scroll bar.
     *  When the property changes, the scroll bar policy will be updated accordingly.
     *  <p>
     *  The scroll policy can be one of the following:
     *  <ul>
     *      <li>{@link swingtree.UI.Active#NEVER}: The vertical scroll bar will never be displayed.</li>
     *      <li>{@link swingtree.UI.Active#ALWAYS}: The vertical scroll bar will always be displayed.</li>
     *      <li>{@link swingtree.UI.Active#AS_NEEDED}:
     *          The vertical scroll bar will only be displayed when needed,
     *          i.e. when the content is too large to fit in the viewport
     *          and scrolling is required.
     *      </li>
     *  </ul>
     *
     * @param scrollBarPolicy The scroll policy property, whose value will determine when
     *                        the vertical scroll bar should be displayed.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withVerticalScrollBarPolicy( Val<UI.Active> scrollBarPolicy ) {
        NullUtil.nullArgCheck(scrollBarPolicy, "scrollBarPolicy", Val.class);
        NullUtil.nullPropertyCheck(scrollBarPolicy, "scrollBarPolicy", "Null is not a valid scroll bar policy.");
        return _withOnShow( scrollBarPolicy, (thisComponent,v) -> {
                    _setVerticalScrollBarPolicy(thisComponent, v);
                })
                ._with( thisComponent -> {
                    _setVerticalScrollBarPolicy(thisComponent, scrollBarPolicy.get());
                })
                ._this();
    }

    /**
     *  Use this to set the scroll bars policy for the horizontal scroll bar.
     *  The scroll policy can be one of the following:
     *  <ul>
     *      <li>{@link swingtree.UI.Active#NEVER}: The horizontal scroll bar will never be displayed.</li>
     *      <li>{@link swingtree.UI.Active#ALWAYS}: The horizontal scroll bar will always be displayed.</li>
     *      <li>{@link swingtree.UI.Active#AS_NEEDED}:
     *          The horizontal scroll bar will only be displayed when needed,
     *          i.e. when the content is too large to fit in the viewport
     *          and scrolling is required.
     *      </li>
     *  </ul>
     *
     * @param scrollBarPolicy The scroll policy to use.
     * @return The next builder instance, to allow for method chaining.
     * @throws NullPointerException If the argument is null.
     */
    public final I withHorizontalScrollBarPolicy( UI.Active scrollBarPolicy ) {
        Objects.requireNonNull(scrollBarPolicy);
        return _with( thisComponent -> {
                    _setHorizontalScrollBarPolicy(thisComponent, scrollBarPolicy);
                })
                ._this();
    }

    private void _setHorizontalScrollBarPolicy( P thisComponent, UI.Active scrollBarPolicy ) {
        switch ( scrollBarPolicy ) {
            case NEVER: thisComponent.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); break;
            case ALWAYS: thisComponent.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); break;
            case AS_NEEDED: thisComponent.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); break;
        }
    }

    /**
     *  Use this to dynamically set the scroll bars policy for the horizontal scroll bar.
     *  When the property changes, the scroll bar policy will be updated accordingly.
     *  <p>
     *  The scroll policy can be one of the following:
     *  <ul>
     *      <li>{@link swingtree.UI.Active#NEVER}: The horizontal scroll bar will never be displayed.</li>
     *      <li>{@link swingtree.UI.Active#ALWAYS}: The horizontal scroll bar will always be displayed.</li>
     *      <li>{@link swingtree.UI.Active#AS_NEEDED}:
     *          The horizontal scroll bar will only be displayed when needed,
     *          i.e. when the content is too large to fit in the viewport
     *          and scrolling is required.
     *      </li>
     *  </ul>
     *
     * @param scrollBarPolicy The scroll policy property, whose value will determine when
     *                        the horizontal scroll bar should be displayed.
     * @return The next builder instance, to allow for method chaining.
     * @throws NullPointerException If the argument is null.
     */
    public final I withHorizontalScrollBarPolicy( Val<UI.Active> scrollBarPolicy ) {
        NullUtil.nullArgCheck(scrollBarPolicy, "scrollBarPolicy", Val.class);
        NullUtil.nullPropertyCheck(scrollBarPolicy, "scrollBarPolicy", "Null is not a valid scroll bar policy.");
        return _withOnShow( scrollBarPolicy, (thisComponent,v) -> {
                    _setHorizontalScrollBarPolicy(thisComponent, v);
                })
                ._with( thisComponent -> {
                    _setHorizontalScrollBarPolicy(thisComponent, scrollBarPolicy.get());
                })
                ._this();
    }

    /**
     *  Use this to set the vertical scroll increment unit,
     *  which controls how far the content moves when you
     *  use the mouse wheel, scroll gesture on a touchpad or
     *  press the arrow buttons on the scrollbar.
     *  This can be thought of as the smallest step size for
     *  scrolling. Like for example, scrolling by one line of text
     *  at a time in a text area.
     *
     * @param increment The scroll vertical increment to use.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withVerticalScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getVerticalScrollBar().setUnitIncrement(increment);
                })
                ._this();
    }

    /**
     *  Use this to set the horizontal scroll increment unit,
     *  which typically controls how far the content moves when you:
     *  <ul>
     *      <li>press the left and right arrow buttons on the scrollbar</li>
     *      <li>press the left and right arrow buttons on the keyboard</li>
     *      <li>use the mouse wheel or scroll gesture on a touchpad</li>
     *  </ul>
     *  <br>
     *  This can be thought of as the smallest step size for
     *  scrolling. Like for example, scrolling by one line of text
     *  at a time in a text area.
     *
     * @param increment The scroll horizontal increment to use.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withHorizontalScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getHorizontalScrollBar().setUnitIncrement(increment);
                })
                ._this();
    }

    /**
     * Use this to set the vertical and horizontal scroll increment,
     * which controls how far the content moves when you:
     *  <ul>
     *      <li>press the arrow buttons on the scrollbars</li>
     *      <li>press the arrow buttons on the keyboard</li>
     *      <li>use the mouse wheel or scroll gesture on a touchpad</li>
     *  </ul>
     *  <br>
     *  This can be thought of as the smallest step size for
     *  scrolling. Like for example, scrolling by one line of text
     *  at a time in a text area.
     *
     * @see #withVerticalScrollIncrement(int) if you only want to define the vertical increment.
     * @see #withHorizontalScrollIncrement(int) if you only want to define the horizontal increment.
     * @param increment The scroll increment to use.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getVerticalScrollBar().setUnitIncrement(increment);
                    thisComponent.getHorizontalScrollBar().setUnitIncrement(increment);
                })
                ._this();
    }

    /**
     *  Use this to set the vertical scroll bar block increment,
     *  which typically controls how far the content moves when you:
     *  <ul>
     *      <li>press the page up or page down keys (not to be confused with the arrow keys)</li>
     *      <li>click on a scroll bar track (the empty area of the scrollbar, not the thumb or arrows)</li>
     *  </ul>
     *  It represents a larger jump, like moving an entire "page" or a
     *  significant chunk of content.
     *  <p>
     *  Note, that if the argument is equal to the value of Integer.MIN_VALUE,
     *  then most look and feel implementations will not provide scrolling
     *  to the right/down.
     *  <br><b>
     *  Please be aware that look and feel implementations
     *  that provide custom scrolling behavior may ignore
     *  the block increment value.
     *  </b>
     *
     * @param increment The scroll vertical block increment to use when scrolling by a "block".
     * @return This builder instance, to allow for method chaining.
     */
    public final I withVerticalBlockScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getVerticalScrollBar().setBlockIncrement(increment);
                })
                ._this();
    }

    /**
     *  Use this to set the horizontal scroll bar block increment,
     *  which typically controls how far the content moves
     *  to the left or right when you:
     *  <ul>
     *      <li>press the page up or page down keys (not to be confused with the arrow keys)</li>
     *      <li>click on a scroll bar track (the empty area of the scrollbar, not the thumb or arrows)</li>
     *  </ul>
     *  <br><b>
     *  Please be aware that look and feel implementations
     *  that provide custom scrolling behavior may ignore
     *  the block increment value.
     *  </b>
     *
     * @param increment The scroll horizontal block increment to use.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withHorizontalBlockScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getHorizontalScrollBar().setBlockIncrement(increment);
                })
                ._this();
    }

    /**
     * Use this to set both the vertical and horizontal scroll block increment.
     * The block increment is the amount to change the scrollbar's value by,
     * given a block (usually "page") up/down request or when the user clicks
     * above or below the scrollbar "knob" to change the value
     * up or down by large amount.
     * <br><b>
     *  Please be aware that look and feel implementations
     *  that provide custom scrolling behavior may ignore
     *  the block increment value.
     * </b>
     *
     * @see #withVerticalBlockScrollIncrement(int) if you only want to define the vertical increment.
     * @see #withHorizontalBlockScrollIncrement(int) if you only want to define the horizontal increment.
     * @param increment The scroll block increment to use.
     * @return This builder instance, to allow for method chaining.
     */
    public final I withBlockScrollIncrement( int increment ) {
        return _with( thisComponent -> {
                    thisComponent.getVerticalScrollBar().setBlockIncrement(increment);
                    thisComponent.getHorizontalScrollBar().setBlockIncrement(increment);
                })
                ._this();
    }


    /**
     *  Delegate class for wrapping a component in a thin container
     *  which always has the same sizes as the wrapped component.
     */
    static class ThinDelegationBox extends JBox {

        protected final JComponent _child;

        ThinDelegationBox(JComponent child) {
            setLayout(new MigLayout("fill, ins 0, hidemode 2, gap 0"));
            _child = child;
        }

        @Override
        public void setSize(Dimension d) {
            super.setSize(d);
            _child.setSize(d);
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension prefChildSize = _child.getPreferredSize();
            Dimension prefSelfSize  = super.getPreferredSize();
            if ( !Objects.equals(prefChildSize, prefSelfSize) ) {
                this.setPreferredSize(prefChildSize);
            }
            return prefChildSize;
        }

        @Override
        public Dimension getMinimumSize() {
            Dimension minChildSize = _child.getMinimumSize();
            Dimension minSelfSize  = super.getMinimumSize();
            if ( !Objects.equals(minChildSize, minSelfSize) ) {
                this.setMinimumSize(minChildSize);
            }
            return minChildSize;
        }

        @Override
        public Dimension getMaximumSize() {
            Dimension maxChildSize = _child.getMaximumSize();
            Dimension maxSelfSize  = super.getMaximumSize();
            if ( !Objects.equals(maxChildSize, maxSelfSize) ) {
                this.setMinimumSize(maxChildSize);
            }
            return maxChildSize;
        }
    }

    private static final class ThinScrollableDelegateBox extends ThinDelegationBox implements Scrollable {

        private final Scrollable _scrollable;


        ThinScrollableDelegateBox( Scrollable child ) {
            super((JComponent) child);
            _scrollable = child;
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return _scrollable.getPreferredScrollableViewportSize();
        }

        @Override
        public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) {
            return _scrollable.getScrollableUnitIncrement(visibleRect, orientation, direction);
        }

        @Override
        public int getScrollableBlockIncrement( Rectangle visibleRect, int orientation, int direction ) {
            return _scrollable.getScrollableBlockIncrement(visibleRect, orientation, direction);
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return _scrollable.getScrollableTracksViewportWidth();
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return _scrollable.getScrollableTracksViewportHeight();
        }
    }
}