/* * Copyright ©1998-2020 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.ui.layout; import com.trollworks.gcs.ui.scale.Scale; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.LayoutManager2; /** * Provides a standardized column layout. Columns are sized according to the preferred size of its * contents. Width is altered to accommodate available space while respecting minimum/maximum sizes, * while height is altered in in one three ways: * <ul> * <li>{@link RowDistribution#USE_PREFERRED_HEIGHT} - use the preferred height of each component. * <li>{@link RowDistribution#DISTRIBUTE_HEIGHT} - Give each component its preferred height, * distributing any excess height evenly among all components. * <li>{@link RowDistribution#GIVE_EXCESS_TO_LAST} - Give each component its preferred height, but * give any excess height to the last component in a column. * </ul> * This layout manager does not track any state specific to components, so it may be re-used in * multiple containers without creating new copies. */ public class ColumnLayout implements LayoutManager2 { /** The default horizontal gap size. */ public static final int DEFAULT_H_GAP_SIZE = 5; /** The default vertical gap size. */ public static final int DEFAULT_V_GAP_SIZE = 2; private int mColumns; private int mHGap; private int mVGap; private RowDistribution mDistribution; /** Creates a new layout with a single column. */ public ColumnLayout() { this(1, DEFAULT_H_GAP_SIZE, DEFAULT_V_GAP_SIZE, RowDistribution.USE_PREFERRED_HEIGHT); } /** * Creates a new layout with the specified number of columns. * * @param columns The number of columns. */ public ColumnLayout(int columns) { this(columns, DEFAULT_H_GAP_SIZE, DEFAULT_V_GAP_SIZE, RowDistribution.USE_PREFERRED_HEIGHT); } /** * Creates a new layout with the specified number of columns and height distribution. * * @param columns The number of columns. * @param distribution The height distribution style. */ public ColumnLayout(int columns, RowDistribution distribution) { this(columns, DEFAULT_H_GAP_SIZE, DEFAULT_V_GAP_SIZE, distribution); } /** * Creates a new layout. * * @param columns The number of columns. * @param hgap The horizontal gap value. * @param vgap The vertical gap value. */ public ColumnLayout(int columns, int hgap, int vgap) { this(columns, hgap, vgap, RowDistribution.USE_PREFERRED_HEIGHT); } /** * Creates a new layout. * * @param columns The number of columns. * @param hgap The horizontal gap value. * @param vgap The vertical gap value. * @param distribution The height distribution style. */ public ColumnLayout(int columns, int hgap, int vgap, RowDistribution distribution) { if (columns < 1) { throw new IllegalArgumentException("columns must be greater than zero"); } mColumns = columns; mHGap = hgap; mVGap = vgap; mDistribution = distribution; } @Override public void addLayoutComponent(String name, Component component) { // Nothing to do... } @Override public void addLayoutComponent(Component component, Object constraints) { // Nothing to do... } /** @return The number of columns. */ public int getColumns() { return mColumns; } /** @return The way vertical space will be distributed to components. */ public RowDistribution getHeightDistribution() { return mDistribution; } /** @return The horizontal gap used by this layout. */ public int getHorizontalGap() { return mHGap; } /** @return The vertical gap used by this layout. */ public int getVerticalGap() { return mVGap; } @Override public float getLayoutAlignmentX(Container target) { return Component.CENTER_ALIGNMENT; } @Override public float getLayoutAlignmentY(Container target) { return Component.CENTER_ALIGNMENT; } @Override public void invalidateLayout(Container target) { // Nothing to do... } @Override public void layoutContainer(Container parent) { Scale scale = Scale.get(parent); synchronized (parent.getTreeLock()) { Dimension pSize = parent.getSize(); Insets insets = parent.getInsets(); int compCount = parent.getComponentCount(); int y = insets.top; int rows = 1 + (compCount - 1) / mColumns; int[] widths = new int[mColumns]; int[] minWidths = new int[mColumns]; int[] maxWidths = new int[mColumns]; int[] heights = new int[rows]; int scaledHGap = scale.scale(mHGap); int scaledVGap = scale.scale(mVGap); int width = pSize.width - (insets.left + insets.right + (mColumns - 1) * scaledHGap); int height = pSize.height - (insets.top + insets.bottom + (rows - 1) * scaledVGap); Dimension[] prefSizes = new Dimension[compCount]; Dimension[] maxSizes = new Dimension[compCount]; Dimension[] minSizes = new Dimension[compCount]; Component comp; int i; int j; int k; int which; int portion; int participants; for (i = 0; i < compCount; i++) { comp = parent.getComponent(i); prefSizes[i] = comp.getPreferredSize(); maxSizes[i] = comp.getMaximumSize(); minSizes[i] = comp.getMinimumSize(); } for (i = 0; i < compCount; i += mColumns) { for (j = i; j < i + mColumns && j < compCount; j++) { int compWidth = prefSizes[j].width; which = j - i; if (compWidth > widths[which]) { widths[which] = compWidth; } compWidth = minSizes[j].width; if (compWidth > minWidths[which]) { minWidths[which] = compWidth; } compWidth = maxSizes[j].width; if (compWidth > maxWidths[which]) { maxWidths[which] = compWidth; } } } for (i = 0; i < mColumns; i++) { width -= widths[i]; } j = mColumns; if (width > 0) { while (width > 0 && j > 0) { portion = width / j; if (portion == 0) { portion = 1; } for (i = j = 0; i < mColumns && width != 0; i++) { if (widths[i] < maxWidths[i]) { if (widths[i] + portion <= maxWidths[i]) { widths[i] += portion; width -= portion; } else { width -= maxWidths[i] - widths[i]; widths[i] = maxWidths[i]; } } if (widths[i] < maxWidths[i]) { j++; } if (portion > width) { portion = width; } } } } else if (width < 0) { width = -width; while (width > 0 && j > 0) { portion = width / j; if (portion == 0) { portion = 1; } for (i = j = 0; i < mColumns && width != 0; i++) { if (widths[i] > minWidths[i]) { if (widths[i] - portion >= minWidths[i]) { widths[i] -= portion; width -= portion; } else { width -= widths[i] - minWidths[i]; widths[i] = minWidths[i]; } } if (widths[i] > minWidths[i]) { j++; } if (portion > width) { portion = width; } } } } portion = height; for (i = 0; i < rows; i++) { heights[i] = 0; for (j = 0; j < mColumns; j++) { k = i * mColumns + j; if (k < compCount) { k = prefSizes[k].height; if (k > heights[i]) { heights[i] = k; } } } portion -= heights[i]; } participants = rows; while (portion < 0 && participants > 0) { int newPortion = participants > 1 ? 1 + -portion / participants : -portion; participants = 0; for (i = 0; i < rows; i++) { int newHeight = heights[i] - newPortion; boolean doit = true; for (j = 0; j < mColumns; j++) { k = i * mColumns + j; if (k < compCount) { int minHeight = minSizes[k].height; if (minHeight > newHeight) { if (minHeight < heights[i]) { newHeight = minHeight; } else { doit = false; break; } } } } if (doit) { portion += heights[i] - newHeight; heights[i] = newHeight; participants++; } } } if (portion > 0 && rows > 0) { if (mDistribution == RowDistribution.DISTRIBUTE_HEIGHT) { j = portion / rows; if (j > 0) { for (i = 0; i < rows; i++) { heights[i] += j; } } for (i = 0; i < portion - j * rows; i++) { heights[i]++; } } else if (mDistribution == RowDistribution.GIVE_EXCESS_TO_LAST) { heights[rows - 1] += portion; } } for (i = 0; i < rows; i++) { int x = insets.left; for (j = 0; j < mColumns; j++) { k = i * mColumns + j; if (k < compCount) { int compY = y; int cheight = heights[i]; if (cheight < minSizes[k].height) { cheight = minSizes[k].height; } comp = parent.getComponent(k); if (cheight > maxSizes[k].height) { float align = comp.getAlignmentY(); cheight = maxSizes[k].height; if (align >= 0.75) { compY += heights[i] - cheight; } else if (align >= 0.25) { compY += (heights[i] - cheight) / 2; } } comp.setBounds(x, compY, widths[j], cheight); x += widths[j] + scaledHGap; } } y += heights[i] + scaledVGap; } } } @Override public Dimension maximumLayoutSize(Container parent) { Scale scale = Scale.get(parent); long height = 0; long width = (long) (mColumns - 1) * scale.scale(mHGap); synchronized (parent.getTreeLock()) { Insets insets = parent.getInsets(); int compCount = parent.getComponentCount(); int[] widths = new int[mColumns]; int rows = 1 + (compCount - 1) / mColumns; int i; width += insets.left + insets.right; for (int y = 0; y < rows; y++) { int rowHeight = 0; for (int x = 0; x < mColumns; x++) { i = y * mColumns + x; if (i < compCount) { Dimension size = parent.getComponent(i).getMaximumSize(); if (size.height > rowHeight) { rowHeight = size.height; } if (size.width > widths[x]) { widths[x] = size.width; } } } height += rowHeight; } height += insets.top + insets.bottom; if (rows > 0) { height += (long) (rows - 1) * scale.scale(mVGap); } for (i = 0; i < mColumns; i++) { width += widths[i]; } } if (width > LayoutSize.MAXIMUM_SIZE) { width = LayoutSize.MAXIMUM_SIZE; } if (height > LayoutSize.MAXIMUM_SIZE) { height = LayoutSize.MAXIMUM_SIZE; } return new Dimension((int) width, (int) height); } @Override public Dimension minimumLayoutSize(Container parent) { Scale scale = Scale.get(parent); int height = 0; int width = (mColumns - 1) * scale.scale(mHGap); synchronized (parent.getTreeLock()) { Insets insets = parent.getInsets(); int compCount = parent.getComponentCount(); int[] widths = new int[mColumns]; int rows = 1 + (compCount - 1) / mColumns; int i; width += insets.left + insets.right; for (int y = 0; y < rows; y++) { int rowHeight = 0; for (int x = 0; x < mColumns; x++) { i = y * mColumns + x; if (i < compCount) { Dimension size = parent.getComponent(i).getMinimumSize(); if (size.width > widths[x]) { widths[x] = size.width; } if (size.height > rowHeight) { rowHeight = size.height; } } } height += rowHeight; } height += insets.top + insets.bottom; if (rows > 0) { height += (rows - 1) * scale.scale(mVGap); } for (i = 0; i < mColumns; i++) { width += widths[i]; } } return new Dimension(width, height); } @Override public Dimension preferredLayoutSize(Container parent) { Scale scale = Scale.get(parent); int height = 0; int width = (mColumns - 1) * scale.scale(mHGap); synchronized (parent.getTreeLock()) { Insets insets = parent.getInsets(); int compCount = parent.getComponentCount(); int[] widths = new int[mColumns]; int rows = 1 + (compCount - 1) / mColumns; int i; width += insets.left + insets.right; for (int y = 0; y < rows; y++) { int rowHeight = 0; for (int x = 0; x < mColumns; x++) { i = y * mColumns + x; if (i < compCount) { Dimension size = parent.getComponent(i).getPreferredSize(); if (size.height > rowHeight) { rowHeight = size.height; } if (size.width > widths[x]) { widths[x] = size.width; } } } height += rowHeight; } height += insets.top + insets.bottom; if (rows > 0) { height += (rows - 1) * scale.scale(mVGap); } for (i = 0; i < mColumns; i++) { width += widths[i]; } } return new Dimension(width, height); } @Override public void removeLayoutComponent(Component comp) { // Nothing to do... } }