/* * VariableGridLayout.java - a grid layout manager with variable cell sizes * :tabSize=4:indentSize=4:noTabs=false: * * Originally written by Dirk Moebius for the jEdit project. This work has been * placed into the public domain. You may use this work in any way and for any * purpose you wish. * * THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, NOT EVEN THE * IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE, ASSUMES * _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, * OR REDISTRIBUTION OF THIS SOFTWARE. */ package org.gjt.sp.jedit.gui; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.LayoutManager2; import java.util.Arrays; /** A rectangular grid layout manager with variable cell sizes * * The container is divided into rectangles, and one component is placed * in each rectangle. Each row is as large as the largest component in * that row, and each column is as wide as the widest component in * that column.<p> * * This behavior is basically the same as in * <code>java.awt.GridLayout</code>, but with different row heights and * column widths for each row/column.<p> * * For example, the following is an applet that lays out six buttons * into three rows and two columns:<p> * * <blockquote><pre> * import java.awt.*; * import java.applet.Applet; * public class ButtonGrid extends Applet { * public void init() { * setLayout(new VariableGridLayout(VariableGridLayout.FIXED_NUM_COLUMNS, 2)); * add(new Button("1")); * add(new Button("2")); * add(new Button("3")); * add(new Button("4")); * add(new Button("5")); * add(new Button("6")); * } * } * </pre></blockquote><p> * * <b>Programmer's remark:</b> VariableGridLayout could be faster, if it would * reside in the package java.awt, because then it could access some * package private fields of <code>Container</code> or * <code>Component</code>. Instead, it has to call * <code>Component.getSize()</code>, * which allocates memory on the heap.<p> * * <b>Todo:</b> * <ul> * <li>Ability to span components over more than one cell horizontally and vertically. * </ul> * * @author Dirk Moebius, Björn "Vampire" Kautler * @version 1.5 * @see java.awt.GridLayout */ public class VariableGridLayout implements LayoutManager2, java.io.Serializable { public static final int FIXED_NUM_ROWS = 1; public static final int FIXED_NUM_COLUMNS = 2; private static enum LayoutSize { MINIMUM, MAXIMUM, PREFERRED } /** * Creates a variable grid layout manager with the specified mode, * size, horizontal and vertical gap, eventually taking minimum and maximum * sizes into account when distributing free space, depending on takeSizesIntoAccount * and the specified distance to the borders. * * @param mode The mode in which to operate. Either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS * @param size The amount of rows for mode FIXED_NUM_ROWS or the amount of columns for mode FIXED_NUM_COLUMNS (>0) * @param hgap The horizontal space between cells (>=0) * @param vgap The vertical space between cells (>=0) * @param takeSizesIntoAccount Whether to take minimum and maximum sizes into account when distributing free space * Javier Diaz Soto (jbds) warns in #2997417 that this may cause gui freeze and provides a patch * @param distanceToBorders The distances to the borders * @throws IllegalArgumentException if mode is not either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS or size is <= 0 or hgap or vgap is < 0 */ public VariableGridLayout(int mode, int size, int hgap, int vgap, boolean takeSizesIntoAccount, Insets distanceToBorders) { if (mode != FIXED_NUM_ROWS && mode != FIXED_NUM_COLUMNS) { throw new IllegalArgumentException("illegal mode; value is " + mode); } if (size <= 0) { throw new IllegalArgumentException("size cannot be zero or less; value is " + size); } if (hgap < 0) { throw new IllegalArgumentException("hgap cannot be negative; value is " + hgap); } if (vgap < 0) { throw new IllegalArgumentException("vgap cannot be negative; value is " + vgap); } this.mode = mode; this.size = size; this.hgap = hgap; this.vgap = vgap; this.takeSizesIntoAccount = takeSizesIntoAccount; this.distanceToBorders = (Insets)distanceToBorders.clone(); } /** * Creates a variable grid layout manager with the specified mode, * size, horizontal and vertical gap, eventually taking minimum and maximum * sizes into account when distributing free space, depending on takeSizesIntoAccount * and zero distance to borders. * * @param mode The mode in which to operate. Either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS * @param size The amount of rows for mode FIXED_NUM_ROWS or the amount of columns for mode FIXED_NUM_COLUMNS (>0) * @param hgap The horizontal space between cells (>=0) * @param vgap The vertical space between cells (>=0) * @param takeSizesIntoAccount Whether to take minimum and maximum sizes into account when distributing free space * Javier Diaz Soto (jbds) warns in #2997417 that this may cause gui freeze and provides a patch * @throws IllegalArgumentException if mode is not either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS or size is <= 0 or hgap or vgap is < 0 */ public VariableGridLayout(int mode, int size, int hgap, int vgap, boolean takeSizesIntoAccount) { this(mode, size, hgap, vgap, takeSizesIntoAccount, new Insets(0,0,0,0)); } /** * Creates a variable grid layout manager with the specified mode, * size, horizontal and vertical gap, and zero distance to borders. * The minimum and maximum Component sizes are not taken into account * when distributing free space. * * @param mode The mode in which to operate. Either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS * @param size The amount of rows for mode FIXED_NUM_ROWS or the amount of columns for mode FIXED_NUM_COLUMNS * @param hgap The horizontal space between cells * @param vgap The vertical space between cells * @throws IllegalArgumentException if mode is not either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS or size is <= 0 or hgap or vgap is < 0 */ public VariableGridLayout(int mode, int size, int hgap, int vgap) { this(mode, size, hgap, vgap, false, new Insets(0,0,0,0)); } /** * Creates a variable grid layout manager with the specified mode * and size, zero horizontal and vertical gap, and zero distance to borders. * Does not take minimum and maximum Component sizes into account when distributing * free space. * * @param mode The mode in which to operate. Either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS * @param size The amount of rows for mode FIXED_NUM_ROWS or the amount of columns for mode FIXED_NUM_COLUMNS * @throws IllegalArgumentException if mode is not either FIXED_NUM_ROWS or FIXED_NUM_COLUMNS or size is <= 0 */ public VariableGridLayout(int mode, int size) { this(mode, size, 0, 0, false, new Insets(0,0,0,0)); } /** * Creates a variable grid layout manager with mode FIXED_NUM_ROWS, * number of rows == 1, zero horizontal and vertical gap, and zero distance to borders. * Does not take minimum and maximum Component sizes into account when * distributing free space. */ public VariableGridLayout() { this(FIXED_NUM_ROWS, 1, 0, 0, false, new Insets(0,0,0,0)); } /** * Not used in this class. */ public void addLayoutComponent(String name, Component component) { } /** * Not used in this class. */ public void addLayoutComponent(Component component, Object constraints) { } /** * Not used in this class. */ public void removeLayoutComponent(Component component) { } /** * Always returns 0.5. */ public float getLayoutAlignmentX(Container container) { return 0.5f; } /** * Always returns 0.5. */ public float getLayoutAlignmentY(Container container) { return 0.5f; } public Dimension preferredLayoutSize(Container parent) { return getLayoutSize(parent,LayoutSize.PREFERRED); } public Dimension minimumLayoutSize(Container parent) { return getLayoutSize(parent,LayoutSize.MINIMUM); } public Dimension maximumLayoutSize(Container parent) { return getLayoutSize(parent,LayoutSize.MAXIMUM); } public void layoutContainer(Container parent) { synchronized (parent.getTreeLock()) { update(parent); int ncomponents = parent.getComponentCount(); if (ncomponents == 0) { return; } // Pass 1: compute minimum, preferred and maximum row heights / column widths int total_height = 0; Arrays.fill(row_heights,0); Arrays.fill(col_widths,0); if (takeSizesIntoAccount) { Arrays.fill(minimum_row_heights,0); Arrays.fill(minimum_col_widths,0); Arrays.fill(maximum_row_heights,Integer.MAX_VALUE); Arrays.fill(maximum_col_widths,Integer.MAX_VALUE); } for (int r = 0, i = 0; r < nrows; r++) { for (int c = 0; c < ncols; c++, i++) { if (i < ncomponents) { Component comp = parent.getComponent(i); Dimension d = comp.getPreferredSize(); row_heights[r] = Math.max(row_heights[r], d.height); col_widths[c] = Math.max(col_widths[c], d.width); if (takeSizesIntoAccount) { d = comp.getMinimumSize(); minimum_row_heights[r] = Math.max(minimum_row_heights[r], d.height); minimum_col_widths[c] = Math.max(minimum_col_widths[c], d.width); d = comp.getMaximumSize(); maximum_row_heights[r] = Math.min(maximum_row_heights[r], d.height); maximum_col_widths[c] = Math.min(maximum_col_widths[c], d.width); } } else { break; } } if (takeSizesIntoAccount) { // correct cases where // minimum_row_heights[row] <= row_heights[row] <= maximum_row_heights[row] // is not true by clipping to the minimum_row_heights and maximum_row_heights if (minimum_row_heights[r] >= maximum_row_heights[r]) { maximum_row_heights[r] = minimum_row_heights[r]; row_heights[r] = minimum_row_heights[r]; } else if (row_heights[r] < minimum_row_heights[r]) { row_heights[r] = minimum_row_heights[r]; } else if (row_heights[r] > maximum_row_heights[r]) { row_heights[r] = maximum_row_heights[r]; } } total_height += row_heights[r]; } int total_width = 0; for (int c = 0; c < ncols; c++) { if (takeSizesIntoAccount) { // correct cases where // minimum_col_widths[col] <= col_widths[col] <= maximum_col_widths[col] // is not true by clipping to the minimum_col_widths and maximum_col_widths if (minimum_col_widths[c] >= maximum_col_widths[c]) { maximum_col_widths[c] = minimum_col_widths[c]; col_widths[c] = minimum_col_widths[c]; } else if (col_widths[c] < minimum_col_widths[c]) { col_widths[c] = minimum_col_widths[c]; } else if (col_widths[c] > maximum_col_widths[c]) { col_widths[c] = maximum_col_widths[c]; } } total_width += col_widths[c]; } // Pass 2: redistribute free space Dimension parent_size = parent.getSize(); Insets insets = parent.getInsets(); int free_height = parent_size.height - insets.top - insets.bottom - (nrows - 1) * vgap - distanceToBorders.top - distanceToBorders.bottom; int free_width = parent_size.width - insets.left - insets.right - (ncols - 1) * hgap - distanceToBorders.left - distanceToBorders.right; redistributeSpace(total_height,free_height, takeSizesIntoAccount, nrows,row_heights, minimum_row_heights, maximum_row_heights); redistributeSpace(total_width,free_width, takeSizesIntoAccount, ncols,col_widths, minimum_col_widths, maximum_col_widths); // Pass 3: layout components for (int r = 0, y = insets.top + distanceToBorders.top, i = 0; r < nrows; y += row_heights[r] + vgap, r++) { for (int c = 0, x = insets.left + distanceToBorders.left; c < ncols; x += col_widths[c] + hgap, c++, i++) { if (i < ncomponents) { Component comp = parent.getComponent(i); Dimension d = comp.getMaximumSize(); int width = col_widths[c]; int height = row_heights[r]; int xCorrection = 0; int yCorrection = 0; if (width > d.width) { xCorrection = (int)((width - d.width) * comp.getAlignmentX()); width = d.width; } if (height > d.height) { yCorrection = (int)((height-d.height) * comp.getAlignmentY()); height = d.height; } comp.setBounds(x + xCorrection, y + yCorrection, width, height); } } } } // synchronized } public void invalidateLayout(Container container) { } /** * Returns the string representation of this variable grid layout's values. * @return a string representation of this variable grid layout. */ public String toString() { return getClass().getName() + "[mode=" + ((FIXED_NUM_ROWS == mode) ? "FIXED_NUM_ROWS" : ((FIXED_NUM_COLUMNS == mode) ? "FIXED_NUM_COLUMNS" : "UNKNOWN(" + mode + ")")) + ",size=" + size + ",hgap=" + hgap + ",vgap=" + vgap + ",takeSizesIntoAccount=" + takeSizesIntoAccount + ",distanceToBorders=" + distanceToBorders + "]"; } /** * @param which if LayoutSize.MINIMUM compute minimum layout size, * if LayoutSize.MAXIMUM compute maximum layout size, * if LayoutSize.PREFERRED compute preferred layout size. */ private Dimension getLayoutSize(Container parent, LayoutSize which) { synchronized (parent.getTreeLock()) { update(parent); int ncomponents = parent.getComponentCount(); long h = 0; long w = 0; for (int r = 0, i = 0; r < nrows; r++) { int row_height = 0; for (int c = 0; c < ncols; c++, i++) { if (i < ncomponents) { switch (which) { case MINIMUM: row_height = Math.max(row_height, parent.getComponent(i).getMinimumSize().height); break; case MAXIMUM: row_height = Math.max(row_height, parent.getComponent(i).getMaximumSize().height); break; case PREFERRED: row_height = Math.max(row_height, parent.getComponent(i).getPreferredSize().height); break; default: throw new InternalError("Missing case branch for LayoutSize: " + which); } } } h += row_height; } for (int c = 0; c < ncols; c++) { int col_width = 0; for (int r = 0; r < nrows; r++) { int i = r * ncols + c; if (i < ncomponents) { switch (which) { case MINIMUM: col_width = Math.max(col_width, parent.getComponent(i).getMinimumSize().width); break; case MAXIMUM: col_width = Math.max(col_width, parent.getComponent(i).getMaximumSize().width); break; case PREFERRED: col_width = Math.max(col_width, parent.getComponent(i).getPreferredSize().width); break; default: throw new InternalError("Missing case branch for LayoutSize: " + which); } } } w += col_width; } Insets insets = parent.getInsets(); w += insets.left + insets.right + ((ncols - 1) * hgap) + distanceToBorders.left + distanceToBorders.right; h += insets.top + insets.bottom + ((nrows - 1) * vgap) + distanceToBorders.top + distanceToBorders.bottom; if (w > Integer.MAX_VALUE) { w = Integer.MAX_VALUE; } if (h > Integer.MAX_VALUE) { h = Integer.MAX_VALUE; } return new Dimension((int)w,(int)h); } } private void update(Container container) { int ncomponents = container.getComponentCount(); int old_nrows = nrows; int old_ncols = ncols; if (this.mode == FIXED_NUM_ROWS) { nrows = this.size; ncols = (ncomponents + nrows - 1) / nrows; } else { ncols = this.size; nrows = (ncomponents + ncols - 1) / ncols; } if (old_nrows != nrows) { row_heights = new int[nrows]; if (takeSizesIntoAccount) { minimum_row_heights = new int[nrows]; maximum_row_heights = new int[nrows]; } } if (old_ncols != ncols) { col_widths = new int[ncols]; if (takeSizesIntoAccount) { minimum_col_widths = new int[ncols]; maximum_col_widths = new int[ncols]; } } } private void redistributeSpace(int total_size, int free_size, boolean takeSizesIntoAccount, int nelements, int[] element_sizes, int[] minimum_element_sizes, int[] maximum_element_sizes) { if (total_size != free_size) { if (takeSizesIntoAccount) { boolean grow = total_size < free_size; // calculate the size that is available for redistribution free_size = (free_size - total_size) * (grow ? 1 : -1); while (free_size != 0) { // calculate the amount of elements that can be resized without violating // the minimum and maximum sizes and their current cumulated size int modifyableAmount = 0; int modifySize = 0; for (int i = 0 ; i < nelements ; i++) { if ((grow && (element_sizes[i] < maximum_element_sizes[i])) || (!grow && (element_sizes[i] > minimum_element_sizes[i]))) { modifyableAmount++; modifySize += element_sizes[i]; } } boolean checkBounds = true; // if all elements are at their minimum or maximum size, resize all elements if (0 == modifyableAmount) { for (int i = 0 ; i < nelements ; i++) { modifySize += element_sizes[i]; } checkBounds = false; modifyableAmount = nelements; } // to prevent an endless loop if the container gets resized to a very small amount if (modifySize == 0) { break; } // resize the elements if (free_size < modifyableAmount) { for (int i = 0 ; i < nelements ; i++) { if ((free_size != 0) && (!checkBounds || (checkBounds && (grow && (element_sizes[i] < maximum_element_sizes[i])) || (!grow && (element_sizes[i] > minimum_element_sizes[i]))))) { element_sizes[i] += (grow ? 1 : -1); if (0 > element_sizes[i]) { element_sizes[i] = 0; } free_size--; } } } else { int modifySizeAddition = 0; for (int i = 0 ; i < nelements ; i++) { int modifyableSize = (checkBounds ? (grow ? maximum_element_sizes[i] - element_sizes[i] : element_sizes[i] - minimum_element_sizes[i]) : Integer.MAX_VALUE - element_sizes[i]); int elementModifySize = (int)((double)free_size / (double)modifySize * (double)element_sizes[i]); if (elementModifySize <= modifyableSize) { element_sizes[i] += (grow ? elementModifySize : -elementModifySize); modifySizeAddition += (grow ? elementModifySize : -elementModifySize); free_size -= elementModifySize; } else { element_sizes[i] += (grow ? modifyableSize : -modifyableSize); modifySizeAddition += (grow ? modifyableSize : -modifyableSize); free_size -= modifyableSize; } if (0 > element_sizes[i]) { element_sizes[i] = 0; } } modifySize += modifySizeAddition; } } } else { double d = (double)free_size / (double)total_size; for (int i = 0; i < nelements; i++) { element_sizes[i] = (int)(element_sizes[i] * d); } } } } private int mode; private int size; private int hgap; private int vgap; private boolean takeSizesIntoAccount; private Insets distanceToBorders; private transient int nrows = -1; private transient int ncols = -1; private transient int[] minimum_row_heights = null; private transient int[] minimum_col_widths = null; private transient int[] row_heights = null; private transient int[] col_widths = null; private transient int[] maximum_row_heights = null; private transient int[] maximum_col_widths = null; }