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

com.alee.laf.grouping.GroupPaneLayout Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
package com.alee.laf.grouping;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.api.data.BoxOrientation;
import com.alee.painter.PainterSupport;
import com.alee.painter.decoration.DecorationUtils;
import com.alee.utils.general.Pair;
import com.alee.utils.swing.SizeType;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;

import javax.swing.*;
import java.awt.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Layout designed specifically for usage within {@link com.alee.laf.grouping.GroupPane} container.
 * It constructs a grid of components to be visually grouped and knows how to retrieve components at specific cells.
 *
 * @author Mikle Garin
 */
@XStreamAlias ( "GroupPaneLayout" )
public class GroupPaneLayout extends AbstractGroupingLayout implements SwingConstants
{
    /**
     * Components placement order orientation.
     */
    @XStreamAsAttribute
    protected int orientation;

    /**
     * Amount of columns used to place components.
     */
    @XStreamAsAttribute
    protected int columns;

    /**
     * Amount of rows used to place components.
     */
    @XStreamAsAttribute
    protected int rows;

    /**
     * Component constraints.
     */
    @NotNull
    protected transient final Map constraints;

    /**
     * Constructs default layout.
     */
    public GroupPaneLayout ()
    {
        this ( HORIZONTAL, Integer.MAX_VALUE, 1 );
    }

    /**
     * Constructs layout with the specified amount of rows and columns.
     *
     * @param orientation components placement order orientation
     */
    public GroupPaneLayout ( final int orientation )
    {
        this ( orientation, Integer.MAX_VALUE, 1 );
    }

    /**
     * Constructs layout with the specified amount of rows and columns.
     *
     * @param columns amount of columns used to place components
     * @param rows    amount of rows used to place components
     */
    public GroupPaneLayout ( final int columns, final int rows )
    {
        this ( HORIZONTAL, columns, rows );
    }

    /**
     * Constructs layout with the specified amount of rows and columns.
     *
     * @param orientation components placement order orientation
     * @param columns     amount of columns used to place components
     * @param rows        amount of rows used to place components
     */
    public GroupPaneLayout ( final int orientation, final int columns, final int rows )
    {
        this.constraints = new HashMap ( 5 );
        this.orientation = orientation;
        this.columns = columns;
        this.rows = rows;
    }

    /**
     * Returns components placement order orientation.
     *
     * @return components placement order orientation
     */
    public int getOrientation ()
    {
        return orientation;
    }

    /**
     * Sets components placement order orientation.
     *
     * @param orientation components placement order orientation
     */
    public void setOrientation ( final int orientation )
    {
        this.orientation = orientation;
    }

    /**
     * Returns amount of columns used to place components.
     *
     * @return amount of columns used to place components
     */
    public int getColumns ()
    {
        return columns;
    }

    /**
     * Sets amount of columns used to place components.
     *
     * @param columns amount of columns to place components
     */
    public void setColumns ( final int columns )
    {
        this.columns = columns;
    }

    /**
     * Returns amount of rows used to place components.
     *
     * @return amount of rows used to place components
     */
    public int getRows ()
    {
        return rows;
    }

    /**
     * Sets amount of rows used to place components.
     *
     * @param rows amount of rows to place components
     */
    public void setRows ( final int rows )
    {
        this.rows = rows;
    }

    /**
     * Returns default component constraints in this layout.
     *
     * @return default component constraints in this layout
     */
    @NotNull
    protected GroupPaneConstraints getDefaultConstraint ()
    {
        return orientation == HORIZONTAL ? GroupPaneConstraints.VERTICAL_FILL : GroupPaneConstraints.HORIZONTAL_FILL;
    }

    @Override
    public void addComponent ( @NotNull final Component component, @Nullable final Object constraints )
    {
        // Saving constraints
        if ( constraints != null && !( constraints instanceof GroupPaneConstraints ) )
        {
            throw new RuntimeException ( "Unsupported layout constraints: " + constraints );
        }
        this.constraints.put ( component, constraints != null ? ( GroupPaneConstraints ) constraints : getDefaultConstraint () );

        // Performing basic operations
        super.addComponent ( component, constraints );
    }

    @Override
    public void removeComponent ( @NotNull final Component component )
    {
        // Performing basic operations
        super.removeComponent ( component );

        // Removing saved constraints
        constraints.remove ( component );
    }

    @Override
    public void layoutContainer ( @NotNull final Container container )
    {
        // Retrieving actual grid size
        final GridSize gridSize = getActualGridSize ( container );

        // Calculating children preferred sizes
        final Pair sizes = calculateSizes ( container, gridSize, SizeType.current );

        // Laying out components
        // To do that we will simply iterate through the whole grid
        // Some cells we will iterate through won't have components, we will simply skip those
        final Insets border = container.getInsets ();
        int y = border.top;
        for ( int row = 0; row < gridSize.rows; row++ )
        {
            int x = border.left;
            for ( int column = 0; column < gridSize.columns; column++ )
            {
                // Converting grid point to component index
                final int index = pointToIndex ( container, column, row, gridSize );

                // Retrieving cell component if it exists
                final Component component = container.getComponent ( index );
                if ( component != null )
                {
                    // Updating its bounds
                    component.setBounds ( x, y, sizes.key[ column ], sizes.value[ row ] );
                }

                // Move forward into grid
                x += sizes.key[ column ];
            }

            // Move forward into grid
            y += sizes.value[ row ];
        }
    }

    @NotNull
    @Override
    public Dimension preferredLayoutSize ( @NotNull final Container container )
    {
        // Retrieving actual grid size
        final GridSize gridSize = getActualGridSize ( container );

        // Calculating children preferred sizes
        final Pair sizes = calculateSizes ( container, gridSize, SizeType.preferred );

        // Calculating preferred size
        final Dimension ps = new Dimension ( 0, 0 );
        for ( final Integer columnWith : sizes.key )
        {
            ps.width += columnWith;
        }
        for ( final Integer rowHeight : sizes.value )
        {
            ps.height += rowHeight;
        }
        final Insets border = container.getInsets ();
        ps.width += border.left + border.right;
        ps.height += border.top + border.bottom;

        return ps;
    }

    /**
     * Returns actual grid size according to container components amount.
     * Actual grid size is very important for all calculations as it defines the final size of the grid.
     * 

* For example: Layout settings are set to have 5 columns and 5 rows which in total requires 25 components to fill-in the grid. * Though there might not be enough components provided to fill the grid, in that case the actual grid size might be less. * * @param container group pane * @return actual grid size according to container components amount */ @NotNull protected GridSize getActualGridSize ( @NotNull final Container container ) { final GridSize gridSize; final int count = container.getComponentCount (); if ( orientation == HORIZONTAL ) { gridSize = new GridSize ( Math.min ( count, columns ), ( count - 1 ) / columns + 1 ); } else { gridSize = new GridSize ( ( count - 1 ) / rows + 1, Math.min ( count, rows ) ); } return gridSize; } /** * Returns component at the specified cell. * * @param container group pane * @param column component column * @param row component row * @return component at the specified cell */ @Nullable protected Component getComponentAt ( @NotNull final Container container, final int column, final int row ) { final GridSize gridSize = getActualGridSize ( container ); final int index = pointToIndex ( container, column, row, gridSize ); final int count = container.getComponentCount (); return index < count ? container.getComponent ( index ) : null; } /** * Returns grid column in which component under the specified index is placed. * * @param container group pane * @param index component index * @param gridSize actual grid size * @return grid column in which component under the specified index is placed */ protected int indexToColumn ( @NotNull final Container container, final int index, @NotNull final GridSize gridSize ) { final boolean ltr = container.getComponentOrientation ().isLeftToRight (); final int column = orientation == HORIZONTAL ? index % columns : index / rows; return ltr ? column : gridSize.columns - 1 - column; } /** * Returns grid row in which component under the specified index is placed. * * @param index component index * @return grid row in which component under the specified index is placed */ protected int indexToRow ( final int index ) { return orientation == HORIZONTAL ? index / columns : index % rows; } /** * Returns index of the component placed in the specified grid cell or {@code null} if cell is empty. * * @param container group pane * @param column grid column index * @param row grid row index * @param gridSize actual grid size * @return index of the component placed in the specified grid cell or {@code null} if cell is empty */ protected int pointToIndex ( @NotNull final Container container, final int column, final int row, @NotNull final GridSize gridSize ) { final boolean ltr = container.getComponentOrientation ().isLeftToRight (); final int c = ltr ? column : gridSize.columns - 1 - column; return orientation == HORIZONTAL ? row * columns + c : c * rows + row; } /** * Returns column and row sizes. * * @param container group pane * @param gridSize actual grid size * @param type requested sizes type * @return column and row sizes */ @NotNull protected Pair calculateSizes ( @NotNull final Container container, @NotNull final GridSize gridSize, @NotNull final SizeType type ) { final int count = container.getComponentCount (); // Calculating initially available column and row sizes final int cols = gridSize.columns; final int[] colWidths = new int[ cols ]; final double[] colPercents = new double[ cols ]; final int rows = gridSize.rows; final int[] rowHeights = new int[ rows ]; final double[] rowPercents = new double[ rows ]; for ( int i = 0; i < count; i++ ) { final Component component = container.getComponent ( i ); final GroupPaneConstraints c = constraints.get ( component ); final Dimension ps = component.getPreferredSize (); final int col = indexToColumn ( container, i, gridSize ); final int row = indexToRow ( i ); colWidths[ col ] = Math.max ( colWidths[ col ], ( int ) Math.floor ( c.width > 1 ? c.width : ps.width ) ); colPercents[ col ] = Math.max ( colPercents[ col ], 1 >= c.width && c.width > 0 ? c.width : 0 ); rowHeights[ row ] = Math.max ( rowHeights[ row ], ( int ) Math.floor ( c.height > 1 ? c.height : ps.height ) ); rowPercents[ row ] = Math.max ( rowPercents[ row ], 1 >= c.height && c.height > 0 ? c.height : 0 ); } // Calculating resulting column and row sizes final Dimension size = container.getSize (); final Pair rc = calculateSizes ( cols, size.width, colWidths, colPercents ); final Pair rr = calculateSizes ( rows, size.height, rowHeights, rowPercents ); // Updating sizes with current values // This block is only performed for actual layout operation if ( type == SizeType.current ) { for ( int i = 0; i < count; i++ ) { final int col = indexToColumn ( container, i, gridSize ); if ( colPercents[ col ] > 0 && colPercents[ col ] <= 1 ) { final int pw = ( int ) Math.floor ( rc.getValue () * colPercents[ col ] / rc.getKey () ); colWidths[ col ] = Math.max ( pw, colWidths[ col ] ); } final int row = indexToRow ( i ); if ( rowPercents[ row ] > 0 && rowPercents[ row ] <= 1 ) { final int ph = ( int ) Math.floor ( rr.getValue () * rowPercents[ row ] / rr.getKey () ); rowHeights[ row ] = Math.max ( ph, rowHeights[ row ] ); } } appendDelta ( cols, colWidths, size.width ); appendDelta ( rows, rowHeights, size.height ); } return new Pair ( colWidths, rowHeights ); } /** * Calculates proper component sizes along with percents summ and free size. * * @param count parts count * @param size total available size * @param sizes part sizes * @param percents part percentages * @return percents summ and free size pair */ @NotNull protected Pair calculateSizes ( final int count, final int size, @NotNull final int[] sizes, @NotNull final double[] percents ) { final int[] initSizes = Arrays.copyOf ( sizes, count ); boolean changed; double maxWeight; double freePercents; int freeSize; do { changed = false; // Determining max column and row weights maxWeight = 0; for ( int i = 0; i < count; i++ ) { if ( percents[ i ] > 0 ) { maxWeight = Math.max ( maxWeight, initSizes[ i ] / percents[ i ] ); } } // Applying column and row weights for ( int i = 0; i < count; i++ ) { if ( percents[ i ] > 0 ) { sizes[ i ] = ( int ) Math.floor ( maxWeight * percents[ i ] ); } else { sizes[ i ] = initSizes[ i ]; } } // Calculating summary of percent sizes and free pixel size freeSize = size; freePercents = 0; for ( int i = 0; i < count; i++ ) { freeSize -= percents[ i ] == 0 ? sizes[ i ] : 0; freePercents += percents[ i ]; } // Normalize percents so that fill parts will be able to take less than 100% of free space // So far it have been disabled due to some minor shrinking issues // freePercents = Math.max ( 1, freePercents ); // Stop parts from shrinking below their preferred size for ( int i = 0; i < count; i++ ) { if ( percents[ i ] > 0 ) { final double availSize = freeSize * percents[ i ] / freePercents; if ( sizes[ i ] > availSize && initSizes[ i ] > availSize ) { percents[ i ] = 0; changed = true; break; } } } } while ( changed ); return new Pair ( freePercents, freeSize ); } /** * Appends delta space equally to last elements to properly fill in all available space. * * @param count parts count * @param sizes part sizes * @param size total available size */ protected void appendDelta ( final int count, @NotNull final int[] sizes, final int size ) { int roughColSize = 0; for ( int i = 0; i < count; i++ ) { roughColSize += sizes[ i ]; } int delta = size - roughColSize; if ( delta < count ) { for ( int i = count - 1; delta > 0; i--, delta-- ) { sizes[ i ]++; } } } @NotNull @Override public Pair getDescriptors ( @NotNull final Container container, @NotNull final Component component, final int index ) { // Retrieving actual grid size final GridSize gridSize = getActualGridSize ( container ); // Retrieving component position final int row = indexToRow ( index ); final int col = indexToColumn ( container, index, gridSize ); // Calculating descriptors values final boolean paintTop; final boolean paintTopLine; final boolean paintLeft; final boolean paintLeftLine; final boolean paintBottom; final boolean paintBottomLine; final boolean paintRight; final boolean paintRightLine; if ( isNeighbourDecoratable ( container, gridSize, col, row, BoxOrientation.top ) ) { paintTop = false; paintTopLine = false; } else if ( !isPaintTop () && isAtBorder ( container, gridSize, col, row, BoxOrientation.top ) ) { paintTop = false; paintTopLine = false; } else { paintTop = true; paintTopLine = false; } if ( isNeighbourDecoratable ( container, gridSize, col, row, BoxOrientation.left ) ) { paintLeft = false; paintLeftLine = false; } else if ( !isPaintLeft () && isAtBorder ( container, gridSize, col, row, BoxOrientation.left ) ) { paintLeft = false; paintLeftLine = false; } else { paintLeft = true; paintLeftLine = false; } if ( isNeighbourDecoratable ( container, gridSize, col, row, BoxOrientation.bottom ) ) { paintBottom = false; paintBottomLine = true; } else if ( !isPaintBottom () && isAtBorder ( container, gridSize, col, row, BoxOrientation.bottom ) ) { paintBottom = false; paintBottomLine = false; } else { paintBottom = true; paintBottomLine = false; } if ( isNeighbourDecoratable ( container, gridSize, col, row, BoxOrientation.right ) ) { paintRight = false; paintRightLine = true; } else if ( !isPaintRight () && isAtBorder ( container, gridSize, col, row, BoxOrientation.right ) ) { paintRight = false; paintRightLine = false; } else { paintRight = true; paintRightLine = true; } // Returning descriptors final String sides = DecorationUtils.toString ( paintTop, paintLeft, paintBottom, paintRight ); final String lines = DecorationUtils.toString ( paintTopLine, paintLeftLine, paintBottomLine, paintRightLine ); return new Pair ( sides, lines ); } /** * Returns whether or not neighbour painter for component at the specified column/row is decoratable. * * @param container container * @param gridSize actual grid size * @param col component column * @param row component row * @param direction neighbour direction * @return {@code true} if neighbour painter for component at the specified column/row is decoratable, {@code false} otherwise */ protected boolean isNeighbourDecoratable ( @NotNull final Container container, @NotNull final GridSize gridSize, final int col, final int row, @NotNull final BoxOrientation direction ) { final Component neighbour = getNeighbour ( container, gridSize, col, row, direction ); return PainterSupport.isDecoratable ( neighbour ); } /** * Returns neighbour for component at the specified column/row. * * @param container container * @param gridSize actual grid size * @param col component column * @param row component row * @param direction neighbour direction * @return neighbour for component at the specified column/row */ @Nullable protected Component getNeighbour ( @NotNull final Container container, @NotNull final GridSize gridSize, final int col, final int row, @NotNull final BoxOrientation direction ) { final Component neighbour; final boolean ltr = container.getComponentOrientation ().isLeftToRight (); if ( direction.isTop () ) { neighbour = row > 0 ? getComponentAt ( container, col, row - 1 ) : null; } else if ( direction.isBottom () ) { neighbour = row < gridSize.rows - 1 ? getComponentAt ( container, col, row + 1 ) : null; } else if ( ltr ? direction.isLeft () : direction.isRight () ) { neighbour = col > 0 ? getComponentAt ( container, col - 1, row ) : null; } else if ( ltr ? direction.isRight () : direction.isLeft () ) { neighbour = col < gridSize.columns - 1 ? getComponentAt ( container, col + 1, row ) : null; } else { throw new IllegalArgumentException ( "Unknown neighbour direction: " + direction ); } return neighbour; } /** * Returns whether or not component at the specified column/row is positioned right at container border. * * @param container container * @param gridSize actual grid size * @param col component column * @param row component row * @param direction neighbour direction * @return {@code true} if component at the specified column/row is positioned right at container border, {@code false} otherwise */ protected boolean isAtBorder ( @NotNull final Container container, @NotNull final GridSize gridSize, final int col, final int row, @NotNull final BoxOrientation direction ) { final boolean atBorder; final boolean ltr = container.getComponentOrientation ().isLeftToRight (); if ( direction.isTop () ) { atBorder = row == 0; } else if ( direction.isBottom () ) { atBorder = row == gridSize.rows - 1; } else if ( direction.isLeft () ) { atBorder = col == ( ltr ? 0 : gridSize.columns - 1 ); } else if ( direction.isRight () ) { atBorder = col == ( ltr ? gridSize.columns - 1 : 0 ); } else { throw new IllegalArgumentException ( "Unknown border direction: " + direction ); } return atBorder; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy