StackLayout.java Source code

Java tutorial

Introduction

Here is the source code for StackLayout.java

Source

/*
 *  StackLayout.java
 *  2005-07-15
 */

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;

import javax.swing.JSeparator;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;

/**
 * Similar to BoxLayout, uses an orientation to determine if the contents
 * should be arranged horizontally or vertically.  By default, Resizes each
 * item to equal width or height (depending on the orientation) based on the
 * maximum preferred width or height of all items.
 * @author Christopher Bach
 */
public class StackLayout implements LayoutManager {
    public static final int HORIZONTAL = SwingConstants.HORIZONTAL;
    public static final int VERTICAL = SwingConstants.VERTICAL;

    private int ourOrientation = HORIZONTAL;
    private int ourSpacing = 0;

    private boolean ourDepthsMatched = true;
    private boolean ourLengthsMatched = false;
    private boolean ourFill = false;
    private boolean ourDrop = false;

    private int ourSqueezeFactor = 100;

    /**
     * Creates a new StackLayout with a horizontal orientation.
     */
    public StackLayout() {

    }

    /**
     * Creates a new StackLayout with the specified orientation.
     */
    public StackLayout(int orientation) {
        setOrientation(orientation);
    }

    /**
     * Creates a new StackLayout with the specified orientation and spacing.
     */
    public StackLayout(int orientation, int spacing) {
        setOrientation(orientation);
        setSpacing(spacing);
    }

    /**
     * Creates a new StackLayout matching the component lengths and
     * depths as indicated.
     */
    public StackLayout(boolean matchLengths, boolean matchDepths) {
        setMatchesComponentLengths(matchLengths);
        setMatchesComponentDepths(matchDepths);
    }

    /**
     * Creates a new StackLayout with the specified orientation
     * and spacing, matching the component lengths and depths
     * as indicated.
     */
    public StackLayout(int orientation, int spacing, boolean matchLengths, boolean matchDepths) {
        setOrientation(orientation);
        setSpacing(spacing);
        setMatchesComponentLengths(matchLengths);
        setMatchesComponentDepths(matchDepths);
    }

    /**
     * Sets this StackLayout's orientation, either
     * SwingConstants.HORIZONTAL or SwingConstants.VERTICAL.
     */
    public void setOrientation(int orientation) {
        if (orientation == HORIZONTAL || orientation == VERTICAL) {
            ourOrientation = orientation;
        }
    }

    /**
     * Returns this StackLayout's orientation, either
     * SwingConstants.HORIZONTAL or SwingConstants.VERTICAL.
     */
    public int getOrientation() {
        return ourOrientation;
    }

    /**
     * Sets the spacing between components that this StackLayout uses
     * when laying out the components.
     */
    public void setSpacing(int spacing) {
        ourSpacing = Math.max(0, spacing);
    }

    /**
     * Returns the spacing between components that this StackLayout uses
     * when laying out the components.
     */
    public int getSpacing() {
        return ourSpacing;
    }

    /**
     * Sets whether or not the last component in the stack
     * should be stretched to fill any remaining space within
     * the parent container.  The default value is false.
     */
    public void setFillsTrailingSpace(boolean shouldFill) {
        ourFill = shouldFill;
    }

    /**
     * Returns whether or not the last component in the stack
     * should be stretched to fill any remaining space within
     * the parent container.
     */
    public boolean fillsTrailingSpace() {
        return ourFill;
    }

    /**
     * Sets whether or not components in the stack that do not
     * fit in the parent container should be left out of the layout.
     * The default value is false;
     */
    public void setDropsPartialComponents(boolean shouldDrop) {
        ourDrop = shouldDrop;
    }

    /**
     * Returns whether or not components in the stack that do not
     * fit in the parent container should be left out of the layout.
     */
    public boolean dropsPartialComponents() {
        return ourDrop;
    }

    /**
     * Sets whether or not all components in the stack will be sized
     * to the same height (when in a horizontal orientation) or width
     * (when in a vertical orientation).  The default value is true.
     */
    public void setMatchesComponentDepths(boolean match) {
        ourDepthsMatched = match;
    }

    /**
     * Returns whether or not all components in the stack will be sized
     * to the same height (when in a horizontal orientation) or width
     * (when in a vertical orientation).
     */
    public boolean matchesComponentDepths() {
        return ourDepthsMatched;
    }

    /**
     * Sets whether or not all components in the stack will be sized
     * to the same width (when in a horizontal orientation) or height
     * (when in a vertical orientation).  The default value is false.
     */
    public void setMatchesComponentLengths(boolean match) {
        ourLengthsMatched = match;
    }

    /**
     * Returns whether or not all components in the stack will be sized
     * to the same width (when in a horizontal orientation) or height
     * (when in a vertical orientation).
     */
    public boolean matchesComponentLengths() {
        return ourLengthsMatched;
    }

    /**
     * Sets the percentage of a component's preferred size that it
     * may be squeezed in order to attempt to fit all components
     * into the layout.  The squeeze factor will only be applied
     * when this layout is set to match component lengths.
     *
     * For example, if the parent container is 100 pixels wide
     * and holds two buttons, the largest having a preferred
     * width of 80 pixels, a squeeze factor of 50 will allow each
     * button to be sized to as small as 40 pixels wide (50 percent
     * of the preferred width.
     *
     * The default value is 100.
     */
    public void setSqueezeFactor(int factor) {
        if (factor < 0)
            ourSqueezeFactor = 0;
        else if (factor > 100)
            ourSqueezeFactor = 100;
        else
            ourSqueezeFactor = factor;
    }

    /**
     * Returns the percentage of a component's preferred size that it
     * may be squeezed in order to attempt to fit all components
     * into the layout.
     */
    public int getSqueezeFactor() {
        return ourSqueezeFactor;
    }

    ////// LayoutManager implementation //////

    /**
     * Adds the specified component with the specified name to this layout.
     */
    public void addLayoutComponent(String name, Component comp) {

    }

    /**
     * Removes the specified component from this layout.
     */
    public void removeLayoutComponent(Component comp) {

    }

    /**
     * Returns the preferred size for this layout to arrange the
     * indicated parent's children.
     */
    public Dimension preferredLayoutSize(Container parent) {
        if (parent instanceof JToolBar) {
            setOrientation(((JToolBar) parent).getOrientation());
        }

        return preferredLayoutSize(parent, ourOrientation);
    }

    /**
     * Returns the preferred size for this layout to arrange the
     * indicated parent's children at the specified orientation.
     */
    // public, because it's useful - not one of the LayoutManager methods
    public Dimension preferredLayoutSize(Container parent, int orientation) {
        synchronized (parent.getTreeLock()) {
            Component[] comps = parent.getComponents();
            Dimension total = new Dimension(0, 0);

            int depth = calculatePreferredDepth(comps, orientation);

            int length = (ourLengthsMatched ? calculateAdjustedLength(comps, orientation, ourSpacing)
                    : calculatePreferredLength(comps, orientation, ourSpacing));

            total.width = (orientation == HORIZONTAL ? length : depth);
            total.height = (orientation == HORIZONTAL ? depth : length);

            Insets in = parent.getInsets();
            total.width += in.left + in.right;
            total.height += in.top + in.bottom;

            return total;
        }
    }

    /**
     * Returns the minimum size for this layout to arrange the
     * indicated parent's children at the specified orientation.
     */
    public Dimension minimumLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            if (parent instanceof JToolBar) {
                setOrientation(((JToolBar) parent).getOrientation());
            }

            Component[] comps = parent.getComponents();
            Dimension total = new Dimension(0, 0);

            int depth = calculatePreferredDepth(comps, ourOrientation);
            int length = calculateMinimumLength(comps, ourOrientation, ourSpacing);

            total.width = (ourOrientation == HORIZONTAL ? length : depth);
            total.height = (ourOrientation == HORIZONTAL ? depth : length);

            Insets in = parent.getInsets();
            total.width += in.left + in.right;
            total.height += in.top + in.bottom;

            return total;
        }
    }

    /**
     * Lays out the child components within the indicated parent container.
     */
    public void layoutContainer(Container parent) {
        synchronized (parent.getTreeLock()) {
            if (parent instanceof JToolBar) {
                setOrientation(((JToolBar) parent).getOrientation());
            }

            layoutComponents(parent);

        }
    }

    private void layoutComponents(Container parent) {
        Component[] components = parent.getComponents();
        Insets in = parent.getInsets();

        int maxHeight = parent.getHeight() - in.top - in.bottom;
        int maxWidth = parent.getWidth() - in.left - in.right;
        boolean horiz = (ourOrientation == HORIZONTAL);

        int totalDepth = calculatePreferredDepth(components, ourOrientation);
        totalDepth = Math.max(totalDepth, (horiz ? maxHeight : maxWidth));

        int prefLength = (ourLengthsMatched ? calculateAdjustedLength(components, ourOrientation, ourSpacing)
                : calculatePreferredLength(components, ourOrientation, ourSpacing));
        int totalLength = Math.min(prefLength, (horiz ? maxWidth : maxHeight));

        int a = (horiz ? in.left : in.top);
        int b = (horiz ? in.top : in.left);
        int l = 0, d = 0, sum = 0;
        int matchedLength = 0;
        Dimension prefsize = null;

        if (ourLengthsMatched) {
            matchedLength = (horiz ? getMaxPrefWidth(components) : getMaxPrefHeight(components));

            if (prefLength > totalLength && ourSqueezeFactor < 100) {
                int minLength = calculateMinimumLength(components, ourOrientation, ourSpacing);

                if (minLength >= totalLength) {
                    matchedLength = (matchedLength * ourSqueezeFactor) / 100;
                }

                else {
                    int numSeparators = countSeparators(components);
                    int numComponents = components.length - numSeparators;
                    int diff = (prefLength - totalLength) / numComponents;
                    if ((prefLength - totalLength) % numComponents > 0)
                        diff++;
                    matchedLength -= diff;
                }
            }
        }

        for (int i = 0; i < components.length; i++) {
            prefsize = components[i].getPreferredSize();
            if (!ourLengthsMatched)
                l = (horiz ? prefsize.width : prefsize.height);
            else
                l = matchedLength;

            if (components[i] instanceof JSeparator) {
                // l = Math.min(prefsize.width, prefsize.height);
                l = (horiz ? prefsize.width : prefsize.height);
                d = totalDepth;
                sum += l;
                if (ourDrop && sum > totalLength)
                    l = 0;
            }

            else {
                sum += l;
                if (ourDrop && sum > totalLength)
                    l = 0;

                else if (ourFill && !ourLengthsMatched && i == components.length - 1) {
                    l = Math.max(l, (horiz ? maxWidth : maxHeight));
                }

                if (ourDepthsMatched)
                    d = totalDepth;
                else
                    d = (horiz ? prefsize.height : prefsize.width);
            }

            if (horiz)
                components[i].setBounds(a, b + (totalDepth - d) / 2, l, d);
            else
                components[i].setBounds(b + (totalDepth - d) / 2, a, d, l);

            a += l + ourSpacing;
            sum += ourSpacing;
        }

    }

    /**
     * Returns the largest preferred width of the provided components.
     */
    private int getMaxPrefWidth(Component[] components) {
        int maxWidth = 0;
        int componentWidth = 0;
        Dimension d = null;

        for (int i = 0; i < components.length; i++) {
            d = components[i].getPreferredSize();
            componentWidth = d.width;

            if (components[i] instanceof JSeparator) {
                componentWidth = Math.min(d.width, d.height);
            }

            maxWidth = Math.max(maxWidth, componentWidth);
        }

        return maxWidth;
    }

    /**
     * Returns the largest preferred height of the provided components.
     */
    private int getMaxPrefHeight(Component[] components) {
        int maxHeight = 0;
        int componentHeight = 0;
        Dimension d = null;

        for (int i = 0; i < components.length; i++) {
            d = components[i].getPreferredSize();
            componentHeight = d.height;

            if (components[i] instanceof JSeparator) {
                componentHeight = Math.min(d.width, d.height);
            }

            else
                maxHeight = Math.max(maxHeight, componentHeight);
        }

        return maxHeight;
    }

    /**
     * Calculates the preferred "length" of this layout for the provided
     * components based on the largest component preferred size.
     */
    private int calculateAdjustedLength(Component[] components, int orientation, int spacing) {
        int total = 0;
        int componentLength = (orientation == HORIZONTAL ? getMaxPrefWidth(components)
                : getMaxPrefHeight(components));

        for (int i = 0; i < components.length; i++) {
            if (components[i] instanceof JSeparator) {
                Dimension d = components[i].getPreferredSize();
                // total += Math.min(d.width, d.height);
                total += (orientation == HORIZONTAL ? d.width : d.height);
            }

            else
                total += componentLength;
        }

        int gaps = Math.max(0, spacing * (components.length - 1));
        total += gaps;

        return total;
    }

    /**
     * Calculates the minimum "length" of this layout for the provided
     * components, taking the squeeze factor into account when necessary.
     */
    private int calculateMinimumLength(Component[] components, int orientation, int spacing) {
        if (!ourLengthsMatched)
            return calculatePreferredLength(components, orientation, spacing);

        if (ourSqueezeFactor == 100)
            return calculateAdjustedLength(components, orientation, spacing);

        int total = 0;
        int componentLength = (orientation == HORIZONTAL ? getMaxPrefWidth(components)
                : getMaxPrefHeight(components));

        componentLength = (componentLength * ourSqueezeFactor) / 100;

        for (int i = 0; i < components.length; i++) {
            if (components[i] instanceof JSeparator) {
                Dimension d = components[i].getPreferredSize();
                // total += Math.min(d.width, d.height);
                total += (orientation == HORIZONTAL ? d.width : d.height);
            }

            else
                total += componentLength;
        }

        int gaps = Math.max(0, spacing * (components.length - 1));
        total += gaps;

        return total;
    }

    /**
     * Calculates the preferred "length" of this layout for the provided
     * components.
     */
    private int calculatePreferredLength(Component[] components, int orientation, int spacing) {
        int total = 0;
        Dimension d = null;

        for (int i = 0; i < components.length; i++) {
            d = components[i].getPreferredSize();

            //      if (components[i] instanceof JSeparator)
            //      {
            //        total += Math.min(d.width, d.height);
            //      }
            //
            //      else
            total += (orientation == HORIZONTAL ? d.width : d.height);
        }

        int gaps = Math.max(0, spacing * (components.length - 1));
        total += gaps;

        return total;
    }

    /**
     * Returns the preferred "depth" of this layout for the provided
     * components.
     */
    private int calculatePreferredDepth(Component[] components, int orientation) {
        if (orientation == HORIZONTAL)
            return getMaxPrefHeight(components);
        else if (orientation == VERTICAL)
            return getMaxPrefWidth(components);
        else
            return 0;
    }

    private int countSeparators(Component[] components) {
        int count = 0;

        for (int i = 0; i < components.length; i++) {
            if (components[i] instanceof JSeparator)
                count++;
        }

        return count;
    }

}