UIForLabel.java

package swingtree;

import org.slf4j.Logger;
import sprouts.Val;
import swingtree.api.IconDeclaration;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import java.awt.Desktop;
import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;

/**
 *  A SwingTree builder node designed for configuring {@link JLabel} instances.
 * 	<p>
 * 	<b>Take a look at the <a href="https://globaltcad.github.io/swing-tree/">living swing-tree documentation</a>
 * 	where you can browse a large collection of examples demonstrating how to use the API of this class.</b>
 */
public final class UIForLabel<L extends JLabel> extends UIForAnySwing<UIForLabel<L>, L>
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(UIForLabel.class);

    private final BuilderState<L> _state;

    UIForLabel( BuilderState<L> state ) {
        Objects.requireNonNull(state);
        _state = state;
    }

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

    private void _makeBold( L thisComponent ) {
        Font f = thisComponent.getFont();
        thisComponent.setFont(f.deriveFont(f.getStyle() | Font.BOLD));
    }

    private void _makePlain( L thisComponent ) {
        Font f = thisComponent.getFont();
        thisComponent.setFont(f.deriveFont(f.getStyle() & ~Font.BOLD));
    }

    /**
     *  Makes the wrapped {@link JLabel} font bold (!plain).
     *
     * @return This very builder to allow for method chaining.
     */
    public UIForLabel<L> makeBold() {
        return this.peek( label -> {
            _makeBold(label);
        });
    }

    /**
     *  Use this to make the underlying {@link JLabel} into a clickable link.
     *
     * @param href A string containing a valid URL used as link hyper reference.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code href} is {@code null}.
     */
    public UIForLabel<L> makeLinkTo( String href ) {
        NullUtil.nullArgCheck( href, "href", String.class );
        return makeLinkTo( Val.of(href) );
    }

    /**
     *  Use this to make the underlying {@link JLabel} into a clickable link
     *  based on the string provided property defining the link address.
     *  When the link wrapped by the provided property changes,
     *  then a click on the label will lead to the wrapped link.
     *
     * @param href A string property containing a valid URL used as link hyper reference.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code href} is {@code null}.
     */
    public UIForLabel<L> makeLinkTo( Val<String> href ) {
        NullUtil.nullArgCheck( href, "href", Val.class );
        NullUtil.nullPropertyCheck( href, "href", "Use an empty String instead of null to model a link going nowhere." );
        return _with( thisComponent -> {
                    LazyRef<String> text = LazyRef.of(thisComponent::getText);
                    thisComponent.addMouseListener(new MouseAdapter() {
                        @Override
                        public void mouseClicked(MouseEvent e) {
                            try {
                                String ref = href.orElseThrow().trim();
                                if ( ref.isEmpty() ) return;
                                if ( !ref.startsWith("http") ) ref = "https://" + ref;
                                Desktop.getDesktop().browse(new URI(ref));
                            } catch (IOException | URISyntaxException e1) {
                                log.error("Failed to open link: " + href.orElseThrow(), e1);
                            }
                        }
                        @Override  public void mouseExited(MouseEvent e) { thisComponent.setText(text.get()); }
                        @Override
                        public void mouseEntered(MouseEvent e) {
                            thisComponent.setText("<html><a href=''>" + text.get() + "</a></html>");
                        }
                    });
                })
                ._this();
    }

    /**
     *  Makes the wrapped {@link JLabel} font plain (!bold).
     *
     * @return This very builder to allow for method chaining.
     */
    public UIForLabel<L> makePlain() {
        return _with( label -> {
                    _makePlain(label);
                })
                ._this();
    }

    /**
     *  Makes the wrapped {@link JLabel} font bold if it is plain
     *  and plain if it is bold...
     *
     * @return This very builder to allow for method chaining.
     */
    public final UIForLabel<L> toggleBold() {
        return _with( label -> {
                    Font f = label.getFont();
                    label.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));
                })
                ._this();
    }

    /**
     *  Makes the wrapped {@link JLabel} font bold if the provided flag is true,
     *  and plain if the flag is false.
     *  See {@link #makeBold()} and {@link #makePlain()} for more information.
     *
     * @param isBold The flag determining if the font of this label should be bold or plain.
     * @return This very builder to allow for method chaining.
     */
    public final UIForLabel<L> isBoldIf( boolean isBold ) {
        if ( isBold )
            return makeBold();
        else
            return makePlain();
    }

    /**
     *  When the flag wrapped by the provided property changes,
     *  then the font of this label will switch between being bold and plain.
     *
     * @param isBold The property which should be bound to the boldness of this label.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code isBold} is {@code null}.
     */
    public final UIForLabel<L> isBoldIf( Val<Boolean> isBold ) {
        NullUtil.nullArgCheck( isBold, "isBold", Val.class );
        NullUtil.nullPropertyCheck( isBold, "isBold", "You can not use null to model if a label is bold or not." );
        return _withOnShow( isBold, (thisComponent,v) -> {
                    if ( v )
                        _makeBold(thisComponent);
                    else
                        _makePlain(thisComponent);
                })
                ._with( thisComponent -> {
                    if ( isBold.orElseThrow() )
                        _makeBold(thisComponent);
                    else
                        _makePlain(thisComponent);
                })
                ._this();
    }

    /**
     * Defines the single line of text this component will display.  If
     * the value of text is null or empty string, nothing is displayed.
     * <p>
     * The default value of this property is null.
     * <p>
     *
     * @param text The new text to be set for the wrapped label.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code text} is {@code null}.
     */
    public final UIForLabel<L> withText( String text ) {
        NullUtil.nullArgCheck( text, "text", String.class );
        return _with( thisComponent -> {
                    thisComponent.setText(text);
                })
                ._this();
    }

    /**
     *  Dynamically defines a single line of text displayed on this label.
     *  If the value of text is null or an empty string, nothing is displayed.
     *  When the text wrapped by the provided property changes,
     *  then so does the text displayed on this label change.
     *
     * @param text The text property to be bound to the wrapped label.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code text} is {@code null}.
     */
    public final UIForLabel<L> withText( Val<String> text ) {
        NullUtil.nullArgCheck( text, "text", Val.class );
        NullUtil.nullPropertyCheck( text, "text", "Please use an empty String instead of null." );
        return _withOnShow( text, (thisComponent,v) -> {
                    thisComponent.setText(v);
                })
                ._with( thisComponent -> {
                    thisComponent.setText( text.orElseThrow() );
                })
                ._this();
    }

    /**
     *  A convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *     .peek( label -> label.setHorizontalAlignment(...) );
     *  }</pre>
     * This sets the horizontal alignment of the label's content (icon and text).
     *
     * @param horizontalAlign The horizontal alignment which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code horizontalAlign} is {@code null}.
     */
    public UIForLabel<L> withHorizontalAlignment( UI.HorizontalAlignment horizontalAlign ) {
        NullUtil.nullArgCheck( horizontalAlign, "horizontalAlign", UI.HorizontalAlignment.class );
        return _with( thisComponent -> {
                    horizontalAlign.forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                })
                ._this();
    }


    /**
     *  This binds to a property defining the horizontal alignment of the label's content (icon and text).
     *  When the alignment enum wrapped by the provided property changes,
     *  then so does the alignment of this label.
     *
     * @param horizontalAlign The horizontal alignment property which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code horizontalAlign} is {@code null}.
     */
    public UIForLabel<L> withHorizontalAlignment(Val<UI.HorizontalAlignment> horizontalAlign ) {
        NullUtil.nullArgCheck( horizontalAlign, "horizontalAlign", Val.class );
        NullUtil.nullPropertyCheck( horizontalAlign, "horizontalAlign", "Null is not a valid alignment." );
        return _withOnShow( horizontalAlign, (thisComponent,v) -> {
                    v.forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                })
                ._with( thisComponent -> {
                    horizontalAlign.orElseThrow().forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                })
                ._this();
    }

    /**
     *  Use this to set the vertical alignment of the label's content (icon and text).
     *  This is a convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *     .peek( label -> label.setVerticalAlignment(...) );
     *  }</pre>
     *
     * @param verticalAlign The vertical alignment which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code verticalAlign} is {@code null}.
     */
    public UIForLabel<L> withVerticalAlignment( UI.VerticalAlignment verticalAlign ) {
        NullUtil.nullArgCheck( verticalAlign, "verticalAlign", UI.VerticalAlignment.class );
        return _with( thisComponent -> {
                    verticalAlign.forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._this();
    }

    /**
     * This binds to a property defining the vertical alignment of the label's content (icon and text).
     *  When the alignment enum wrapped by the provided property changes,
     *  then so does the alignment of this label.
     *
     * @param verticalAlign The vertical alignment property which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code verticalAlign} is {@code null}.
     */
    public UIForLabel<L> withVerticalAlignment( Val<UI.VerticalAlignment> verticalAlign ) {
        NullUtil.nullArgCheck( verticalAlign, "verticalAlign", Val.class );
        NullUtil.nullPropertyCheck( verticalAlign, "verticalAlign", "Null is not a valid alignment." );
        return _withOnShow( verticalAlign, (thisComponent,v) -> {
                    v.forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._with( thisComponent -> {
                    verticalAlign.orElseThrow().forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._this();
    }

    /**
     *  Use this to set the horizontal and vertical alignment of the label's content (icon and text).
     *  This is a convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *     .peek( label -> label.setHorizontalAlignment(...); label.setVerticalAlignment(...) );
     *  }</pre>
     *
     * @param alignment The alignment which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code alignment} is {@code null}.
     */
    public UIForLabel<L> withAlignment( UI.Alignment alignment ) {
        NullUtil.nullArgCheck( alignment, "alignment", UI.Alignment.class );
        return _with( thisComponent -> {
                    alignment.getHorizontal().forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                    alignment.getVertical().forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._this();
    }

    /**
     *  This binds to a property defining the horizontal and vertical alignment of the label's content (icon and text).
     *  When the alignment enum wrapped by the provided property changes,
     *  then so does the alignment of this label.
     *
     * @param alignment The alignment property which should be applied to the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code alignment} is {@code null}.
     */
    public UIForLabel<L> withAlignment( Val<UI.Alignment> alignment ) {
        NullUtil.nullArgCheck( alignment, "alignment", Val.class );
        NullUtil.nullPropertyCheck( alignment, "alignment", "Null is not a valid alignment." );
        return _withOnShow( alignment, (thisComponent,v) -> {
                    v.getHorizontal().forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                    v.getVertical().forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._with( thisComponent -> {
                    UI.Alignment a = alignment.orElseThrow();
                    a.getHorizontal().forSwing().ifPresent(thisComponent::setHorizontalAlignment);
                    a.getVertical().forSwing().ifPresent(thisComponent::setVerticalAlignment);
                })
                ._this();
    }


    /**
     *  Use this to set the horizontal position of the label's text, relative to its image.
     *  A convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *         .peek( label -> label.setHorizontalTextPosition(...) );
     *  }</pre>
     *
     * @param horizontalAlign The horizontal alignment which should be applied to the text of the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code horizontalAlign} is {@code null}.
     */
    public UIForLabel<L> withHorizontalTextPosition( UI.HorizontalAlignment horizontalAlign ) {
        NullUtil.nullArgCheck( horizontalAlign, "horizontalAlign", UI.HorizontalAlignment.class );
        return _with( thisComponent -> {
                    horizontalAlign.forSwing().ifPresent(thisComponent::setHorizontalTextPosition);
                })
                ._this();
    }

    /**
     *  Use this to bind to a property defining the horizontal position of the label's text, relative to its image.
     *  When the alignment enum wrapped by the provided property changes,
     *  then so does the alignment of this label.
     *
     * @param horizontalAlign The horizontal alignment property which should be applied to the text of the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code horizontalAlign} is {@code null}.
     */
    public UIForLabel<L> withHorizontalTextPosition( Val<UI.HorizontalAlignment> horizontalAlign ) {
        NullUtil.nullArgCheck( horizontalAlign, "horizontalAlign", Val.class );
        NullUtil.nullPropertyCheck( horizontalAlign, "horizontalAlign", "Null is not a valid alignment." );
        return _withOnShow( horizontalAlign, (thisComponent, v) -> {
                    v.forSwing().ifPresent(thisComponent::setHorizontalTextPosition);
                })
                ._with( thisComponent -> {
                    horizontalAlign.orElseThrow().forSwing().ifPresent(thisComponent::setHorizontalTextPosition);
                })
                ._this();
    }

    /**
     *  Use this to set the horizontal position of the label's text, relative to its image. <br>
     *  This is a convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *     .peek( label -> label.setVerticalTextPosition(...) );
     *  }</pre>
     *
     * @param verticalAlign The vertical alignment which should be applied to the text of the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code verticalAlign} is {@code null}.
     */
    public UIForLabel<L> withVerticalTextPosition( UI.VerticalAlignment verticalAlign ) {
        NullUtil.nullArgCheck( verticalAlign, "verticalAlign", UI.VerticalAlignment.class );
        return _with( thisComponent -> {
                    verticalAlign.forSwing().ifPresent(thisComponent::setVerticalTextPosition);
                })
                ._this();
    }

    /**
     *  Use this to bind to a property defining the vertical position of the label's text, relative to its image.
     *  When the alignment enum wrapped by the provided property changes,
     *  then so does the alignment of this label.
     *
     * @param verticalAlign The vertical alignment property which should be applied to the text of the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code verticalAlign} is {@code null}.
     */
    public UIForLabel<L> withVerticalTextPosition( Val<UI.VerticalAlignment> verticalAlign ) {
        NullUtil.nullArgCheck( verticalAlign, "verticalAlign", Val.class );
        NullUtil.nullPropertyCheck( verticalAlign, "verticalAlign", "Null is not a valid alignment." );
        return _withOnShow( verticalAlign, (thisComponent,v) -> {
                    v.forSwing().ifPresent(thisComponent::setVerticalTextPosition);
                })
                ._with( thisComponent -> {
                    verticalAlign.orElseThrow().forSwing().ifPresent(thisComponent::setVerticalTextPosition);
                })
                ._this();
    }

    /**
     *  Use this to set the horizontal and vertical position of the label's text, relative to its image.
     *  This is a convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *         .peek( label -> label.setHorizontalTextPosition(...); label.setVerticalTextPosition(...) );
     *  }</pre>
     *
     * @param alignment The alignment which should be applied to the text of the underlying component.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code alignment} is {@code null}.
     */
    public UIForLabel<L> withTextPosition( UI.Alignment alignment ) {
        NullUtil.nullArgCheck( alignment, "alignment", UI.Alignment.class );
        return _with( thisComponent -> {
                    alignment.getHorizontal().forSwing().ifPresent(thisComponent::setHorizontalTextPosition);
                    alignment.getVertical().forSwing().ifPresent(thisComponent::setVerticalTextPosition);
                })
                ._this();
    }

    /**
     *  Use this to set the icon for the wrapped {@link JLabel}. 
     *  This is in essence a convenience method to avoid peeking into this builder like so:
     *  <pre>{@code
     *     UI.label("Something")
     *         .peek( label -> label.setIcon(...) );
     *  }</pre>
     *
     *
     * @param icon The {@link Icon} which should be displayed on the label.
     * @return This very builder to allow for method chaining.
     */
    public UIForLabel<L> withIcon( Icon icon ) {
        return _with( thisComponent -> {
                    thisComponent.setIcon(icon);
                })
                ._this();
    }

    /**
     *  Use this to set the icon for the wrapped {@link JLabel}
     *  based on the provided {@link IconDeclaration}.
     *  <p>
     *  An {@link IconDeclaration} should be preferred over the {@link Icon} class
     *  as part of a view model, because it is a lightweight value object that merely
     *  models the resource location of the icon even if it is not yet loaded or even
     *  does not exist at all.
     *
     * @param icon The {@link IconDeclaration} which should be displayed on the label.
     * @return This very builder to allow for method chaining.
     */
    public UIForLabel<L> withIcon( IconDeclaration icon ) {
        Objects.requireNonNull(icon,"icon");
        return _with( thisComponent -> {
                    icon.find().ifPresent( i -> thisComponent.setIcon(i) );
                })
                ._this();
    }

    /**
     *  Use this to dynamically set the icon property for the wrapped {@link JLabel}.
     *  When the icon wrapped by the provided property changes,
     *  then so does the icon of this label.
     *  <p>
     *  But note that you may not use the {@link Icon} or {@link ImageIcon} classes directly,
     *  instead <b>you must use implementations of the {@link IconDeclaration} interface</b>,
     *  which merely models the resource location of the icon, but does not load
     *  the whole icon itself.
     *  <p>
     *  The reason for this distinction is the fact that traditional Swing icons
     *  are heavy objects whose loading may or may not succeed, and so they are
     *  not suitable for direct use in a property as part of your view model.
     *  Instead, you should use the {@link IconDeclaration} interface, which is a
     *  lightweight value object that merely models the resource location of the icon
     *  even if it is not yet loaded or even does not exist at all.
     *  <p>
     *  This is especially useful in case of unit tests for you view model,
     *  where the icon may not be available at all, but you still want to test
     *  the behaviour of your view model.
     *
     * @param icon The {@link Icon} property which should be displayed on the label.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code icon} is {@code null}.
     */
    public UIForLabel<L> withIcon( Val<IconDeclaration> icon ) {
        NullUtil.nullArgCheck(icon,"icon",Val.class);
        return _withOnShow( icon, (thisComponent,v) -> {
                    v.find().ifPresent(thisComponent::setIcon);
                })
                ._with( thisComponent -> {
                    icon.orElseThrow().find().ifPresent(thisComponent::setIcon);
                })
                ._this();
    }

    /**
     *  Use this to set the size of the font of the wrapped {@link JLabel}.
     * @param size The size of the font which should be displayed on the label.
     * @return This very builder to allow for method chaining.
     */
    public UIForLabel<L> withFontSize( int size ) {
        return _with( thisComponent -> {
                    Font f = thisComponent.getFont();
                    thisComponent.setFont(f.deriveFont((float)size));
                })
                ._this();
    }

    /**
     *  Use this to dynamically set the size of the font of the wrapped {@link JLabel}
     *  through the provided view model property.
     *  When the integer wrapped by the provided property changes,
     *  then so does the font size of this label.
     *
     * @param size The size property of the font which should be displayed on the label.
     * @return This very builder to allow for method chaining.
     * @throws IllegalArgumentException if {@code size} is {@code null}.
     */
    public UIForLabel<L> withFontSize( Val<Integer> size ) {
        NullUtil.nullArgCheck( size, "size", Val.class );
        NullUtil.nullPropertyCheck( size, "size", "Use the default font size of this component instead of null!" );
        return _withOnShow( size, (thisComponent,v) -> {
                    Font f = thisComponent.getFont();
                    thisComponent.setFont(f.deriveFont((float)v));
                })
                ._with( thisComponent -> {
                    Font f = thisComponent.getFont();
                    thisComponent.setFont(f.deriveFont((float)size.orElseThrow()));
                })
                ._this();
    }

    /**
     *  Use this to set the font of the wrapped {@link JLabel}.
     * @param font The font of the text which should be displayed on the label.
     * @return This builder instance, to allow for method chaining.
     * @throws IllegalArgumentException if {@code font} is {@code null}.
     */
    public final UIForLabel<L> withFont( Font font ) {
        NullUtil.nullArgCheck(font, "font", Font.class, "Use 'UI.FONT_UNDEFINED' instead of null!");
        return _with( thisComponent -> {
                    if ( _isUndefinedFont(font) )
                        thisComponent.setFont(null);
                    else
                        thisComponent.setFont(font);
                })
                ._this();
    }

    /**
     *  Use this to dynamically set the font of the wrapped {@link JLabel}
     *  through the provided view model property.
     *  When the font wrapped by the provided property changes,
     *  then so does the font of this label.
     *
     * @param font The font property of the text which should be displayed on the label.
     * @return This builder instance, to allow for method chaining.
     * @throws IllegalArgumentException if {@code font} is {@code null}.
     * @throws IllegalArgumentException if {@code font} is a property which can wrap {@code null}.
     */
    public final UIForLabel<L> withFont( Val<Font> font ) {
        NullUtil.nullArgCheck(font, "font", Val.class);
        NullUtil.nullPropertyCheck(font, "font", "Use the default font of this component instead of null!");
        return _withOnShow( font, (thisComponent,v) -> {
                    if ( _isUndefinedFont(v) )
                        thisComponent.setFont(null);
                    else
                        thisComponent.setFont(v);
                })
                ._with( thisComponent -> {
                    thisComponent.setFont(font.orElseThrow());
                })
                ._this();
    }

}