All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.extended.accordion.AccordionLayout Maven / Gradle / Ivy

/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.extended.accordion;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.api.clone.behavior.OmitOnClone;
import com.alee.api.data.BoxOrientation;
import com.alee.api.merge.Mergeable;
import com.alee.api.merge.behavior.OmitOnMerge;
import com.alee.laf.grouping.AbstractGroupingLayout;
import com.alee.managers.animation.easing.Easing;
import com.alee.managers.animation.transition.AbstractTransition;
import com.alee.managers.animation.transition.TimedTransition;
import com.alee.managers.animation.transition.Transition;
import com.alee.managers.animation.transition.TransitionAdapter;
import com.alee.painter.decoration.DecorationUtils;
import com.alee.utils.SwingUtils;
import com.alee.utils.general.Pair;
import com.alee.utils.parsing.DurationUnits;
import com.thoughtworks.xstream.annotations.XStreamAlias;

import javax.swing.*;
import java.awt.*;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 * {@link LayoutManager} for {@link AccordionPane}.
 *
 * @author Mikle Garin
 * @see How to use WebAccordion
 * @see WebAccordion
 * @see AccordionPane
 * @see AccordionModel
 * @see WebAccordionModel
 */
@XStreamAlias ( "AccordionLayout" )
public class AccordionLayout extends AbstractGroupingLayout implements Mergeable, Cloneable, Serializable
{
    /**
     * Gap in pixels between {@link AccordionPane}s.
     * Cane also be {@code null} in which case default value will be used.
     */
    @Nullable
    protected Integer panesGap;

    /**
     * {@link Easing} used for expansion and collapse animation.
     * Can be set to {@code null} to disable animated transition.
     */
    @Nullable
    protected Easing easing;

    /**
     * Single transition duration.
     * Can be set to {@code null} or {@code 0} to disable animated transition.
     *
     * @see DurationUnits for more information on duration format
     */
    @Nullable
    protected String duration;

    /**
     * Transition used for expansion and collapse animations.
     */
    @OmitOnClone
    @OmitOnMerge
    protected transient Map transitions;

    /**
     * Fractional size of the contents, could only have values between {@code 0.0f} and {@code 1.0f}.
     * Whenever content is fully visible it is {@code 1.0f}, whenever content is not visible it is {@code 0.0f}.
     * This is a lazy map and will not contain all sizes at all times, use {@link #size(WebAccordion, String)} to retrieve live values.
     */
    @OmitOnClone
    @OmitOnMerge
    protected transient Map contentSizes;

    /**
     * Constructs new {@link AccordionLayout} that doesn't use any animations.
     */
    public AccordionLayout ()
    {
        this ( null, null );
    }

    /**
     * Constructs new {@link AccordionLayout} that doesn't use any animations.
     *
     * @param easing   {@link Easing} used for expansion and collapse animation or {@code null} to disable animation
     * @param duration single transition duration in milliseconds or either {@code null} or {@code 0L} to disable animation
     */
    public AccordionLayout ( @Nullable final Easing easing, @Nullable final Long duration )
    {
        setEasing ( easing );
        setDuration ( duration );
    }

    /**
     * Returns gap in pixels between {@link AccordionPane}s.
     *
     * @return gap in pixels between {@link AccordionPane}s
     */
    public int getPanesGap ()
    {
        return panesGap != null ? panesGap : 0;
    }

    /**
     * Sets gap in pixels between {@link AccordionPane}s
     *
     * @param gap gap in pixels between {@link AccordionPane}s
     */
    public void setPanesGap ( final int gap )
    {
        this.panesGap = gap;
    }

    /**
     * Returns {@link Easing} used for expansion and collapse animation or {@code null} if animation is disabled.
     *
     * @return {@link Easing} used for expansion and collapse animation or {@code null} if animation is disabled
     */
    @Nullable
    public Easing getEasing ()
    {
        return easing;
    }

    /**
     * Sets {@link Easing} used for expansion and collapse animation or {@code null} to disable animation.
     *
     * @param easing {@link Easing} used for expansion and collapse animation or {@code null} to disable animation
     */
    public void setEasing ( @Nullable final Easing easing )
    {
        this.easing = easing;
    }

    /**
     * Returns single transition duration in milliseconds or either {@code null} or {@code 0L} if animation is disabled
     *
     * @return single transition duration in milliseconds or either {@code null} or {@code 0L} if animation is disabled
     */
    public long getDuration ()
    {
        return duration != null ? DurationUnits.get ().fromString ( duration ) : 0L;
    }

    /**
     * Sets single transition duration in milliseconds or either {@code null} or {@code 0L} to disable animation.
     *
     * @param duration single transition duration in milliseconds or either {@code null} or {@code 0L} to disable animation
     */
    public void setDuration ( @Nullable final Long duration )
    {
        this.duration = duration != null ? DurationUnits.get ().toString ( duration ) : null;
    }

    /**
     * Installs this {@link AccordionModel} into specified {@link WebAccordion}.
     *
     * @param accordion {@link WebAccordion} this model should be installed into
     */
    public void install ( @NotNull final WebAccordion accordion )
    {
        // Setting up resources
        contentSizes = new HashMap ( accordion.getComponentCount () );
        transitions = new HashMap ( 3 );

        // Re-adding components added before this layout was set into accordion
        for ( final AccordionPane pane : accordion.getPanes () )
        {
            addComponent ( pane, null );
            paneAdded ( accordion, pane );
        }
    }

    /**
     * Uninstalls this {@link AccordionModel} from the specified {@link WebAccordion}.
     *
     * @param accordion {@link WebAccordion} this model should be uninstalled from
     */
    public void uninstall ( @NotNull final WebAccordion accordion )
    {
        // Removing all components from this layout
        for ( final AccordionPane pane : accordion.getPanes () )
        {
            removeComponent ( pane );
            paneRemoved ( accordion, pane );
        }

        // Cleaning up resources
        transitions = null;
        contentSizes = null;
    }

    /**
     * Informs about {@link AccordionPane} addition to {@link WebAccordion}.
     * This method is used instead of the layout one to ensure the operations order is correct.
     *
     * @param accordion {@link WebAccordion}
     * @param pane      added {@link AccordionPane}
     */
    public void paneAdded ( @NotNull final WebAccordion accordion, @NotNull final AccordionPane pane )
    {
        size ( accordion, pane.getId () );
    }

    /**
     * Informs about {@link AccordionPane} removal from {@link WebAccordion}.
     * This method is used instead of the layout one to ensure the operations order is correct.
     *
     * @param accordion {@link WebAccordion}
     * @param pane      removed {@link AccordionPane}
     */
    public void paneRemoved ( @NotNull final WebAccordion accordion, @NotNull final AccordionPane pane )
    {
        // Stopping transition
        final AbstractTransition transition = transitions.get ( pane.getId () );
        if ( transition != null )
        {
            transition.stop ();
            transitions.remove ( pane.getId () );
        }

        // Removing cached size
        contentSizes.remove ( pane.getId () );
    }

    /**
     * Returns current content size of {@link AccordionPane} with the specified identifier.
     * If value haven't been stored in this layout yet it will be retrieved from {@link WebAccordion}.
     *
     * @param accordion {@link WebAccordion}
     * @param id        {@link AccordionPane} identifier
     * @return current content size of {@link AccordionPane} with the specified identifier
     */
    protected float size ( final WebAccordion accordion, final String id )
    {
        final float size;
        if ( contentSizes.containsKey ( id ) )
        {
            size = contentSizes.get ( id );
        }
        else
        {
            size = accordion.isPaneExpanded ( id ) ? 1f : 0f;
            contentSizes.put ( id, size );
        }
        return size;
    }

    /**
     * Asks layout to expand {@link AccordionPane} with the specified identifier.
     *
     * @param accordion {@link WebAccordion}
     * @param id        {@link AccordionPane} identifier
     */
    public void expandPane ( @NotNull final WebAccordion accordion, @NotNull final String id )
    {
        changePaneState ( accordion, id, size ( accordion, id ), 1f, new Runnable ()
        {
            @Override
            public void run ()
            {
                final AccordionPane pane = accordion.getPane ( id );
                pane.fireExpanding ( accordion );
                accordion.fireExpanding ( pane );
            }
        }, new Runnable ()
        {
            @Override
            public void run ()
            {
                final AccordionPane pane = accordion.getPane ( id );
                pane.fireExpanded ( accordion );
                accordion.fireExpanded ( pane );
            }
        } );
    }

    /**
     * Asks layout to collapse {@link AccordionPane} with the specified identifier.
     *
     * @param accordion {@link WebAccordion}
     * @param id        {@link AccordionPane} identifier
     */
    public void collapsePane ( @NotNull final WebAccordion accordion, @NotNull final String id )
    {
        changePaneState ( accordion, id, size ( accordion, id ), 0f, new Runnable ()
        {
            @Override
            public void run ()
            {
                final AccordionPane pane = accordion.getPane ( id );
                pane.fireCollapsing ( accordion );
                accordion.fireCollapsing ( pane );
            }
        }, new Runnable ()
        {
            @Override
            public void run ()
            {
                final AccordionPane pane = accordion.getPane ( id );
                pane.fireCollapsed ( accordion );
                accordion.fireCollapsed ( pane );
            }
        } );
    }

    /**
     * Asks layout to change state for {@link AccordionPane} with the specified identifier.
     *
     * @param accordion   {@link WebAccordion}
     * @param id          {@link AccordionPane} identifier
     * @param start       starting {@link AccordionPane} content size
     * @param target      target {@link AccordionPane} content size
     * @param preChange   {@link Runnable} to be executed once state change has started
     * @param afterChange {@link Runnable} to be executed once state change has completed
     */
    protected void changePaneState ( @NotNull final WebAccordion accordion, @NotNull final String id,
                                     final float start, final float target,
                                     @NotNull final Runnable preChange, @NotNull final Runnable afterChange )
    {
        // Make sure we stop previous transition
        final AbstractTransition transition = transitions.get ( id );
        if ( transition != null )
        {
            transition.stop ();
            transitions.remove ( id );
        }

        final Easing easing = getEasing ();
        final long fullDuration = getDuration ();
        if ( accordion.isAnimated () && accordion.isShowing () && easing != null && fullDuration > 0L )
        {
            final TimedTransition stateTransition = new TimedTransition ( start, target, easing, fullDuration );
            transitions.put ( id, stateTransition );

            // Pre-change action
            preChange.run ();

            stateTransition.addListener ( new TransitionAdapter ()
            {
                @Override
                public void adjusted ( final Transition transition, final Float value )
                {
                    // Updating size
                    contentSizes.put ( id, value );
                    SwingUtils.update ( accordion );
                }

                @Override
                public void finished ( final Transition transition, final Float value )
                {
                    SwingUtilities.invokeLater ( new Runnable ()
                    {
                        @Override
                        public void run ()
                        {
                            // Removing unused queue
                            transitions.remove ( id );

                            // Post-change action
                            afterChange.run ();
                        }
                    } );
                }
            } );
            stateTransition.play ();
        }
        else
        {
            // Pre-change action
            preChange.run ();

            // Updating size
            contentSizes.put ( id, target );
            SwingUtils.update ( accordion );

            // Post-change action
            afterChange.run ();
        }
    }

    /**
     * Returns whether or not {@link AccordionPane} with the specified identifier is in transition to either of two expansion states.
     *
     * @param id {@link AccordionPane} identifier
     * @return {@code true} if {@link AccordionPane} with the specified identifier is in transition, {@code false} otherwise
     */
    public boolean isPaneInTransition ( @NotNull final String id )
    {
        return transitions.containsKey ( id );
    }

    @Override
    public void layoutContainer ( @NotNull final Container parent )
    {
        final WebAccordion accordion = ( WebAccordion ) parent;
        final BoxOrientation headerPosition = accordion.getHeaderPosition ();
        final int panesGap = getPanesGap ();
        final boolean vertical = headerPosition.isTop () || headerPosition.isBottom ();
        final int count = accordion.getComponentCount ();
        final int w = parent.getWidth ();
        final int h = parent.getHeight ();
        final Insets insets = parent.getInsets ();
        final int cw = w - insets.left - insets.right;
        final int ch = h - insets.top - insets.bottom;

        float totalExpanded = 0f;
        int expandedCount = 0;
        int freePixels = vertical ? ch : cw;
        final Dimension[] preferred = new Dimension[ count ];
        for ( int i = 0; i < count; i++ )
        {
            final AccordionPane pane = ( AccordionPane ) accordion.getComponent ( i );

            // Pane preferred size
            final Dimension cps = pane.getPreferredSize ();
            preferred[ i ] = cps;

            // Counting expanded panes
            final float expansion = size ( accordion, pane.getId () );
            final boolean fullyExpanded = Float.compare ( expansion, 1f ) == 0;
            final boolean partiallyExpanded = !fullyExpanded && Float.compare ( expansion, 0f ) == 1;
            totalExpanded += fullyExpanded || partiallyExpanded ? expansion : 0f;
            expandedCount += fullyExpanded || partiallyExpanded ? 1 : 0;

            // Pixels left for expanded panes
            freePixels -= vertical ? cps.height : cps.width;
            freePixels -= i > 0 ? panesGap : 0;
        }

        final float totalFreePixels = freePixels;
        int x = insets.left;
        int y = insets.top;
        int expandedPositioned = 0;
        for ( int i = 0; i < count; i++ )
        {
            final AccordionPane pane = ( AccordionPane ) accordion.getComponent ( i );
            final Dimension cps = preferred[ i ];
            final float expanded = size ( accordion, pane.getId () );
            if ( Float.compare ( expanded, 0f ) == 1 )
            {
                // Positioning fully or partially expanded pane
                final int panePixels;
                if ( expandedPositioned < expandedCount - 1 )
                {
                    panePixels = Math.round ( totalFreePixels * expanded / totalExpanded );
                }
                else
                {
                    panePixels = freePixels;
                }
                pane.setBounds (
                        x, y,
                        vertical ? cw : cps.width + panePixels,
                        vertical ? cps.height + panePixels : ch
                );
                freePixels -= panePixels;
                expandedPositioned++;
            }
            else
            {
                // Positioning collapsed pane
                pane.setBounds (
                        x, y,
                        vertical ? cw : cps.width,
                        vertical ? cps.height : ch
                );
            }
            x += vertical ? 0 : pane.getWidth () + panesGap;
            y += vertical ? pane.getHeight () + panesGap : 0;
        }
    }

    /*
    todo Slightly smoother visual size sharing, but have issues with specific cases

    @Override
    public void layoutContainer ( @NotNull final Container parent )
    {
        final WebAccordion accordion = ( WebAccordion ) parent;
        final BoxOrientation headerPosition = accordion.getHeaderPosition ();
        final int panesGap = getPanesGap ();
        final boolean vertical = headerPosition.isTop () || headerPosition.isBottom ();
        final int count = accordion.getComponentCount ();
        final int w = parent.getWidth ();
        final int h = parent.getHeight ();
        final Insets insets = parent.getInsets ();
        final int cw = w - insets.left - insets.right;
        final int ch = h - insets.top - insets.bottom;

        // Calculating total expanded panes size and free space available for the expanded panes
        // Note that "expanded" in this context means that pane content is visible
        // Panes mentioned as expanded further on might be actually expanded or be in process of expanding or even collapsing
        // As long as they are visible on the screen they are interesting for us and will be accounted for
        //float totalExpanded = 0f;
        int fullyExpandedCount = 0;
        int partiallyExpandedCount = 0;
        float totalPartiallyExpanded = 0f;
        int freePixels = vertical ? ch : cw;
        final Dimension[] preferred = new Dimension[ count ];
        for ( int i = 0; i < count; i++ )
        {
            final AccordionPane pane = ( AccordionPane ) accordion.getComponent ( i );

            // Pane preferred size
            final Dimension cps = pane.getPreferredSize ();
            preferred[ i ] = cps;

            // Counting expanded panes
            final float expansion = contentSizes.get ( pane.getId () );
            final boolean fullyExpanded = Float.compare ( expansion, 1f ) == 0;
            final boolean partiallyExpanded = !fullyExpanded && Float.compare ( expansion, 0f ) == 1;
            fullyExpandedCount += fullyExpanded ? 1 : 0;
            partiallyExpandedCount += partiallyExpanded ? 1 : 0;
            totalPartiallyExpanded += partiallyExpanded ? expansion : 0f;

            // Pixels left for expanded panes
            freePixels -= vertical ? cps.height : cps.width;
            freePixels -= i > 0 ? panesGap : 0;
        }
        freePixels = Math.max ( 0, freePixels );

        // Workaround for edge cases when one of the panes already fully collapsed or expanded
        //        if ( partiallyExpandedCount == 1 )
        //        {
        //            if ( Math.round ( totalPartiallyExpanded ) == 1 )
        //            {
        //                fullyExpandedCount += 1;
        //            }
        //            partiallyExpandedCount = 0;
        //        }

        // Calculating total expanded pane sizes
        final int totalPixelsForFullyExpanded;
        final int totalPixelsForPartiallyExpanded;
        if ( fullyExpandedCount > 0 && partiallyExpandedCount > 0 )
        {
            final int totalExpandedPaneSpots = fullyExpandedCount + Math.round ( totalPartiallyExpanded );
            totalPixelsForFullyExpanded = Math.round ( ( float ) freePixels / totalExpandedPaneSpots );
            totalPixelsForPartiallyExpanded = freePixels - totalPixelsForFullyExpanded;
        }
        else if ( fullyExpandedCount > 0 )
        {
            totalPixelsForFullyExpanded = freePixels;
            totalPixelsForPartiallyExpanded = 0;
        }
        else if ( partiallyExpandedCount > 0 )
        {
            totalPixelsForFullyExpanded = 0;
            totalPixelsForPartiallyExpanded = freePixels;
        }
        else
        {
            totalPixelsForFullyExpanded = 0;
            totalPixelsForPartiallyExpanded = 0;
        }

        // Calculating component preferred sizes
        int x = insets.left;
        int y = insets.top;
        int fullyExpandedPositioned = 0;
        int fullyExpandedFreePixels = totalPixelsForFullyExpanded;
        int partiallyExpandedPositioned = 0;
        int partiallyExpandedFreePixels = totalPixelsForPartiallyExpanded;
        for ( int i = 0; i < count; i++ )
        {
            final AccordionPane pane = ( AccordionPane ) accordion.getComponent ( i );
            final Dimension cps = preferred[ i ];
            final float expanded = contentSizes.get ( pane.getId () );
            if ( Float.compare ( expanded, 0f ) == 1 )
            {
                // Positioning fully or partially expanded pane
                final int panePixels;
                if ( Float.compare ( expanded, 1f ) == 0 )
                {
                    if ( fullyExpandedPositioned < fullyExpandedCount - 1 )
                    {
                        panePixels = totalPixelsForFullyExpanded / fullyExpandedCount;
                    }
                    else
                    {
                        panePixels = fullyExpandedFreePixels;
                    }
                    fullyExpandedFreePixels -= panePixels;
                    fullyExpandedPositioned++;
                }
                else
                {
                    if ( partiallyExpandedPositioned < partiallyExpandedCount - 1 )
                    {
                        panePixels = Math.round ( totalPixelsForPartiallyExpanded * expanded / totalPartiallyExpanded );
                    }
                    else
                    {
                        panePixels = partiallyExpandedFreePixels;
                    }
                    partiallyExpandedFreePixels -= panePixels;
                    partiallyExpandedPositioned++;
                }
                pane.setBounds (
                        x, y,
                        vertical ? cw : cps.width + panePixels,
                        vertical ? cps.height + panePixels : ch
                );
                freePixels -= panePixels;
            }
            else
            {
                // Positioning collapsed pane
                pane.setBounds (
                        x, y,
                        vertical ? cw : cps.width,
                        vertical ? cps.height : ch
                );
            }
            x += vertical ? 0 : pane.getWidth () + panesGap;
            y += vertical ? pane.getHeight () + panesGap : 0;
        }
    }*/

    @NotNull
    @Override
    public Dimension preferredLayoutSize ( @NotNull final Container parent )
    {
        final Dimension ps = new Dimension ( 0, 0 );

        final WebAccordion accordion = ( WebAccordion ) parent;
        final BoxOrientation headerPosition = accordion.getHeaderPosition ();
        final int panesGap = getPanesGap ();
        final boolean vertical = headerPosition.isTop () || headerPosition.isBottom ();
        final int count = accordion.getComponentCount ();
        final Insets insets = parent.getInsets ();

        // Including panes preferred sizes
        for ( int i = 0; i < count; i++ )
        {
            final AccordionPane pane = ( AccordionPane ) accordion.getComponent ( i );
            final Dimension cps = pane.getPreferredSize ();
            if ( vertical )
            {
                ps.width = Math.max ( ps.width, cps.width );
                ps.height += cps.height + ( i > 0 ? panesGap : 0 ) + ( i > 0 ? panesGap : 0 );
            }
            else
            {
                ps.width += cps.width + ( i > 0 ? panesGap : 0 ) + ( i > 0 ? panesGap : 0 );
                ps.height = Math.max ( ps.height, cps.height );
            }
        }

        // Accounting for minimum preferred content size
        if ( vertical )
        {
            ps.height += accordion.getMinimumPaneContentSize () * accordion.getMinimumExpandedPaneCount ();
        }
        else
        {
            ps.width += accordion.getMinimumPaneContentSize () * accordion.getMinimumExpandedPaneCount ();
        }

        // Counting insets in
        ps.width += insets.left + insets.right;
        ps.height += insets.top + insets.bottom;

        return ps;
    }

    @NotNull
    @Override
    protected String sides ()
    {
        return sides != null ? sides : ( sides = "0,0,0,0" );
    }

    @NotNull
    @Override
    public Pair getDescriptors ( @NotNull final Container container, @NotNull final Component component, final int index )
    {
        final Pair descriptors;
        if ( getPanesGap () == 0 )
        {
            final WebAccordion accordion = ( WebAccordion ) container;
            final int last = container.getComponentCount () - 1;
            final boolean hor = accordion.getHeaderPosition ().isLeft () || accordion.getHeaderPosition ().isRight ();
            final boolean top = ( hor || index == 0 ) && isPaintTop ();
            final boolean left = ( index == 0 || !hor ) && isPaintLeft ();
            final boolean bottom = ( hor || index == last ) && isPaintBottom ();
            final boolean right = ( index == last || !hor ) && isPaintRight ();
            final String sides = DecorationUtils.toString ( top, left, bottom, right );
            final String lines = DecorationUtils.toString ( false, false, !hor && index != last, hor && index != last );
            descriptors = new Pair ( sides, lines );
        }
        else
        {
            descriptors = new Pair ();
        }
        return descriptors;
    }

    /**
     * The UI resource version of {@link AccordionLayout}.
     */
    @XStreamAlias ( "AccordionLayout$UIResource" )
    public static final class UIResource extends AccordionLayout implements javax.swing.plaf.UIResource
    {
        /**
         * Implementation is used completely from {@link AccordionLayout}.
         */
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy