/*
* IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
*
* http://izpack.org/
* http://izpack.codehaus.org/
*
* Copyright 2002 Elmar Grom
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.izforge.izpack.gui;
import java.awt.*;
import java.util.Vector;
/**
* This class implements a layout manager that generally lays out components in two columns. <BR>
* <BR>
* The design goal for this layout manager was to lay out forms for data entry, where there are
* several rows of entry fields with associated labels. The goal was to have the beginning off all
* labels line up, as well as the left edge of all the data entry fields. This leads to a situation
* where all components are essentially laid out in two columns. The columns adjust to accommodate
* components of various sizes. This means that components that are added are laid out top to
* bottom, either in the left column, in the right column or straddling both columns. In addition to
* this general behavior, the following additional layout capabilities are supported:<br>
* <ul>
* <li>Resizable margins are provided on the left and right side.
* <li>A special region is provided at the top that is only affected by the side margins but not by
* any other layout behavior.
* <li>It is possible to specify the vertical positioning of the cluster of laid out components for
* the case that they do not occupy the entire available real estate.
* <li>Individual components can be indented.
* </ul>
* <p/>
* <b>The Layout Behavior</b> <br>
* <br>
* The left and right margin are absolute. This means that they can not be penetrated by components.
* All layout happens between the limits established by these margins. The purpose of these margins
* is to ensure that components are not laid out all the way to the edge of their container, without
* the need to set matching borders for each component. <br>
* <br>
* The title margin at the top factors only into the layout behavior if there is a component set to
* be laid out in that region, otherwise it is ignored. <br>
* <br>
* The vertical space available to each row of components depends on the space requirements of the
* tallest component in that row. Both components are placed vertically centered in their row. <br>
* <br>
* All horizontal layout is based on the position of three vertical rules, the left rule, the right
* rule and the center rule. <br>
* <br>
* <img src="doc-files/TwoColumnLayout.gif"/> <br>
* <br>
* The actual position of each rule depends on the alignment strategy, margin settings and component
* sizes. Regardless of these factors, components placed in the left column are <i>always</i>
* positioned with their left edge aligned with the left rule. Components placed in the right column
* are <i>always</i> positioned with their left edge aligned with the center rule. If a component
* straddles both columns, it is <i>always</i> positioned with the left edge aligned with the left
* rule, but is allowed to extend all the way to the right rule. The only exception is a component
* that is specified with an indent. In this case the component is moved to the right of the
* respective rule by the indent amount. <br>
* <br>
* The location of the rules is determined based on the alignment strategy as follows:<br>
* <ul>
* <li>The right rule is always located at the edge of the right margin.
* <li><b>Left Alignment:</b> The left rule is located the edge of the left margin. The center
* rule is located far enough to the right to clear the widest component in the left column.
* <li><b>Center Alignment:</b> The center rule is located at the center of the panel. The left
* rule is located far enough to the left to make the widest component in the left column fit.
* <li><b>Right Alignment</b> The center rule is located far enough to the left of the right rule
* to make the widest component in the right column fit. The left rule is located far enough to the
* left to make the widest component in the left column fit.
* </ul>
* All components clump together vertically and are positioned right beneath the title margin. This
* is of course not a very appealing presentation. By setting how the remaining vertical space is
* distributed above and below the cluster of components the cluster can be positioned more
* favorably (see the shaded area in the illustration).
*
* @author Elmar Grom
* @version 0.0.1 / 11/14/02
* @see com.izforge.izpack.gui.TwoColumnConstraints
*/
public class TwoColumnLayout implements LayoutManager2
{
public static final int LEFT = 0;
public static final int RIGHT = 1;
public static final int CENTER = 2;
/**
* holds all the components and layout constraints.
*/
private Vector[] components = {new Vector(), new Vector()};
/**
* holds the component to be placed in the title region, including layout constraints.
*/
private TwoColumnConstraints title = null;
/**
* the margin setting in % of the container's width
*/
private int margin = 0;
/**
* the setting for the buffer area on top of hte comonent cluster in % of the left over height.
*/
private int topBuffer = 0;
/**
* the indent setting in % of the conteiner's width
*/
private int indent = 0;
/**
* the gap between the two columns
*/
private int gap = 5;
private int alignment = LEFT;
private int leftRule;
private int rightRule;
private int centerRule;
private int titleHeight;
/**
* Constructs a <code>TwoColumnLayout</code> layout manager. To add components use the
* container's <code>add(comp, constraints)</code> method with a TwoColumnConstraints object.
*
* @param margin the margin width to use on the left and right side in % of the total container
* width. Values less than 0% and greater than 50% are not accepted.
* @param gap the gap between the two columns.
* @param indent the indent to use for components that have that constraint set. This is a value
* in pixels.
* @param topBuffer the percentage of left over vertical space to place on top of the component
* cluster. Values between 0% and 100% are accepted.
* @param alignment how to align the overall layout. Legal values are LEFT, CENTER, RIGHT.
*/
public TwoColumnLayout(int margin, int gap, int indent, int topBuffer, int alignment)
{
this.indent = indent;
this.gap = gap;
if ((margin >= 0) && (margin <= 50))
{
this.margin = margin;
}
if ((topBuffer >= 0) && (topBuffer <= 100))
{
this.topBuffer = topBuffer;
}
if ((alignment == LEFT) || (alignment == CENTER) || (alignment == RIGHT))
{
this.alignment = alignment;
}
}
/**
* Sets the constraints for the specified component in this layout. <code>null</code> is a
* legal value for a component, but not for a constraints object.
*
* @param comp the component to be modified.
* @param constraints the constraints to be applied.
*/
public void addLayoutComponent(Component comp, Object constraints)
{
if (constraints == null)
{
return;
}
TwoColumnConstraints component = null;
try
{
component = (TwoColumnConstraints) constraints;
component = (TwoColumnConstraints) component.clone();
}
catch (Throwable exception)
{
return;
}
component.component = comp;
// ----------------------------------------------------
// the title component is recorded in a separate
// variable, displacing any component that might have
// been previously recorded for that location.
// ----------------------------------------------------
if (component.position == TwoColumnConstraints.NORTH)
{
title = component;
if (title.stretch)
{
title.align = LEFT;
}
}
// ----------------------------------------------------
// components that straddle both columns are a bit
// tricky because these components are recorded in the
// left column and the same row cannot contain a
// component in the right column.
//
// If there are fewer components in the left column
// than in the right one, a null is inserted at this
// place in the right column. This allows the component
// to use both columns. The component that previously
// occupied this position and any that were placed
// below will be pushed down by one row due to this
// action.
//
// If there are the same number of components in both
// columns or if there are fewer in the right column
// then the component is added to the left column and
// then the right column filled with null until both
// contain the same number of components. this means
// that any components that will now be placed in the
// right column are positioned beneath this component.
// Unoccupied spots higher in the right column become
// inaccessible.
// ----------------------------------------------------
else if (component.position == TwoColumnConstraints.BOTH)
{
// first make sure that both columns have the same number of entries
while (components[RIGHT].size() > components[LEFT].size())
{
components[LEFT].add(null);
}
while (components[LEFT].size() > components[RIGHT].size())
{
components[RIGHT].add(null);
}
components[LEFT].add(component);
components[RIGHT].add(null);
}
// ----------------------------------------------------
// WEST components are added to the left column
// ----------------------------------------------------
else if (component.position == TwoColumnConstraints.WEST)
{
components[LEFT].add(component);
}
// ----------------------------------------------------
// WESTONLY components are added to the left column
// the right column has to be kept free
// ----------------------------------------------------
else if (component.position == TwoColumnConstraints.WESTONLY)
{
components[LEFT].add(component);
// fill right column to make sure nothing is placed there
while (components[RIGHT].size() < components[LEFT].size())
{
components[RIGHT].add(null);
}
}
// ----------------------------------------------------
// EAST components are added to the right column
// ----------------------------------------------------
else if (component.position == TwoColumnConstraints.EAST)
{
components[RIGHT].add(component);
}
// ----------------------------------------------------
// EASTONLY components are added to the left column
// the right column has to be kept free
// ----------------------------------------------------
else if (component.position == TwoColumnConstraints.EASTONLY)
{
components[RIGHT].add(component);
// fill left column to make sure nothing is placed there
while (components[LEFT].size() < components[RIGHT].size())
{
components[LEFT].add(null);
}
}
// ----------------------------------------------------
// If the position did not match any of the above
// criteria then the component is not added and
// consequently will not be laid out.
// ----------------------------------------------------
}
/**
* Lays out the container in the specified panel.
*
* @param parent the component which needs to be laid out.
*/
public void layoutContainer(Container parent)
{
positionRules(parent);
positionTitle(parent);
positionComponents(parent);
}
/**
* Positions the three rules in preparation for layout. Sets the variables:<br>
* <ul>
* <li><code>leftRule</code>
* <li><code>rightRule</code>
* <li><code>centerRule</code>
* </ul>
*
* @param parent the component which needs to be laid out.
*/
private void positionRules(Container parent)
{
int margin = margin(parent);
if (alignment == LEFT)
{
leftRule = margin;
centerRule = leftRule + minimumColumnWidth(LEFT, parent) + gap;
rightRule = parent.getWidth() - margin;
}
else if (alignment == CENTER)
{
centerRule = (int) (parent.getMinimumSize().getWidth() / 2);
leftRule = centerRule - minimumColumnWidth(LEFT, parent) - gap;
rightRule = parent.getWidth() - margin;
}
else if (alignment == RIGHT)
{
rightRule = parent.getWidth() - margin;
centerRule = rightRule - minimumColumnWidth(RIGHT, parent);
leftRule = centerRule - minimumColumnWidth(LEFT, parent) - gap;
}
}
/**
* Positions the title component and sets the variable <code>titleHeight</code>. <b>Note:</b>
* this method depends on the fact that the rules are set to their correct layout position.
*
* @param parent the component which needs to be laid out.
*/
private void positionTitle(Container parent)
{
if (title != null)
{
Component component = title.component;
int width = (int) component.getMinimumSize().getWidth();
titleHeight = (int) component.getMinimumSize().getHeight();
if (component != null)
{
if (title.stretch)
{
width = rightRule - leftRule;
component.setBounds(leftRule, 0, width, titleHeight);
}
else if (title.align == TwoColumnConstraints.LEFT)
{
component.setBounds(leftRule, 0, width, titleHeight);
}
else if (title.align == TwoColumnConstraints.CENTER)
{
int left = centerRule - (width / 2);
component.setBounds(left, 0, width, titleHeight);
}
else if (title.align == TwoColumnConstraints.RIGHT)
{
int left = rightRule - width;
component.setBounds(left, 0, width, titleHeight);
}
}
}
}
/**
* Positions all components in the container.
*
* @param parent the component which needs to be laid out.
*/
private void positionComponents(Container parent)
{
int usedHeight = titleHeight + minimumClusterHeight();
int topBuffer = topBuffer(usedHeight, parent);
int leftHeight = 0;
int rightHeight = 0;
if (topBuffer < 0)
{
topBuffer = 0;
}
int y = titleHeight + topBuffer;
for (int i = 0; i < rows(); i++)
{
leftHeight = height(i, LEFT);
rightHeight = height(i, RIGHT);
if (leftHeight > rightHeight)
{
int offset = (leftHeight - rightHeight) / 2;
positionComponent(y, i, LEFT, parent);
positionComponent((y + offset), i, RIGHT, parent);
y += leftHeight;
}
else if (leftHeight < rightHeight)
{
int offset = (rightHeight - leftHeight) / 2;
positionComponent((y + offset), i, LEFT, parent);
positionComponent(y, i, RIGHT, parent);
y += rightHeight;
}
else
{
positionComponent(y, i, LEFT, parent);
positionComponent(y, i, RIGHT, parent);
y += leftHeight;
}
}
}
/**
* Positiones one component as instructed. Constraints for each component, such as
* <code>stretch</code>, <code>BOTH</code> and <code>indent</code> are taken into
* account. In addition, empty comonents are handled properly.
*
* @param y the y location within the continer, where the component should be positioned.
* @param row the row of the component
* @param column the column of the component
* @param parent the container which needs to be laid out.
*/
private void positionComponent(int y, int row, int column, Container parent)
{
TwoColumnConstraints constraints = null;
try
{
constraints = (TwoColumnConstraints) (components[column].elementAt(row));
}
catch (Throwable exception)
{
return;
}
int x = 0;
if (constraints != null)
{
Component component = constraints.component;
int width = (int) component.getPreferredSize().getWidth();
int height = (int) component.getPreferredSize().getHeight();
// --------------------------------------------------
// set x to the appropriate rule. The only need to
// modify this is for indent
// --------------------------------------------------
if (column == LEFT)
{
x = leftRule;
}
else
{
x = centerRule;
}
if (component != null)
{
// --------------------------------------------------
// set the width for stretch based on BOTH, LEFT and
// RIGHT positionsing
// --------------------------------------------------
if ((constraints.stretch) && (constraints.position == TwoColumnConstraints.BOTH))
{
width = rightRule - leftRule;
x = leftRule;
}
else if ((constraints.stretch) && (column == LEFT))
{
width = centerRule - leftRule;
}
else if ((constraints.stretch) && (column == RIGHT))
{
width = rightRule - centerRule;
}
// --------------------------------------------------
// if we straddle both columns but are not stretching
// use the preferred width as long as it is less then
// the width of both columns combined. Also set the x
// position to left, just to be sure.
// --------------------------------------------------
else if (constraints.position == TwoColumnConstraints.BOTH)
{
if (width > (rightRule - leftRule))
{
width = rightRule - leftRule;
}
x = leftRule;
}
// --------------------------------------------------
// correct for indent if this option is set
// --------------------------------------------------
if (constraints.indent)
{
width -= indent;
x += indent;
}
component.setBounds(x, y, width, height);
}
}
}
/**
* Returns the minimum width of the column requested.
*
* @param column the columns to measure (LEFT / RIGHT)
* @param parent the component which needs to be laid out.
* @return the minimum width required to fis the components in this column
*/
private int minimumColumnWidth(int column, Container parent)
{
Component component = null;
TwoColumnConstraints constraints = null;
int width = 0;
int temp = 0;
for (int i = 0; i < components[column].size(); i++)
{
constraints = (TwoColumnConstraints) components[column].elementAt(i);
if ((constraints != null) && (constraints.position != TwoColumnConstraints.BOTH))
{
component = constraints.component;
temp = (int) component.getMinimumSize().getWidth();
if (constraints.indent)
{
temp += indent;
}
if (temp > width)
{
width = temp;
}
}
}
return (width);
}
/**
* Retrunds the minimum width both columns together should have based on the minimum widths of
* all the components that straddle both columns and the minimum width of the title component.
*
* @param parent the component which needs to be laid out.
* @return the minimum width required to fis the components in this column
*/
private int minimumBothColumnsWidth(Container parent)
{
Component component = null;
TwoColumnConstraints constraints = null;
int width = 0;
int temp = 0;
if (title != null)
{
component = title.component;
width = (int) component.getMinimumSize().getWidth();
}
for (int i = 0; i < components[LEFT].size(); i++)
{
constraints = (TwoColumnConstraints) components[LEFT].elementAt(i);
if ((constraints != null) && (constraints.position == TwoColumnConstraints.BOTH))
{
component = constraints.component;
temp = (int) component.getMinimumSize().getWidth();
if (constraints.indent)
{
temp += indent;
}
if (temp > width)
{
width = temp;
}
}
}
return (width);
}
private int minimumClusterHeight()
{
int height = 0;
for (int i = 0; i < rows(); i++)
{
height += rowHeight(i);
}
return (height);
}
/**
* Returns the number of rows that need to be laid out.
*/
private int rows()
{
int rows = 0;
int leftRows = components[LEFT].size();
int rightRows = components[RIGHT].size();
if (leftRows > rightRows)
{
rows = leftRows;
}
else
{
rows = rightRows;
}
return (rows);
}
/**
* Measures and returns the minimum height required to render the components in the indicated
* row.
*
* @param row the index of the row to measure
*/
private int rowHeight(int row)
{
int height = 0;
int height1 = height(row, LEFT);
int height2 = height(row, RIGHT);
// ----------------------------------------------------
// take the higher one
// ----------------------------------------------------
if (height1 > height2)
{
height = height1;
}
else
{
height = height2;
}
return (height);
}
/**
* Measures and returns the minimum height required to render the component in the indicated row
* and column.
*
* @param row the index of the row to measure
* @param column the column of the component to measure (<code>LEFT</code> or
* <code>RIGHT</code>)
*/
private int height(int row, int column)
{
int height = 0;
int width = 0;
Component component;
TwoColumnConstraints constraints;
try
{
constraints = (TwoColumnConstraints) components[column].elementAt(row);
if (constraints != null)
{
component = constraints.component;
width = (int) component.getMinimumSize().getWidth();
height = (int) component.getMinimumSize().getHeight();
if (constraints.position == TwoColumnConstraints.WEST)
{
if (width > (centerRule - leftRule))
{
component.setBounds(0, 0, (centerRule - leftRule), height);
}
}
else if (constraints.position == TwoColumnConstraints.EAST)
{
if (width > (rightRule - centerRule))
{
component.setBounds(0, 0, (rightRule - centerRule), height);
}
}
else if (constraints.position == TwoColumnConstraints.BOTH)
{
if (width > (rightRule - leftRule))
{
component.setBounds(0, 0, (rightRule - leftRule), height);
}
}
height = (int) component.getMinimumSize().getHeight();
}
}
// ----------------------------------------------------
// we might get an exception if one of the vectors is
// shorter, because we index out of bounds. If there
// is nothing there then the height is 0, nothing
// further to worry about!
// ----------------------------------------------------
catch (Throwable exception)
{
}
return (height);
}
/**
* Computes the margin value based on the container width and the margin setting.
*
* @param parent the component which needs to be laid out.
*/
private int margin(Container parent)
{
int amount = (int) (((parent.getSize().getWidth()) * margin) / 100);
return (amount);
}
/**
* Computes the top buffer value based on the container width and the setting for the top buffer
*
* @param usedHeight the amount of the parent component's height that is already in use (height
* of the title and the combined height of all rows).
* @param parent the component which needs to be laid out.
*/
private int topBuffer(int usedHeight, Container parent)
{
int amount = ((int) parent.getSize().getHeight()) - usedHeight;
amount = amount * topBuffer / 100;
return (amount);
}
/*--------------------------------------------------------------------------*/
/**
* Computes the indent value based on the container width and the indent setting.
*
* @param parent the component which needs to be laid out.
*/
/*--------------------------------------------------------------------------*/
/*
* private int indent (Container parent) { int amount = (int)(((parent.getMinimumSize
* ().getWidth ()) * indent) / 100);
*
* return (amount); }
*/
/**
* Calculates the preferred size dimensions for the specified panel given the components in the
* specified parent container.
*
* @param parent the component to be laid out
*/
public Dimension preferredLayoutSize(Container parent)
{
return (minimumLayoutSize(parent));
}
/**
* Calculates the minimum size dimensions for the specified panel given the components in the
* specified parent container.
*
* @param parent the component to be laid out
*/
public Dimension minimumLayoutSize(Container parent)
{
positionTitle(parent);
int width = minimumBothColumnsWidth(parent);
int height = minimumClusterHeight() + titleHeight;
return (new Dimension(width, height));
}
/**
* Calculates the maximum size dimensions for the specified panel given the components in the
* specified parent container.
*
* @param parent the component to be laid out
*/
public Dimension maximumLayoutSize(Container parent)
{
return (minimumLayoutSize(parent));
}
/**
* Returns the alignment along the x axis. This specifies how the component would like to be
* aligned relative to other components. The value should be a number between 0 and 1 where 0
* represents alignment along the origin, 1 is aligned the furthest away from the origin, 0.5 is
* centered, etc.
*
* @param parent the component to be laid out
*/
public float getLayoutAlignmentX(Container parent)
{
return (0);
}
/**
* Returns the alignment along the y axis. This specifies how the component would like to be
* aligned relative to other components. The value should be a number between 0 and 1 where 0
* represents alignment along the origin, 1 is aligned the furthest away from the origin, 0.5 is
* centered, etc.
*
* @param parent the component to be laid out
*/
public float getLayoutAlignmentY(Container parent)
{
return (0);
}
/**
* Invalidates the layout, indicating that if the layout manager has cached information it
* should be discarded.
*
* @param parent the component to be laid out
*/
public void invalidateLayout(Container parent)
{
leftRule = 0;
rightRule = 0;
centerRule = 0;
titleHeight = 0;
}
/**
* Adds the specified component with the specified name to the layout. This version is not
* supported, use <code>addLayoutComponent</code> with layout contsraints.
*
* @param name the component name
* @param comp the component to be added
*/
public void addLayoutComponent(String name, Component comp)
{
}
/**
* This functionality removes the TwoColumnConstraints from Vectors
* so that alignment of components on UserInputPanel doesn't get
* dirty
*
* @param comp the component to be removed
*/
public void removeLayoutComponent(Component comp)
{
Vector left = components[LEFT];
Vector right = components[RIGHT];
for (int i = 0; i < left.size(); i++)
{
TwoColumnConstraints constraints = (TwoColumnConstraints) left.get(i);
if (constraints == null)
{
continue;
}
Component ctemp = constraints.component;
if (ctemp != null && ctemp.equals(comp))
{
if (constraints.position == TwoColumnConstraints.BOTH || constraints.position == TwoColumnConstraints.WESTONLY)
{
right.remove(i);
}
break;
}
}
for (int j = 0; j < right.size(); j++)
{
TwoColumnConstraints constraints = (TwoColumnConstraints) right.get(j);
if (constraints == null)
{
continue;
}
Component ctemp = constraints.component;
if (ctemp != null && ctemp.equals(comp))
{
if (constraints.position == TwoColumnConstraints.BOTH || constraints.position == TwoColumnConstraints.EASTONLY)
{
left.remove(j);
}
break;
}
}
}
/**
* This method is provided for conveninence of debugging layout problems. It renders the three
* rules and the limit of the title marign visible after these positions have been computed. In
* addition, the indent locations are shown as dashed lines. To use this functionality do the
* following:<br>
* <br>
* <ul>
* <li>in the container using this layout manager override the <code>paint()</code> method.
* <li>in that method, first call <code>super.paint()</code>
* <li>then call this method
* </ul>
* <br>
* <b>Note:</b> cast the graphics object received in the <code>paint()</code> method to
* <code>Graphics2D</code> when making the call.<br>
* <br>
*
* @param graphics the graphics context used for drawing.
* @param color the color to use for rendering the layout grid
*/
public void showRules(Graphics2D graphics, Color color)
{
int height = graphics.getClipBounds().height;
Stroke currentStroke = graphics.getStroke();
Color currentColor = graphics.getColor();
Stroke stroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1.5f,
new float[]{10, 5}, 5);
graphics.setColor(color);
graphics.drawLine(leftRule, 0, leftRule, height);
graphics.drawLine(centerRule, titleHeight, centerRule, height);
graphics.drawLine(rightRule, 0, rightRule, height);
graphics.drawLine(leftRule, titleHeight, rightRule, titleHeight);
graphics.setStroke(stroke);
graphics.drawLine((leftRule + indent), titleHeight, (leftRule + indent), height);
graphics.drawLine((centerRule + indent), titleHeight, (centerRule + indent), height);
graphics.setStroke(currentStroke);
graphics.setColor(currentColor);
}
}
/*---------------------------------------------------------------------------*/