UIForScrollPane.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.api.Configurator;
import swingtree.components.JScrollPanels;
import swingtree.components.listener.NestedJScrollPanelScrollCorrection;
import swingtree.layout.AddConstraint;
import swingtree.layout.Bounds;
import swingtree.layout.Size;
import javax.swing.*;
import java.awt.*;
import java.util.Objects;
/**
* A SwingTree builder node designed for configuring {@link JScrollPane} instances. <br>
* Use {@link UI#scrollPane()} or {@link UI#scrollPane(Configurator)} to create a new instance
* of this builder type.
*
* @param <P> The type of {@link JScrollPane} that this {@link UIForScrollPane} is configuring.
*/
public final class UIForScrollPane<P extends JScrollPane> extends UIForAnyScrollPane<UIForScrollPane<P>,P>
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(UIForScrollPane.class);
private final BuilderState<P> _state;
private final @Nullable Configurator<ScrollableComponentDelegate> _configurator;
/**
* {@link UIForAnySwing} (sub)types always wrap
* a single component for which they are responsible.
*
* @param state The {@link BuilderState} modelling how the underlying component is build.
*/
UIForScrollPane( BuilderState<P> state ) {
this(state, null);
}
/**
* {@link UIForAnySwing} (sub)types always wrap
* a single component for which they are responsible.
*
* @param state The {@link BuilderState} modelling how the underlying component is build.
* @param configurator A {@link Configurator} that can be used to configure how the content
* of the {@link JScrollPane} relates to the {@link JScrollPane} itself.
*/
UIForScrollPane( BuilderState<P> state, @Nullable Configurator<ScrollableComponentDelegate> configurator ) {
Objects.requireNonNull(state);
_state = state.withMutator(thisComponent -> {
if ( !(thisComponent instanceof UI.ScrollPane) && !(thisComponent instanceof JScrollPanels) )
thisComponent.addMouseWheelListener(new NestedJScrollPanelScrollCorrection(thisComponent));
});
_configurator = configurator;
}
@Override
protected BuilderState<P> _state() {
return _state;
}
@Override
protected UIForScrollPane<P> _newBuilderWithState( BuilderState<P> newState ) {
return new UIForScrollPane<>(newState, _configurator);
}
/**
* We override this method to wrap the added component in a {@link ScrollableBox} instance
* in case a {@link Configurator} for {@link ScrollableComponentDelegate} instances was provided.
* We then use the continuously supplied {@link ScrollableComponentDelegate} objects
* to satisfy the needs of the {@link Scrollable} implementation of the {@link ScrollableBox}. <br>
* <b>
* This way the user can take advantage of the Scrollable
* interface without the need for a custom implementation
* </b>
*
* @param thisComponent The component which is wrapped by this builder.
* @param addedComponent A component instance which ought to be added to the wrapped component type.
* @param constraints The layout constraint which ought to be used to add the component to the wrapped component type.
*/
@Override
protected void _addComponentTo( P thisComponent, JComponent addedComponent, @Nullable AddConstraint constraints ) {
if ( _configurator != null ) {
if ( addedComponent instanceof Scrollable ) {
log.warn(
"Trying to add a 'Scrollable' component to a declarative scroll pane UI which is already " +
"configured with a SwingTree based Scrollable through the 'UI.scrollPane(Configurator)' method. " +
"The provided component of type '"+addedComponent.getClass().getName()+"' is most likely not intended to be used this way.",
new Throwable()
);
}
ScrollableBox wrapper = new ScrollableBox(thisComponent, addedComponent, _configurator);
if ( constraints != null ) {
wrapper.add(addedComponent, constraints.toConstraintForLayoutManager());
} else {
wrapper.add(addedComponent, "grow");
/*
If we do not use a Scrollable panel and add the component directly
it will be placed and sized to fill the viewport by default.
But when using a Scrollable wrapper, we have an indirection which causes the
content to NOT fill out the viewport.
This "grow" keyword ensures that MigLayout produces a layout
that mimics what is expected...
*/
}
super._addComponentTo(thisComponent, wrapper, null);
}
else
super._addComponentTo(thisComponent, addedComponent, constraints);
}
/**
* A simple internal wrapper type for {@link JComponent} instances that are to be used as the content
* of a {@link JScrollPane} and that should have a special scroll behaviour defined
* by a {@link ScrollableComponentDelegate} instance whose configuration is
* delivered to the scroll pane through this class implementing the {@link Scrollable} interface.
*/
private static class ScrollableBox extends ThinDelegationBox implements Scrollable
{
private final JScrollPane _parent;
private final Configurator<ScrollableComponentDelegate> _configurator;
ScrollableBox( JScrollPane parent, JComponent child, Configurator<ScrollableComponentDelegate> configurator ) {
super(child);
_parent = parent;
_configurator = configurator;
}
private ScrollableComponentDelegate _createNewScrollableConf() {
int averageBlockIncrement = 10;
int averageUnitIncrement = 10;
try {
int verticalBlockIncrement = _parent.getVerticalScrollBar().getBlockIncrement();
int horizontalBlockIncrement = _parent.getHorizontalScrollBar().getBlockIncrement();
averageBlockIncrement = (verticalBlockIncrement + horizontalBlockIncrement) / 2;
} catch ( Exception e ) {
log.error("Error while calculating average block increment for scrollable component.", e);
}
try {
int verticalUnitIncrement = _parent.getVerticalScrollBar().getUnitIncrement();
int horizontalUnitIncrement = _parent.getHorizontalScrollBar().getUnitIncrement();
averageUnitIncrement = (verticalUnitIncrement + horizontalUnitIncrement) / 2;
} catch ( Exception e ) {
log.error("Error while calculating average unit increment for scrollable component.", e);
}
ScrollableComponentDelegate delegate = ScrollableComponentDelegate.of(
_parent, _child,
Size.of(_child.getPreferredSize()),
averageUnitIncrement,
averageBlockIncrement
);
try {
delegate = _configurator.configure(delegate);
} catch ( Exception e ) {
log.error("Error while configuring scrollable component.", e);
}
return delegate;
}
@Override
public Dimension getPreferredScrollableViewportSize() {
ScrollableComponentDelegate delegate = _createNewScrollableConf();
try {
return delegate.preferredSize().toDimension();
} catch ( Exception e ) {
log.error("Error while calculating preferred size for scrollable component.", e);
return new Dimension(0, 0);
}
}
@Override
public int getScrollableUnitIncrement( java.awt.@Nullable Rectangle visibleRect, int orientation, int direction ) {
ScrollableComponentDelegate delegate = _createNewScrollableConf();
try {
Bounds bounds = Bounds.none();
if ( visibleRect != null )
bounds = Bounds.of(visibleRect);
UI.Align align = (orientation == SwingConstants.VERTICAL ? UI.Align.VERTICAL : UI.Align.HORIZONTAL);
return delegate.unitIncrement(bounds, align, direction);
} catch ( Exception e ) {
log.error("Error while calculating unit increment for scrollable component.", e);
return 0;
}
}
@Override
public int getScrollableBlockIncrement( java.awt.@Nullable Rectangle visibleRect, int orientation, int direction ) {
ScrollableComponentDelegate delegate = _createNewScrollableConf();
try {
Bounds bounds = Bounds.none();
if ( visibleRect != null )
bounds = Bounds.of(visibleRect);
UI.Align align = (orientation == SwingConstants.VERTICAL ? UI.Align.VERTICAL : UI.Align.HORIZONTAL);
return delegate.blockIncrement(bounds, align, direction);
} catch ( Exception e ) {
log.error("Error while calculating block increment for scrollable component.", e);
return 0;
}
}
@Override
public boolean getScrollableTracksViewportWidth() {
try {
ScrollableComponentDelegate delegate = _createNewScrollableConf();
return delegate.fitWidth();
} catch ( Exception e ) {
log.error("Error while calculating fit width for scrollable component.", e);
return false;
}
}
@Override
public boolean getScrollableTracksViewportHeight() {
try {
ScrollableComponentDelegate delegate = _createNewScrollableConf();
return delegate.fitHeight();
} catch ( Exception e ) {
log.error("Error while calculating fit height for scrollable component.", e);
return false;
}
}
}
}