/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package com.bc.ceres.swing; import javax.swing.JPanel; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.LayoutManager2; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import java.util.Set; /** * A convenient layout manager that allows multiple components to be laid out * in form of a table-like grid. * <p/> * It uses the same parameters as Swing's standard {@link java.awt.GridBagLayout GridBagLayout} manager, however clients * don't have to deal with {@link java.awt.GridBagConstraints GridBagConstraints}. Instead, they can easily use the * the various setters to adjust single parameters. Parameters can be specified for * <ol> * <li>the entire table: {@code setTable<Param>()},</li> * <li>an entire row: {@code setRow<Param>()},</li> * <li>an entire column: {@code setColumn<Param>()},</li> * <li>each cell separately: {@code setCell<Param>()}.</li> * </ol> * Note that type-safe enums exists for the {@link com.bc.ceres.swing.TableLayout.Fill fill} and * {@link com.bc.ceres.swing.TableLayout.Anchor anchor} parameter values. * <p/> * Any parameter settings may be removed from the layout by passing {@code null} as value. * <p/> * Components are added to their container using a {@link #cell(int, int) cell(row, col)}</i> or * {@link #cell(int, int, int, int) cell(row, col, rowspan, colspan)} constraint, for example: * <pre> * panel.add(new JLabel("Iterations:"), TableLayout.cell(0, 3)); * </pre> * * @author Norman */ public class TableLayout implements LayoutManager2 { /** * This parameter is used when the component is smaller than its * display area. It determines where, within the display area, to * place the component. */ public enum Fill { /** * Do not resize the component. */ NONE(GridBagConstraints.NONE), // /** * Make the component wide enough to fill its display area horizontally, but do not change its height. */ HORIZONTAL(GridBagConstraints.HORIZONTAL), /** * Make the component tall enough to fill its display area vertically, but do not change its width. */ VERTICAL(GridBagConstraints.VERTICAL), /** * Make the component tall enough to fill its display area vertically, but do not change its width. */ BOTH(GridBagConstraints.BOTH); private final int value; private Fill(int value) { this.value = value; } public int value() { return value; } } /** * This parameter is used when the component's display area is larger * than the component's requested size. It determines whether to * resize the component, and if so, how. */ public enum Anchor { // Absolute CENTER(GridBagConstraints.CENTER), NORTH(GridBagConstraints.NORTH), NORTHEAST(GridBagConstraints.NORTHEAST), EAST(GridBagConstraints.EAST), SOUTHEAST(GridBagConstraints.SOUTHEAST), SOUTH(GridBagConstraints.SOUTH), SOUTHWEST(GridBagConstraints.SOUTHWEST), WEST(GridBagConstraints.WEST), NORTHWEST(GridBagConstraints.NORTHWEST), // Orientation relative PAGE_START(GridBagConstraints.PAGE_START), PAGE_END(GridBagConstraints.PAGE_END), LINE_START(GridBagConstraints.LINE_START), LINE_END(GridBagConstraints.LINE_END), FIRST_LINE_START(GridBagConstraints.FIRST_LINE_START), FIRST_LINE_END(GridBagConstraints.FIRST_LINE_END), LAST_LINE_START(GridBagConstraints.LAST_LINE_START), LAST_LINE_END(GridBagConstraints.LAST_LINE_END), // Baseline relative BASELINE(GridBagConstraints.BASELINE), BASELINE_LEADING(GridBagConstraints.BASELINE_LEADING), BASELINE_TRAILING(GridBagConstraints.LAST_LINE_END), ABOVE_BASELINE(GridBagConstraints.ABOVE_BASELINE), ABOVE_BASELINE_LEADING(GridBagConstraints.ABOVE_BASELINE_LEADING), ABOVE_BASELINE_TRAILING(GridBagConstraints.ABOVE_BASELINE_TRAILING), BELOW_BASELINE(GridBagConstraints.BELOW_BASELINE), BELOW_BASELINE_LEADING(GridBagConstraints.BELOW_BASELINE_LEADING), BELOW_BASELINE_TRAILING(GridBagConstraints.BELOW_BASELINE_TRAILING); private final int value; private Anchor(int value) { this.value = value; } public int value() { return value; } } private GridBagLayout gbl; private HashMap<String, Object> propertyMap; private int columnCount; private Cell currentCell; public TableLayout() { this(1); } public TableLayout(int columnCount) { this.gbl = new GridBagLayout(); this.propertyMap = new HashMap<>(32); this.columnCount = columnCount; this.currentCell = new Cell(); } public int getColumnCount() { return columnCount; } public void setColumnCount(int columnCount) { this.columnCount = columnCount; } ///////////////////////////////////////////////////////////////////////// // gridwidth public void setCellColspan(int row, int col, Integer colspan) { setCellValue("gridwidth", row, col, colspan); } ///////////////////////////////////////////////////////////////////////// // gridheight public void setCellRowspan(int row, int col, Integer rowspan) { setCellValue("gridheight", row, col, rowspan); } ///////////////////////////////////////////////////////////////////////// // insets public void setTablePadding(int hpadding, int vpadding) { setTablePadding(new Insets(0, 0, vpadding, hpadding)); setColumnPadding(0, new Insets(0, hpadding, vpadding, hpadding)); setRowPadding(0, new Insets(vpadding, 0, vpadding, hpadding)); setCellPadding(0, 0, new Insets(vpadding, hpadding, vpadding, hpadding)); } public void setTablePadding(Insets insets) { setValue("insets", insets); } public void setRowPadding(int row, Insets insets) { setRowValue("insets", row, insets); } public void setColumnPadding(int col, Insets insets) { setColumnValue("insets", col, insets); } public void setCellPadding(int row, int col, Insets insets) { setCellValue("insets", row, col, insets); } ///////////////////////////////////////////////////////////////////////// // weighty public void setTableWeightX(Double weightx) { setValue("weightx", weightx); } public void setRowWeightX(int row, Double weightx) { setRowValue("weightx", row, weightx); } public void setColumnWeightX(int col, Double weightx) { setColumnValue("weightx", col, weightx); } public void setCellWeightX(int row, int col, Double weightx) { setCellValue("weightx", row, col, weightx); } ///////////////////////////////////////////////////////////////////////// // weighty public void setTableWeightY(Double weightx) { setValue("weighty", weightx); } public void setRowWeightY(int row, Double weighty) { setRowValue("weighty", row, weighty); } public void setColumnWeightY(int col, Double weighty) { setColumnValue("weighty", col, weighty); } public void setCellWeightY(int row, int col, Double weighty) { setCellValue("weighty", row, col, weighty); } ///////////////////////////////////////////////////////////////////////// // fill public void setTableFill(Fill fill) { setValue("fill", fill); } public void setRowFill(int row, Fill fill) { setRowValue("fill", row, fill); } public void setColumnFill(int col, Fill fill) { setColumnValue("fill", col, fill); } public void setCellFill(int row, int col, Fill fill) { setCellValue("fill", row, col, fill); } ///////////////////////////////////////////////////////////////////////// // anchor public void setTableAnchor(Anchor anchor) { setValue("anchor", anchor); } public void setRowAnchor(int row, Anchor anchor) { setRowValue("anchor", row, anchor); } public void setColumnAnchor(int col, Anchor anchor) { setColumnValue("anchor", col, anchor); } public void setCellAnchor(int row, int col, Anchor anchor) { setCellValue("anchor", row, col, anchor); } ///////////////////////////////////////////////////////////////////////// // spacers public Component createHorizontalSpacer() { setCellFill(currentCell.row, currentCell.col, Fill.BOTH); setCellWeightX(currentCell.row, currentCell.col, 1.0); return new JPanel(); } public Component createVerticalSpacer() { setCellColspan(currentCell.row, 0, columnCount); setCellFill(currentCell.row, currentCell.col, Fill.BOTH); setCellWeightY(currentCell.row, currentCell.col, 1.0); return new JPanel(); } ///////////////////////////////////////////////////////////////////////// /** * 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. */ @Override public float getLayoutAlignmentX(Container target) { return gbl.getLayoutAlignmentX(target); } /** * 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. */ @Override public float getLayoutAlignmentY(Container target) { return gbl.getLayoutAlignmentY(target); } /** * Invalidates the layout, indicating that if the layout manager * has cached information it should be discarded. */ @Override public void invalidateLayout(Container target) { gbl.invalidateLayout(target); } /** * Adds the specified component to the layout, using the specified * constraint object. * * @param comp the component to be added * @param constraints where/how the component is added to the layout. */ @Override public void addLayoutComponent(Component comp, Object constraints) { if (constraints == null) { addLayoutComponent(comp, this.currentCell); } else if (constraints instanceof Cell) { addLayoutComponent(comp, (Cell) constraints); } else { throw new IllegalArgumentException("cannot add to layout: constraints must be null or a TableLayout.Cell"); } } /** * Has no effect, since this layout manager does not use a per-component string. */ @Override public void addLayoutComponent(String name, Component comp) { } /** * Removes the specified component from the layout. * * @param comp the component to be removed */ @Override public void removeLayoutComponent(Component comp) { gbl.removeLayoutComponent(comp); } /** * Lays out the specified container. * * @param parent the container to be laid out */ @Override public void layoutContainer(Container parent) { gbl.layoutContainer(parent); } /** * Calculates the minimum size dimensions for the specified * container, given the components it contains. * * @param parent the component to be laid out * @see #preferredLayoutSize */ @Override public Dimension minimumLayoutSize(Container parent) { return gbl.minimumLayoutSize(parent); } /** * Calculates the preferred size dimensions for the specified * container, given the components it contains. * * @param parent the container to be laid out * @see #minimumLayoutSize */ @Override public Dimension preferredLayoutSize(Container parent) { return gbl.preferredLayoutSize(parent); } /** * Calculates the maximum size dimensions for the specified container, * given the components it contains. * * @see java.awt.Component#getMaximumSize * @see java.awt.LayoutManager */ @Override public Dimension maximumLayoutSize(Container parent) { return gbl.maximumLayoutSize(parent); } public static Cell cell() { return new Cell(); } public static Cell cell(int row, int col) { return new Cell(row, col); } public static Cell cell(int row, int col, int rowspan, int colspan) { return new Cell(row, col, rowspan, colspan); } ///////////////////////////////////////////////////////////////////////// private void addLayoutComponent(Component comp, final Cell cell) { final GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = cell.col; gbc.gridy = cell.row; gbc.gridwidth = cell.colspan; gbc.gridheight = cell.rowspan; setField(gbc, "gridwidth", cell); setField(gbc, "gridheight", cell); setField(gbc, "weightx", cell); setField(gbc, "weighty", cell); setField(gbc, "fill", cell); setField(gbc, "anchor", cell); setField(gbc, "insets", cell); gbl.addLayoutComponent(comp, gbc); this.currentCell.col = gbc.gridx + gbc.gridwidth; if (this.currentCell.col >= columnCount) { this.currentCell.col = 0; this.currentCell.row++; // todo - consider gbc.gridheight } } private void setField(final Object object, final String name, final Cell cell) { Object value = getValue(name, cell); if (value != null) { final Field field; try { field = object.getClass().getField(name); field.set(object, value); } catch (Exception e) { throw new IllegalStateException(name, e); } } } private Object getValue(String name, Cell cell) { Object value; // 1. Get value for cell value = getCellValue(name, cell.row, cell.col); if (value == null) { // 2. Get value for column value = getColumnValue(name, cell.col); if (value == null) { // 3. Get value for row value = getRowValue(name, cell.row); if (value == null) { // 4. Get value for table value = getValue(name); } } } if (value instanceof Fill) { value = ((Fill) value).value(); } else if (value instanceof Anchor) { value = ((Anchor) value).value(); } return value; } private Object getCellValue(String name, int row, int col) { return getValue(getCellName(name, row, col)); } private void setCellValue(String name, int row, int col, Object value) { setValue(getCellName(name, row, col), value); } private Object getRowValue(String name, int row) { return getValue(getCellName(name, row, -1)); } private void setRowValue(String name, int row, Object value) { setValue(getCellName(name, row, -1), value); } private Object getColumnValue(String name, int col) { return getValue(getCellName(name, -1, col)); } private void setColumnValue(String name, int col, Object value) { setValue(getCellName(name, -1, col), value); } private Object getValue(final String name) { return propertyMap.get(name); } private void setValue(String name, Object value) { if (value != null) { propertyMap.put(name, value); } else { propertyMap.remove(name); } } private static String getCellName(String name, int row, int col) { StringBuilder sb = new StringBuilder(name); sb.append('('); appendIndex(sb, row); sb.append(','); appendIndex(sb, col); sb.append(')'); return sb.toString(); } private static void appendIndex(StringBuilder sb, int index) { if (index >= 0) { sb.append(index); } else { sb.append('*'); } } public static class Cell { public int row = 0; public int col = 0; public int rowspan = 1; public int colspan = 1; public Cell() { } public Cell(int row, int col) { this.row = row; this.col = col; } public Cell(int row, int col, int rowspan, int colspan) { this.row = row; this.col = col; this.rowspan = rowspan; this.colspan = colspan; } } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getName()); sb.append('['); Set<Map.Entry<String, Object>> entries = propertyMap.entrySet(); for (Map.Entry<String, Object> entry : entries) { sb.append(entry.getKey()).append('=').append(entry.getValue()); sb.append(','); } sb.append(']'); return sb.toString(); } }