/* $Id: LabelledLayout.java 17896 2010-01-12 21:36:11Z linus $ ***************************************************************************** * Copyright (c) 2009 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * bobtarling ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 2008-2009 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.core.propertypanels.ui; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.LayoutManager; import java.util.ArrayList; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.UIManager; /** * This layout manager lines up components in 2 columns. All JLabels * are the first column and any component the JLabel is registered * with is in a second column next to the label. <p> * * Components are sized automatically to fill available space in the container * when it is resized. <p> * * All JLabel widths will be the largest of the JLabel preferred widths (unless * the container is too narrow). <p> * * The components will take up any left over width unless they are * restricted themselves by a maximum width. <p> * * The height of each component is either fixed or will resize to use up any * available space in the container. Whether a components height is resizable * is determined by checking whether the preferred height of that component is * greater then its minimum height. This is the case for components such as * JList which would require to expand to show the maximum number or items. <p> * * If a component is not to have its height resized then its preferred * height and minimum height should be the same. This is the case for * components such as JTextField and JComboBox* which should always stay the * same height. <p> * * [There is known bug in JRE5 where the prefered height and minimum height of * a JComboBox can differ. LabelledLayout has coded a workaround for this bug. * See - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6255154 ] <p> * * LabelledLayout can show multiple panels of label/component * pairs. The seperation of these panels is indicated by adding a * Seperator component to the container. Labelled layout starts * a new panel when detecting this Seperator. <p> * * When there are multiple panels, each panel is given equal width. * The width restriction of JLabels and components described above are then * dependent on panel width rather than container width. * * @author Bob Tarling */ class LabelledLayout implements LayoutManager, java.io.Serializable { /** * The class uid */ private static final long serialVersionUID = -5596655602155151443L; /** * This is the horizontal gap (in pixels) which specifies the space * between sections. They can be changed at any time. * This should be a non negative integer. * * @serial * @see #getHgap() * @see #setHgap(int) */ private int hgap; /** * This is the vertical gap (in pixels) which specifies the space * between rows. They can be changed at any time. * This should be a non negative integer. * * @serial * @see #getVgap() * @see #setVgap(int) */ private int vgap; private boolean ignoreSplitters; /** * Construct a new LabelledLayout. */ public LabelledLayout() { ignoreSplitters = false; hgap = 0; vgap = 0; } /** * Construct a new LabelledLayout. */ public LabelledLayout(boolean ignoreSplitters) { this.ignoreSplitters = ignoreSplitters; this.hgap = 0; this.vgap = 0; } /** * Construct a new horizontal LabelledLayout with the specified * cell spacing. * @param hgap The horizontal gap between components * @param vgap The vertical gap between components */ public LabelledLayout(int hgap, int vgap) { this.ignoreSplitters = false; this.hgap = hgap; this.vgap = vgap; } /** * Adds the specified component with the specified name to the * layout. This is included to satisfy the LayoutManager interface * but is not actually used in this layout implementation. * * @param name the name of the component * @param comp the component to be added */ public void addLayoutComponent(String name, Component comp) { } /** * Removes the specified component from * the layout. This is included to satisfy the LayoutManager * interface but is not actually used in this layout * implementation. * * @param comp the component */ public void removeLayoutComponent(Component comp) { } /** * Determines the preferred size of the container argument using * this labelled layout. The preferred size is that all child * components are in one section at their own preferred size with * gaps and border indents. */ public Dimension preferredLayoutSize(Container parent) { synchronized (parent.getTreeLock()) { final Insets insets = parent.getInsets(); int preferredWidth = 0; int preferredHeight = 0; int widestLabel = 0; final int componentCount = parent.getComponentCount(); for (int i = 0; i < componentCount; ++i) { Component childComp = parent.getComponent(i); if (childComp.isVisible() && !(childComp instanceof Seperator)) { int childHeight = getPreferredHeight(childComp); preferredHeight += childHeight + this.vgap; } } preferredWidth += insets.left + widestLabel + insets.right; preferredHeight += insets.top + insets.bottom; return new Dimension( insets.left + widestLabel + preferredWidth + insets.right, preferredHeight); } } /** * Required by LayoutManager. * * @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container) */ public Dimension minimumLayoutSize(Container parent) { synchronized (parent.getTreeLock()) { final Insets insets = parent.getInsets(); int minimumHeight = insets.top + insets.bottom; return new Dimension(0, minimumHeight); } } /** * @see java.awt.LayoutManager#layoutContainer(java.awt.Container) */ public void layoutContainer(Container parent) { synchronized (parent.getTreeLock()) { int sectionX = parent.getInsets().left; final ArrayList<Component> components = new ArrayList<Component>(); final int sectionCount = getSectionCount(parent); final int sectionWidth = getSectionWidth(parent, sectionCount); int sectionNo = 0; for (int i = 0; i < parent.getComponentCount(); ++i) { final Component childComp = parent.getComponent(i); if (childComp instanceof Seperator) { if (!this.ignoreSplitters) { layoutSection( parent, sectionX, sectionWidth, components, sectionNo++); sectionX += sectionWidth + this.hgap; components.clear(); } } else { components.add(parent.getComponent(i)); } } layoutSection( parent, sectionX, sectionWidth, components, sectionNo); } } /** Determine the number of sections. There is only ever one * section if oriented vertically. If oriented horizontally the * number of sections is deduced from the number of Splitters in * the parent container. */ private int getSectionCount(Container parent) { int sectionCount = 1; final int componentCount = parent.getComponentCount(); if (!ignoreSplitters) { for (int i = 0; i < componentCount; ++i) { if (parent.getComponent(i) instanceof Seperator) { ++sectionCount; } } } return sectionCount; } /** * Determine the width of each section from the section count. * This is the working width minus the gaps between sections. This * result is then divided equally by the section count. */ private int getSectionWidth(Container parent, int sectionCount) { return (getUsableWidth(parent) - (sectionCount - 1) * this.hgap) / sectionCount; } /** * Determine the usable width of the parent. * This is the full width minus any borders. */ private int getUsableWidth(Container parent) { final Insets insets = parent.getInsets(); return parent.getWidth() - (insets.left + insets.right); } /** * Layout a single section */ private void layoutSection( final Container parent, final int sectionX, final int sectionWidth, final ArrayList components, final int sectionNo) { final ArrayList<Integer> rowHeights = new ArrayList<Integer>(); final int componentCount = components.size(); if (componentCount == 0) { return; } int labelWidth = 0; int unknownHeightCount = 0; int totalHeight = 0; // Build up an array list of the heights of each label/component pair. // Heights of zero indicate a proportional height. for (int i = 0; i < componentCount; ++i) { final Component childComp = (Component) components.get(i); if (childComp instanceof LabelledComponent) { LabelledComponent lc = (LabelledComponent) childComp; final JLabel label = lc.getLabel(); if (label != null) { if (label.getMinimumSize().width > labelWidth) { labelWidth = label.getMinimumSize().width; } } } final int childHeight = getChildHeight(childComp); if (childHeight == 0) { ++unknownHeightCount; } totalHeight += childHeight + this.vgap; rowHeights.add(new Integer(childHeight)); } totalHeight -= this.vgap; final Insets insets = parent.getInsets(); final int parentHeight = parent.getHeight() - (insets.top + insets.bottom); // Set the child components to the heights in the array list // calculating the height of any proportional component on the // fly. int y = insets.top; int row = 0; for (int i = 0; i < componentCount; ++i) { Component childComp = (Component) components.get(i); if (childComp.isVisible()) { if (childComp instanceof LabelledComponent) { final LabelledComponent lc = (LabelledComponent) childComp; final JLabel label = lc.getLabel(); if (label != null) { label.setPreferredSize( new Dimension( labelWidth, label.getPreferredSize().height)); } } int rowHeight; int componentWidth = sectionWidth; int componentX = sectionX; // If the component is a JLabel which has another // component assigned then position/size the label and // calculate the size of the registered component rowHeight = rowHeights.get(row).intValue(); if (rowHeight == 0) { try { rowHeight = calculateHeight( parentHeight, totalHeight, unknownHeightCount--, childComp); } catch (ArithmeticException e) { final String lookAndFeel = UIManager.getLookAndFeel().getClass().getName(); throw new IllegalStateException( "Division by zero laying out " + childComp.getClass().getName() + " on " + parent.getClass().getName() + " in section " + sectionNo + " using " + lookAndFeel, e); } totalHeight += rowHeight; } // Make sure the component width isn't any greater // than its maximum allowed width if (childComp.getMaximumSize() != null && getMaximumWidth(childComp) < componentWidth) { componentWidth = getMaximumWidth(childComp); } childComp.setBounds(componentX, y, componentWidth, rowHeight); y += rowHeight + this.vgap; ++row; } } } /** * @param childComp a component * @return 0 for a resizable component or a positive value for its fixed * height */ private int getChildHeight(Component childComp) { if (isResizable(childComp)) { // If the child component is resizable then // we don't know it's actual size yet. // It will be calculated later as a // proportion of the available left over // space. For now this is flagged as zero. return 0; } else { // If a preferred height is not given or is // the same as the minimum height then fix the // height of this row. return getMinimumHeight(childComp); } } /** * A component is resizable if its minimum size is less than * its preferred size. * There is a workaround here for a bug introduced in JRE5 * where JComboBox minimum and preferred size now differ. * JComboBox is not resizable. * Anything in a JScrollPane is considered resizable * @param comp the component to check for resizability. * @return true if the given component should be resized to take u[p empty * space. */ private boolean isResizable(Component comp) { if (comp == null) { return false; } if (comp instanceof JComboBox) { return false; } if (comp.getPreferredSize() == null) { return false; } if (comp.getMinimumSize() == null) { return false; } return (getMinimumHeight(comp) < getPreferredHeight(comp)); } private final int calculateHeight( final int parentHeight, final int totalHeight, final int unknownHeightsLeft, final Component childComp) { return Math.max( (parentHeight - totalHeight) / unknownHeightsLeft, getMinimumHeight(childComp)); } private int getPreferredHeight(final Component comp) { return (int) comp.getPreferredSize().getHeight(); } private int getPreferredWidth(final Component comp) { return (int) comp.getPreferredSize().getWidth(); } private int getMinimumHeight(final Component comp) { return (int) comp.getMinimumSize().getHeight(); } private int getMaximumWidth(final Component comp) { return (int) comp.getMaximumSize().getWidth(); } /** * Create a new instance of the Separator that splits the layout in columns * @return the separator */ public static Seperator getSeparator() { return new Seperator(); } /** * @return the horizontal gaps between components */ public int getHgap() { return this.hgap; } /** * Set the horizontal gaps between components * @param hgap the horizontal gap */ public void setHgap(int hgap) { this.hgap = hgap; } /** * @return the vertical gaps between components */ public int getVgap() { return this.vgap; } /** * Set the vertical gaps between components * @param vgap the horizontal gap */ public void setVgap(int vgap) { this.vgap = vgap; } } class Seperator extends JPanel { private static final long serialVersionUID = -4143634500959911688L; Seperator() { super.setVisible(false); } }