/* * 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); } } /*---------------------------------------------------------------------------*/