UIForSplitPane.java

package swingtree;

import sprouts.Val;
import sprouts.Var;

import javax.swing.JSplitPane;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Objects;

/**
 *  A SwingTree builder node designed for configuring {@link JSplitPane} instances.
 */
public final class UIForSplitPane<P extends JSplitPane> extends UIForAnySwing<UIForSplitPane<P>, P>
{
    private final BuilderState<P> _state;

    /**
     * {@link UIForAnySwing} (sub)types always wrap
     * a single component for which they are responsible.
     *
     * @param state The {@link BuilderState} modelling how the component is built.
     */

    UIForSplitPane( BuilderState<P> state ) {
        Objects.requireNonNull(state);
        _state = state;
    }

    @Override
    protected BuilderState<P> _state() {
        return _state;
    }
    
    @Override
    protected UIForSplitPane<P> _newBuilderWithState(BuilderState<P> newState ) {
        return new UIForSplitPane<>(newState);
    }

    /**
     * Sets the alignment of the split bar in the split pane.
     *
     * @param align The alignment of the split bar in the split pane.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if the provided alignment is null.
     */
    public final UIForSplitPane<P> withOrientation( UI.Align align ) {
        NullUtil.nullArgCheck( align, "split", UI.Align.class );
        return _with( thisComponent -> {
                    thisComponent.setOrientation( align.forSplitPane() );
                })
                ._this();
    }

    /**
     * Sets the alignment of the split bar in the split pane dynamically
     * based on the provided {@link Val} property which will be observed
     * by the split pane.
     *
     * @param align The alignment property of the split bar in the split pane.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if the provided alignment is null or the property is allowed to wrap a null value.
     */
    public final UIForSplitPane<P> withOrientation( Val<UI.Align> align ) {
        NullUtil.nullArgCheck( align, "align", Val.class );
        NullUtil.nullPropertyCheck( align, "align", "Null is not a valid alignment." );
        return _withOnShow( align, (thisComponent,it) -> {
                    thisComponent.setOrientation( it.forSplitPane() );
                })
                ._with( thisComponent -> {
                    thisComponent.setOrientation( align.orElseThrow().forSplitPane() );
                })
                ._this();
    }

    /**
     * Sets the location of the divider. This is passed off to the
     * look and feel implementation, and then listeners are notified. A value
     * less than 0 implies the divider should be reset to a value that
     * attempts to honor the preferred size of the left/top component.
     * After notifying the listeners, the last divider location is updated,
     * via <code>setLastDividerLocation</code>.
     *
     * @param location An int specifying a UI-specific value (typically a
     *        pixel count)
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSplitPane<P> withDividerAt( int location ) {
        return _with( thisComponent -> {
                    thisComponent.setDividerLocation(location);
                })
                ._this();
    }

    /**
     * Sets the location of the divider in the form of a property,
     * which can be dynamically update the divide.
     * This is passed off to the
     * look and feel implementation, and then listeners are notified. A value
     * less than 0 implies the divider should be reset to a value that
     * attempts to honor the preferred size of the left/top component.
     * After notifying the listeners, the last divider location is updated,
     * via <code>setLastDividerLocation</code>.
     *
     * @param location A property dynamically determining a UI-specific value (typically a
     *        pixel count)
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code location} is {@code null}.
     */
    public final UIForSplitPane<P> withDividerAt( Val<Integer> location ) {
        NullUtil.nullArgCheck( location, "location", Val.class );
        NullUtil.nullPropertyCheck( location, "location", "Null is not a valid divider location." );
        return _withOnShow( location, (thisComponent, it) -> {
                    thisComponent.setDividerLocation(it);
                })
                ._with( thisComponent -> {
                    thisComponent.setDividerLocation( location.orElseThrow() );
                })
                ._this();
    }

    /**
     * Sets the size of the divider.
     *
     * @param size An integer giving the size of the divider in pixels
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSplitPane<P> withDividerSize( int size ) {
        return _with( thisComponent -> {
                    thisComponent.setDividerSize(UI.scale(size));
                })
                ._this();
    }

    /**
     * Sets the size of the divider in the form of a property,
     * which can be dynamically update.
     *
     * @param size A property dynamically determining the size of the divider in pixels
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code size} is {@code null}.
     */
    public final UIForSplitPane<P> withDividerSize( Val<Integer> size ) {
        NullUtil.nullArgCheck( size, "size", Val.class );
        NullUtil.nullPropertyCheck( size, "size", "Null is not a valid divider size." );
        return _withOnShow( size, (thisComponent,it) -> {
                    thisComponent.setDividerSize(UI.scale(it));
                })
                ._with( thisComponent -> {
                    thisComponent.setDividerSize( UI.scale(size.orElseThrow()) );
                })
                ._this();
    }

    private void _calculateDividerLocationFrom( P p, double percentage ) {
        int loc = (int) (
                        p.getOrientation() == JSplitPane.HORIZONTAL_SPLIT
                            ? p.getWidth()  * percentage
                            : p.getHeight() * percentage
                    );
        p.setDividerLocation(loc);
    }

    private double _calculatePercentageFrom( P p ) {
        return p.getOrientation() == JSplitPane.HORIZONTAL_SPLIT
                ? (double) p.getDividerLocation() / p.getWidth()
                : (double) p.getDividerLocation() / p.getHeight();
    }

    /**
     * Sets the location of the divider based on a percentage value.
     * So if the split pane split is aligned horizontally, the divider
     * will be set to the percentage of the height of the split pane.
     * If the split pane is aligned vertically, the divider will be set
     * to the percentage of the width of the split pane.
     * <p>
     * Note that a component listener is installed to the split pane's size temporarily,
     * so that the divider location can be calculated when the split pane is sized
     * by the layout manager for the first time.
     * This is because before the layout manager did its thing, there was no way to know the actual
     * location of the divider based on the percentage.
     * <b>
     *     So keep in mind that changes to the divider location immediately after this
     *     method is called will be overridden by said listener!
     * </b>
     * <p>
     * A change of the divider location is ultimately passed off to the
     * look and feel implementation, where listeners are then notified. A value
     * less than 0 implies the divider should be reset to a value that
     * attempts to honor the preferred size of the left/top component.
     * After notifying the listeners, the last divider location is updated,
     * via {@link JSplitPane#setLastDividerLocation(int)}.
     *
     * @param percentage A double value between 0 and 1, representing the percentage of the split pane's
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSplitPane<P> withDivisionOf( double percentage ) {
        return _with( thisComponent -> {
                    _calculateDividerLocationFrom(thisComponent, percentage);
                    /*
                        Before the layout manager did its thing, there was no way to know the actual
                        location of the divider.
                        So we install a listener to the split pane's size, so that we can recalculate
                        the divider location when the split pane is resized.
                        Then it removes itself after the first time it's called.
                     */
                    thisComponent.addComponentListener(new ComponentAdapter() {
                        @Override
                        public void componentResized( ComponentEvent e ) {
                            _calculateDividerLocationFrom(thisComponent, percentage);
                            thisComponent.removeComponentListener(this);
                        }
                    });
                })
                ._this();
    }

    /**
     * Updates the location of the divider based on a percentage property which means
     * that if the split pane split is aligned horizontally, the divider
     * will be set to the percentage of the height of the split pane and
     * if the split pane is aligned vertically, the divider will be set
     * to the percentage of the width of the split pane.
     * <p>
     * This method binds the property uni-directionally,
     * which means that the property will be observed by the
     * split pane, but the split pane will not change the property
     * (see {@link #withDivisionOf(Var)} for a bidirectional variant).
     * <p>
     * A change of the divider location is ultimately passed off to the
     * look and feel implementation, where listeners are then notified. A value
     * less than 0 implies the divider should be reset to a value that
     * attempts to honor the preferred size of the left/top component.
     * After notifying the listeners, the last divider location is updated,
     * via {@link JSplitPane#setLastDividerLocation(int)}.
     * <p>
     *     Note that the percentage is calculated based on the split pane's
     *     current size, so if the split pane is resized, the divider location
     *     will be recalculated in order to honor the percentage.
     * </p>
     * @param percentage A property dynamically determining a double value between 0 and 1, representing the percentage of the split pane's
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSplitPane<P> withDivisionOf( Val<Double> percentage ) {
        NullUtil.nullArgCheck( percentage, "percentage", Val.class );
        NullUtil.nullPropertyCheck( percentage, "percentage", "Null is not a valid percentage." );
        return _withOnShow( percentage, (thisComponent,v) -> {
                    _calculateDividerLocationFrom(thisComponent, v);
               })
                ._with( thisComponent -> {
                    // Now we need to register a listener to the split pane's size, so that we can recalculate the divider location
                    // when the split pane is resized:
                    thisComponent.addComponentListener(new ComponentAdapter() {
                        @Override
                        public void componentResized( ComponentEvent e ) {
                            _calculateDividerLocationFrom(thisComponent, percentage.orElseThrow());
                        }
                    });
                    _calculateDividerLocationFrom(thisComponent, percentage.orElseThrow());
                })
                ._this();
    }

    /**
     * Updates the location of the divider based on a percentage property which means
     * that if the split pane split is aligned horizontally, the divider
     * will be set to the percentage of the height of the split pane.
     * If, however, the split pane is aligned vertically, then the divider will be set
     * to the percentage of the width of the split pane.
     * <p>
     * Note that this binds the property to the location of the divider
     * bidirectionally, which means that the value inside the property will be updated when the
     * divider location is changed by the user and the divider location will be updated when the
     * property changes in the business logic. <br>
     * <p>
     * A change of the divider location is ultimately passed off to the
     * look and feel implementation, where listeners are then notified. A value
     * less than 0 implies the divider should be reset to a value that
     * attempts to honor the preferred size of the left/top component.
     * After notifying the listeners, the last divider location is updated,
     * via {@link JSplitPane#setLastDividerLocation(int)}.
     * <p>
     *     Note that the percentage is calculated based on the split pane's
     *     current size, so if the split pane is resized, the divider location
     *     will be recalculated.
     * <p>
     * @param percentage A property dynamically determining a double value between 0 and 1, representing the percentage of the split pane's
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSplitPane<P> withDivisionOf( Var<Double> percentage ) {
        NullUtil.nullArgCheck( percentage, "percentage", Var.class );
        NullUtil.nullPropertyCheck( percentage, "percentage", "Null is not a valid percentage." );
        return _withOnShow( percentage, (thisComponent,v) -> {
                    _calculateDividerLocationFrom(thisComponent, v);
               })
                ._with( thisComponent -> {
                    _calculateDividerLocationFrom(thisComponent, percentage.orElseThrow());
                    // Now we need to register a listener to the split pane's size, so that we can recalculate the divider location
                    // when the split pane is resized:
                    thisComponent.addComponentListener(new ComponentAdapter() {
                        @Override
                        public void componentResized( ComponentEvent e ) {
                            _calculateDividerLocationFrom(thisComponent, percentage.orElseThrow());
                        }
                    });
                    // We listen for slider movement as well, so that we can recalculate the divider location
                    thisComponent.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, evt -> {
                        if ( evt.getNewValue() != null ) {
                            double newPercentage = _calculatePercentageFrom(thisComponent);
                            percentage.set(newPercentage);
                        }
                    });
                })
                ._this();
    }

}