Tab.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Action;
import sprouts.From;
import sprouts.Val;
import sprouts.Var;
import swingtree.api.IconDeclaration;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Objects;
import java.util.Optional;
/**
* An immutable data carrier exposing everything needed to configure a tab of a {@link JTabbedPane}.
* One can create instances of this through the {@link UI#tab(String)} factory method
* and then add them to instances of a {@link UIForTabbedPane} builder like so: <br>
* <pre>{@code
* UI.tabbedPane()
* .add(UI.tab("one").add(UI.panel().add(..)))
* .add(UI.tab("two").withTip("I give info!").add(UI.label("read me")))
* .add(UI.tab("three").withIcon(someIcon).add(UI.button("click me")))
* }</pre>
* <p>
* <b>Please 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 Tab
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Tab.class);
@Nullable private final JComponent _contents;
@Nullable private final JComponent _headerComponent;
@Nullable private final Val<String> _title;
@Nullable private final Var<Boolean> _isSelected;
@Nullable private final Val<Boolean> _isEnabled;
@Nullable private final Val<Icon> _icon;
@Nullable private final Val<String> _tip;
@Nullable private final Action<ComponentDelegate<JTabbedPane, ChangeEvent>> _onSelected;
@Nullable private final Action<ComponentDelegate<JTabbedPane, MouseEvent>> _onMouseClick;
Tab(
@Nullable JComponent contents,
@Nullable JComponent headerComponent,
@Nullable Val<String> title,
@Nullable Var<Boolean> isSelected,
@Nullable Val<Boolean> isEnabled,
@Nullable Val<Icon> icon,
@Nullable Val<String> tip,
@Nullable Action<ComponentDelegate<JTabbedPane, ChangeEvent>> onSelected,
@Nullable Action<ComponentDelegate<JTabbedPane, MouseEvent>> onMouseClick
) {
if ( headerComponent == null )
NullUtil.nullArgCheck(title,"title",String.class);
if ( title == null )
NullUtil.nullArgCheck(headerComponent,"headerComponent",JComponent.class);
_contents = contents;
_headerComponent = headerComponent;
_title = title;
_isSelected = isSelected;
_isEnabled = isEnabled;
_icon = icon;
_tip = tip;
_onSelected = onSelected;
_onMouseClick = onMouseClick;
}
/**
* Use this to make the tab selected by default.
* @param isSelected The selected state of the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab isSelectedIf( boolean isSelected ) {
if ( _isSelected != null )
log.warn("Selection flag already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, Var.of(isSelected), _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Binds the boolean property passed to this method to the selected state of the tab,
* which means that when the state of the property changes, the selected state of the tab will change accordingly.
* Conversely, when the tab is selected, the property will be set to true, otherwise it will be set to false.
*
* @param isSelected The selected state property of the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab isSelectedIf( Var<Boolean> isSelected ) {
NullUtil.nullArgCheck(isSelected,"isSelected",Val.class);
if ( _isSelected != null )
log.warn("Selection flag already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, isSelected, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Binds the boolean property passed to this method to the selected state of the tab,
* which means that when the state of the property changes, the selected state of the tab will change accordingly.
* Note that this is not a two-way binding, so when the user changes the selection state of the tab,
* the property will not be updated.
*
* @param isSelected The selected state property of the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab isSelectedIf( Val<Boolean> isSelected ) {
NullUtil.nullArgCheck(isSelected,"isSelected",Val.class);
if ( _isSelected != null )
log.warn("Selection flag already specified!", new Throwable());
Var<Boolean> isSelectedMut = Var.of(isSelected.get());
isSelected.onChange(From.VIEW_MODEL, it -> isSelectedMut.set(it.get()) );
return new Tab(_contents, _headerComponent, _title, isSelectedMut, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Binds the boolean selection state of the tab to a specific enum value
* of a corresponding enum property.
* When the enum property is set to the provided enum value, the tab will be selected.
*
* @param state The state of the tab.
* @param selectedState The selected state property of the tab.
* @param <E> The type of the state.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final <E extends Enum<E>> Tab isSelectedIf( E state, Var<E> selectedState ) {
NullUtil.nullArgCheck(state,"state",Enum.class);
NullUtil.nullArgCheck(selectedState,"selectedState",Var.class);
if ( _isSelected != null )
log.warn("Selection flag already specified!", new Throwable());
Var<Boolean> isSelected = Var.of(state == selectedState.get());
selectedState.onChange(From.VIEW_MODEL, it -> isSelected.set(state == it.get()) );
isSelected.onChange(From.VIEW_MODEL, it -> { if ( it.get() ) selectedState.set(state); });
return new Tab(_contents, _headerComponent, _title, isSelected, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* A tab may be enabled or disabled, which you can specify with this method.
*
* @param isEnabled The enabled state of the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab isEnabledIf( boolean isEnabled ) {
if ( _isEnabled != null )
log.warn("Enabled flag already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, _isSelected, Val.of(isEnabled), _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Binds the boolean property passed to this method to the enabled state of the tab,
* which means that when the state of the property changes, the enabled state of the tab will change accordingly.
* @param isEnabled The enabled state property of the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab isEnabledIf( Val<Boolean> isEnabled ) {
NullUtil.nullArgCheck(isEnabled,"isEnabled",Val.class);
if ( _isEnabled != null )
log.warn("Enabled flag already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, _isSelected, isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Binds the boolean enabled state of the tab to a specific enum value
* and a corresponding enum property.
* When the enum property is set to the provided enum value, the tab will be selected.
*
* @param state The state of the tab.
* @param enabledState The enabled state property of the tab.
* @param <E> The type of the state.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final <E extends Enum<E>> Tab isEnabledIf( E state, Var<E> enabledState ) {
NullUtil.nullArgCheck(state,"state",Enum.class);
NullUtil.nullArgCheck(enabledState,"enabledState",Var.class);
if ( _isEnabled != null )
log.warn("Enabled flag already specified!", new Throwable());
Var<Boolean> isEnabled = Var.of(state == enabledState.get());
enabledState.onChange(From.VIEW_MODEL, it -> isEnabled.set(state == it.get()) );
return new Tab(_contents, _headerComponent, _title, _isSelected, isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* A tab header may have an icon displayed in it, which you can specify with this method.
*
* @param icon The icon which should be displayed in the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withIcon( Icon icon ) {
NullUtil.nullArgCheck(icon,"icon",Icon.class);
if ( _icon != null )
log.warn("Icon already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, Val.of(icon), _tip, _onSelected, _onMouseClick);
}
/**
* Determines the icon to be displayed in the tab header based on a {@link IconDeclaration},
* which is essentially just a path to the icon which should be displayed in the tab header.
* If the icon resource is not found, then no icon will be displayed.
*
* @param icon The icon declaration, essentially just a path to the icon which should be displayed in the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withIcon( IconDeclaration icon ) {
Objects.requireNonNull(icon);
return icon.find().map(this::withIcon).orElse(this);
}
/**
* Allows you to dynamically model the icon displayed on the tab through a property bound
* to this tab.
* <p>
* Note that you may not use the {@link Icon} or {@link ImageIcon} classes directly
* as a value for your property,
* 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 iconDeclaration The icon property which should be displayed in the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withIcon( Val<IconDeclaration> iconDeclaration ) {
NullUtil.nullArgCheck(iconDeclaration,"icon",Val.class);
if ( _icon != null )
log.warn("Icon already specified!", new Throwable());
Val<Icon> asIcon = iconDeclaration.viewAs( Icon.class, it -> it.find().orElse(null) );
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, asIcon, _tip, _onSelected, _onMouseClick);
}
/**
* Allows you to define the tooltip which should be displayed when hovering over the tab header.
*
* @param tip The tooltip which should be displayed when hovering over the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withTip( String tip ) {
NullUtil.nullArgCheck(tip,"tip",String.class);
if ( _tip != null )
log.warn("Tip already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, _icon, Val.of(tip), _onSelected, _onMouseClick);
}
/**
* Allows you to bind a string property to the tooltip of the tab.
* When the item of the property changes, the tooltip will be updated accordingly.
* You can see the tooltip when hovering over the tab header.
*
* @param tip The tooltip property which should be displayed when hovering over the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withTip( Val<String> tip ) {
NullUtil.nullArgCheck(tip,"tip",String.class);
if ( _tip != null )
log.warn("Tip already specified!", new Throwable());
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, _icon, tip, _onSelected, _onMouseClick);
}
public final Tab withHeader( JComponent headerComponent ) {
NullUtil.nullArgCheck(headerComponent,"headerComponent",JComponent.class);
if ( _headerComponent != null )
log.warn("Header component already specified!", new Throwable());
return new Tab(_contents, headerComponent, _title, _isSelected, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Use this to add custom components to the tab header like buttons,
* or labels with icons.
*
* @param headerComponent The component which should be displayed in the tab header.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab withHeader( UIForAnySwing<?,?> headerComponent ) {
NullUtil.nullArgCheck(headerComponent,"headerComponent", UIForAnySwing.class);
return this.withHeader( headerComponent.getComponent() );
}
/**
* Use this to add the contents UI to the tab.
*
* @param contents The contents which should be displayed in the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab add( UIForAnySwing<?,?> contents ) {
if ( _contents != null )
log.warn("Content component already specified!", new Throwable());
return new Tab(contents.getComponent(), _headerComponent, _title, _isSelected, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Use this to add the contents UI to the tab.
*
* @param contents The contents which should be displayed in the tab.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab add( JComponent contents ) {
if ( _contents != null )
log.warn("Content component already specified!", new Throwable());
return new Tab(contents, _headerComponent, _title, _isSelected, _isEnabled, _icon, _tip, _onSelected, _onMouseClick);
}
/**
* Use this to register and catch generic {@link ChangeEvent} based selection events for this tab
* and perform some action when the tab is selected.
*
* @param onSelected The action to be executed when the tab is selected.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab onSelection( Action<ComponentDelegate<JTabbedPane, ChangeEvent>> onSelected ) {
if ( _onSelected != null )
onSelected = _onSelected.andThen(onSelected);
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, _icon, _tip, onSelected, _onMouseClick);
}
/**
* Use this to register and catch generic {@link MouseListener} based mouse click events for this tab.
* This method adds the provided consumer lambda to the {@link JTabbedPane} that this tab is added to.
*
* @param onClick The lambda instance which will be passed to the {@link JTabbedPane} as {@link MouseListener}.
* @return A new {@link Tab} instance with the provided argument, which enables builder-style method chaining.
*/
public final Tab onMouseClick( Action<ComponentDelegate<JTabbedPane, MouseEvent>> onClick ) {
if ( _onMouseClick != null )
onClick = _onMouseClick.andThen(onClick);
return new Tab(_contents, _headerComponent, _title, _isSelected, _isEnabled, _icon, _tip, _onSelected, onClick);
}
final Optional<JComponent> contents() { return Optional.ofNullable(_contents); }
final Optional<Val<String>> title() { return Optional.ofNullable(_title); }
final Optional<Var<Boolean>> isSelected() { return Optional.ofNullable(_isSelected); }
final Optional<Val<Boolean>> isEnabled() { return Optional.ofNullable(_isEnabled); }
final Optional<Val<Icon>> icon() { return Optional.ofNullable(_icon); }
final Optional<Val<String>> tip() { return Optional.ofNullable(_tip); }
final Optional<JComponent> headerContents() { return Optional.ofNullable(_headerComponent); }
final Optional<Action<ComponentDelegate<JTabbedPane, ChangeEvent>>> onSelection() { return Optional.ofNullable(_onSelected); }
final Optional<Action<ComponentDelegate<JTabbedPane, MouseEvent>>> onMouseClick() { return Optional.ofNullable(_onMouseClick); }
}