blob: 5cd2b80f66e17ec9fc0b5c19e16496efa4189cc5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.pivot.wtk.skin;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Dimensions;
import org.apache.pivot.wtk.GraphicsUtilities;
import org.apache.pivot.wtk.Insets;
import org.apache.pivot.wtk.Orientation;
import org.apache.pivot.wtk.TablePane;
import org.apache.pivot.wtk.TablePaneAttributeListener;
import org.apache.pivot.wtk.TablePaneListener;
/**
* Table pane skin.
*/
public class TablePaneSkin extends ContainerSkin implements TablePane.Skin, TablePaneListener,
TablePaneAttributeListener {
private Insets padding = Insets.NONE;
private int horizontalSpacing = 0;
private int verticalSpacing = 0;
private boolean showHorizontalGridLines = false;
private boolean showVerticalGridLines = false;
private Color horizontalGridColor;
private Color verticalGridColor;
private Color highlightBackgroundColor = Color.GRAY;
private int[] columnWidths = null;
private int[] rowHeights = null;
public TablePaneSkin() {
horizontalGridColor = defaultForegroundColor();
verticalGridColor = defaultForegroundColor();
}
@Override
public void install(Component component) {
super.install(component);
TablePane tablePane = (TablePane) component;
tablePane.getTablePaneListeners().add(this);
tablePane.getTablePaneAttributeListeners().add(this);
}
@Override
public int getPreferredWidth(int height) {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int[] columnWidthsLocal = new int[columnCount];
int[] relativeWeights = new int[columnCount];
boolean[] defaultWidthColumns = new boolean[columnCount];
int totalRelativeWeight = 0;
// First, we calculate the base widths of the columns, giving relative
// columns their preferred width
for (int i = 0; i < columnCount; i++) {
TablePane.Column column = columns.get(i);
int columnWidth = column.getWidth();
boolean isRelative = column.isRelative();
defaultWidthColumns[i] = (columnWidth < 0);
if (isRelative) {
relativeWeights[i] = columnWidth;
totalRelativeWeight += columnWidth;
}
if (columnWidth < 0 || isRelative) {
columnWidth = getPreferredColumnWidth(i);
}
columnWidthsLocal[i] = columnWidth;
}
// Next, we adjust the widths of the relative columns upwards where
// necessary to reconcile their widths relative to one another while
// ensuring that they still get at least their preferred width
if (totalRelativeWeight > 0) {
int totalRelativeWidth = 0;
// Calculate the total relative width after the required upward
// adjustments
for (int i = 0; i < columnCount; i++) {
int columnWidth = columnWidthsLocal[i];
int relativeWeight = relativeWeights[i];
if (relativeWeight > 0) {
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
totalRelativeWidth = Math.max(totalRelativeWidth,
(int) (columnWidth / weightPercentage));
}
}
// Perform the upward adjustments using the total relative width
for (int i = 0; i < columnCount; i++) {
int relativeWeight = relativeWeights[i];
if (relativeWeight > 0) {
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
columnWidthsLocal[i] = (int) (weightPercentage * totalRelativeWidth);
}
}
}
// Finally, we account for spanning cells, which have been ignored thus
// far. If any spanned cell is default-width (including relative width
// columns), then we ensure that the sum of the widths of the spanned
// cells is enough to satisfy the preferred width of the spanning
// content
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int columnSpan = TablePane.getColumnSpan(component);
if (columnSpan > 1) {
// We might need to adjust column widths to accomodate
// this spanning cell. First, we find out if any of the
// spanned cells are default width and how much space
// we've allocated thus far for those cells
int spannedDefaultWidthCellCount = 0;
int spannedRelativeWeight = 0;
int spannedWidth = 0;
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
if (defaultWidthColumns[j + k]) {
spannedDefaultWidthCellCount++;
}
spannedRelativeWeight += relativeWeights[j + k];
spannedWidth += columnWidthsLocal[j + k];
}
if (spannedRelativeWeight > 0 || spannedDefaultWidthCellCount > 0) {
int rowHeight = row.isRelative() ? -1 : row.getHeight();
int componentPreferredWidth = component.getPreferredWidth(rowHeight);
if (componentPreferredWidth > spannedWidth) {
// The component's preferred width is larger
// than the width we've allocated thus far, so
// an adjustment is necessary
int adjustment = componentPreferredWidth - spannedWidth;
if (spannedRelativeWeight > 0) {
// We'll distribute the adjustment across
// the spanned relative columns and adjust
// other relative column widths to keep all
// relative column widths reconciled
float unitAdjustment = adjustment
/ (float) spannedRelativeWeight;
for (int k = 0; k < columnCount; k++) {
int relativeWeight = relativeWeights[k];
if (relativeWeight > 0) {
int columnAdjustment = Math.round(unitAdjustment
* relativeWeight);
columnWidthsLocal[k] += columnAdjustment;
}
}
} else {
// We'll distribute the adjustment evenly
// among the default-width columns
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
if (defaultWidthColumns[j + k]) {
int columnAdjustment = adjustment
/ spannedDefaultWidthCellCount;
columnWidthsLocal[j + k] += columnAdjustment;
// Adjust these to avoid rounding
// errors
adjustment -= columnAdjustment;
spannedDefaultWidthCellCount--;
}
}
}
}
}
}
}
}
}
// The preferred width of the table pane is the sum of the column
// widths, plus padding and spacing
boolean[][] occupiedCells = getOccupiedCells();
int visibleColumnCount = 0;
int preferredWidth = padding.left + padding.right;
for (int j = 0; j < columnCount; j++) {
boolean columnVisible = false;
for (int i = 0; i < rowCount; i++) {
if (occupiedCells[i][j]) {
columnVisible = true;
break;
}
}
if (columnVisible) {
preferredWidth += columnWidthsLocal[j];
visibleColumnCount++;
}
}
if (visibleColumnCount > 1) {
preferredWidth += (visibleColumnCount - 1) * horizontalSpacing;
}
return preferredWidth;
}
@Override
public int getPreferredHeight(int width) {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int[] rowHeightsLocal = new int[rowCount];
int[] relativeWeights = new int[rowCount];
boolean[] defaultHeightRows = new boolean[rowCount];
int totalRelativeWeight = 0;
int widthUpdated = width;
if (widthUpdated < 0) {
widthUpdated = getPreferredWidth(-1);
}
int[] columnWidthsLocal = getColumnWidths(widthUpdated);
// First, we calculate the base heights of the rows, giving relative
// rows their preferred height
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
int rowHeight = row.getHeight();
boolean isRelative = row.isRelative();
defaultHeightRows[i] = (rowHeight < 0);
if (isRelative) {
relativeWeights[i] = rowHeight;
totalRelativeWeight += rowHeight;
}
if (rowHeight < 0 || isRelative) {
rowHeight = getPreferredRowHeight(i, columnWidthsLocal);
}
rowHeightsLocal[i] = rowHeight;
}
// Next, we adjust the heights of the relative rows upwards where
// necessary to reconcile their heights relative to one another while
// ensuring that they still get at least their preferred height
if (totalRelativeWeight > 0) {
int totalRelativeHeight = 0;
// Calculate the total relative height after the required upward
// adjustments
for (int i = 0; i < rowCount; i++) {
int rowHeight = rowHeightsLocal[i];
int relativeWeight = relativeWeights[i];
if (relativeWeight > 0) {
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
totalRelativeHeight = Math.max(totalRelativeHeight,
(int) (rowHeight / weightPercentage));
}
}
// Perform the upward adjustments using the total relative height
for (int i = 0; i < rowCount; i++) {
int relativeWeight = relativeWeights[i];
if (relativeWeight > 0) {
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
rowHeightsLocal[i] = (int) (weightPercentage * totalRelativeHeight);
}
}
}
// Finally, we account for spanning cells, which have been ignored thus
// far. If any spanned cell is default-height (including relative height
// rows), then we ensure that the sum of the heights of the spanned
// cells is enough to satisfy the preferred height of the spanning
// content
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int rowSpan = TablePane.getRowSpan(component);
if (rowSpan > 1) {
// We might need to adjust row heights to accomodate
// this spanning cell. First, we find out if any of the
// spanned cells are default height and how much space
// we've allocated thus far for those cells
int spannedDefaultHeightCellCount = 0;
int spannedRelativeWeight = 0;
int spannedHeight = 0;
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
if (defaultHeightRows[i + k]) {
spannedDefaultHeightCellCount++;
}
spannedRelativeWeight += relativeWeights[i + k];
spannedHeight += rowHeightsLocal[i + k];
}
if (spannedRelativeWeight > 0 || spannedDefaultHeightCellCount > 0) {
int componentPreferredHeight = component.getPreferredHeight(columnWidthsLocal[j]);
if (componentPreferredHeight > spannedHeight) {
// The component's preferred height is larger
// than the height we've allocated thus far, so
// an adjustment is necessary
int adjustment = componentPreferredHeight - spannedHeight;
if (spannedRelativeWeight > 0) {
// We'll distribute the adjustment across
// the spanned relative rows and adjust
// other relative row heights to keep all
// relative row heights reconciled
float unitAdjustment = adjustment
/ (float) spannedRelativeWeight;
for (int k = 0; k < rowCount; k++) {
int relativeWeight = relativeWeights[k];
if (relativeWeight > 0) {
int rowAdjustment = Math.round(unitAdjustment
* relativeWeight);
rowHeightsLocal[k] += rowAdjustment;
}
}
} else {
// We'll distribute the adjustment evenly
// among the default-height rows
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
if (defaultHeightRows[i + k]) {
int rowAdjustment = adjustment
/ spannedDefaultHeightCellCount;
rowHeightsLocal[i + k] += rowAdjustment;
// Adjust these to avoid rounding
// errors
adjustment -= rowAdjustment;
spannedDefaultHeightCellCount--;
}
}
}
}
}
}
}
}
}
// The preferred height of the table pane is the sum of the row
// heights, plus padding and spacing
boolean[][] occupiedCells = getOccupiedCells();
int visibleRowCount = 0;
int preferredHeight = padding.top + padding.bottom;
for (int i = 0; i < rowCount; i++) {
boolean rowVisible = false;
for (int j = 0; j < columnCount; j++) {
if (occupiedCells[i][j]) {
rowVisible = true;
break;
}
}
if (rowVisible) {
preferredHeight += rowHeightsLocal[i];
visibleRowCount++;
}
}
if (visibleRowCount > 1) {
preferredHeight += (visibleRowCount - 1) * verticalSpacing;
}
return preferredHeight;
}
@Override
public Dimensions getPreferredSize() {
// TODO Optimize by performing calculations here
int preferredWidth = getPreferredWidth(-1);
int preferredHeight = getPreferredHeight(preferredWidth);
return new Dimensions(preferredWidth, preferredHeight);
}
@Override
public int getBaseline(int width, int height) {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int[] columnWidthsLocal = getColumnWidths(width);
int[] rowHeightsLocal = getRowHeights(height, columnWidthsLocal);
boolean[][] occupiedCells = getOccupiedCells();
int baseline = -1;
int rowY = padding.top;
for (int i = 0; i < rowCount && baseline == -1; i++) {
TablePane.Row row = rows.get(i);
boolean rowVisible = false;
for (int j = 0, n = row.getLength(); j < n && j < columnCount && baseline == -1; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int columnSpan = Math.min(TablePane.getColumnSpan(component), columnCount - j);
int componentWidth = (columnSpan - 1) * horizontalSpacing;
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
componentWidth += columnWidthsLocal[j + k];
}
int rowSpan = Math.min(TablePane.getRowSpan(component), rowCount - i);
int componentHeight = (rowSpan - 1) * verticalSpacing;
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
componentHeight += rowHeightsLocal[i + k];
}
baseline = component.getBaseline(Math.max(componentWidth, 0),
Math.max(componentHeight, 0));
if (baseline != -1) {
baseline += rowY;
}
}
rowVisible |= occupiedCells[i][j];
}
if (rowVisible) {
rowY += (rowHeightsLocal[i] + verticalSpacing);
}
}
return baseline;
}
@Override
public void layout() {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int width = getWidth();
int height = getHeight();
// NOTE We cache column widths and row heights to make getColumnAt()
// and getRowAt() more efficient
columnWidths = getColumnWidths(width);
rowHeights = getRowHeights(height, columnWidths);
// Determine which rows and column should be visible so we know which
// ones should be collapsed
boolean[] visibleRows = new boolean[rowCount];
boolean[] visibleColumns = new boolean[columnCount];
boolean[][] occupiedCells = getOccupiedCells();
for (int i = 0; i < rowCount; i++) {
for (int j = 0; j < columnCount; j++) {
if (occupiedCells[i][j]) {
visibleRows[i] = true;
visibleColumns[j] = true;
}
}
}
int componentY = padding.top;
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
int componentX = padding.left;
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component child = row.get(j);
if (child != null && child.isVisible()) {
child.setLocation(componentX, componentY);
int columnSpan = TablePane.getColumnSpan(child);
columnSpan = Math.min(columnSpan, columnCount - j);
int childWidth = (columnSpan - 1) * horizontalSpacing;
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
childWidth += columnWidths[j + k];
}
int rowSpan = TablePane.getRowSpan(child);
rowSpan = Math.min(rowSpan, rowCount - i);
int childHeight = (rowSpan - 1) * verticalSpacing;
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
childHeight += rowHeights[i + k];
}
// Set the component's size
child.setSize(Math.max(childWidth, 0), Math.max(childHeight, 0));
}
if (visibleColumns[j]) {
componentX += (columnWidths[j] + horizontalSpacing);
}
}
if (visibleRows[i]) {
componentY += (rowHeights[i] + verticalSpacing);
}
}
}
@Override
public void paint(Graphics2D graphics) {
super.paint(graphics);
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int width = getWidth();
int height = getHeight();
graphics.setPaint(highlightBackgroundColor);
// Paint the highlighted rows
for (int i = 0, rowY = padding.top; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
if (row.isHighlighted()) {
graphics.fillRect(0, rowY, width, rowHeights[i]);
}
rowY += rowHeights[i] + verticalSpacing;
}
// Paint the highlighted columns
for (int j = 0, columnX = padding.left; j < columnCount; j++) {
TablePane.Column column = columns.get(j);
if (column.isHighlighted()) {
graphics.fillRect(columnX, 0, columnWidths[j], height);
}
columnX += columnWidths[j] + horizontalSpacing;
}
// Paint the grid lines
if ((showHorizontalGridLines && verticalSpacing > 0)
|| (showVerticalGridLines && horizontalSpacing > 0)) {
Graphics2D gridGraphics = (Graphics2D) graphics.create();
gridGraphics.setStroke(new BasicStroke());
gridGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Find any components that span multiple rows or columns, and
// ensure that the grid lines don't get painted through their
// cells. We'll only instantiate gridClip if we find such cells
Area gridClip = null;
for (int i = 0, componentY = padding.top; i < rowCount; i++) {
for (int j = 0, componentX = padding.left; j < columnCount; j++) {
Component component = tablePane.getCellComponent(i, j);
if (component != null) {
int rowSpan = TablePane.getRowSpan(component);
int columnSpan = TablePane.getColumnSpan(component);
if (rowSpan > 1 || columnSpan > 1) {
int rowY = componentY;
int columnX = componentX;
int rowHeight = rowHeights[i];
int columnWidth = columnWidths[j];
for (int k = i + 1; k < i + rowSpan && k < rowCount; k++) {
rowHeight += rowHeights[k] + verticalSpacing;
}
for (int k = j + 1; k < j + columnSpan && k < columnCount; k++) {
columnWidth += columnWidths[k] + horizontalSpacing;
}
if (gridClip == null) {
gridClip = new Area(graphics.getClip());
}
if (horizontalSpacing > 1) {
columnWidth += horizontalSpacing - 1;
columnX -= (int) ((horizontalSpacing * 0.5f) - 0.5f);
}
if (verticalSpacing > 1) {
rowHeight += verticalSpacing - 1;
rowY -= (int) ((verticalSpacing * 0.5f) - 0.5f);
}
Rectangle2D.Float bounds = new Rectangle2D.Float(columnX, rowY,
columnWidth, rowHeight);
gridClip.subtract(new Area(bounds));
}
}
componentX += columnWidths[j] + horizontalSpacing;
}
componentY += rowHeights[i] + verticalSpacing;
}
if (gridClip != null) {
gridGraphics.clip(gridClip);
}
boolean[][] occupiedCells = getOccupiedCells();
if (showHorizontalGridLines && verticalSpacing > 0 && rowCount > 1) {
gridGraphics.setPaint(horizontalGridColor);
int rowY = padding.top;
int visibleRowCount = 0;
for (int i = 0; i < rowCount; i++) {
boolean rowVisible = false;
for (int j = 0; j < columnCount; j++) {
if (occupiedCells[i][j]) {
rowVisible = true;
break;
}
}
if (rowVisible) {
if (visibleRowCount++ > 0) {
int gridY = Math.max(rowY - (int) Math.ceil(verticalSpacing * 0.5f), 0);
GraphicsUtilities.drawLine(gridGraphics, 0, gridY, width,
Orientation.HORIZONTAL);
}
rowY += (rowHeights[i] + verticalSpacing);
}
}
}
if (showVerticalGridLines && horizontalSpacing > 0 && columnCount > 1) {
gridGraphics.setPaint(verticalGridColor);
int columnX = padding.left;
int visibleColumnCount = 0;
for (int j = 0; j < columnCount; j++) {
boolean columnVisible = false;
for (int i = 0; i < rowCount; i++) {
if (occupiedCells[i][j]) {
columnVisible = true;
break;
}
}
if (columnVisible) {
if (visibleColumnCount++ > 0) {
int gridX = Math.max(
columnX - (int) Math.ceil(horizontalSpacing * 0.5), 0);
GraphicsUtilities.drawLine(gridGraphics, gridX, 0, height,
Orientation.VERTICAL);
}
columnX += (columnWidths[j] + horizontalSpacing);
}
}
}
gridGraphics.dispose();
}
}
/**
* @return The amount of space that will be reserved around the inside edges
* of the table pane.
*/
public Insets getPadding() {
return padding;
}
/**
* Sets the amount of space that will be reserved around the inside edges of
* the table pane.
*
* @param padding The individual padding amounts for each edge.
*/
public void setPadding(Insets padding) {
if (padding == null) {
throw new IllegalArgumentException("padding is null.");
}
this.padding = padding;
invalidateComponent();
}
/**
* Sets the amount of space that will be reserved around the inside edges of
* the table pane.
*
* @param padding The single padding value to use for all the edges.
*/
public final void setPadding(int padding) {
setPadding(new Insets(padding));
}
/**
* Sets the amount of space that will be reserved around the inside edges of
* the table pane.
*
* @param padding A dictionary with keys in the set {left, top, bottom,
* right}.
*/
public final void setPadding(Dictionary<String, ?> padding) {
if (padding == null) {
throw new IllegalArgumentException("padding is null.");
}
setPadding(new Insets(padding));
}
/**
* Sets the amount of space that will be reserved around the inside edges of
* the table pane.
*
* @param padding The single padding value to use for all the edges.
*/
public final void setPadding(Number padding) {
if (padding == null) {
throw new IllegalArgumentException("padding is null.");
}
setPadding(padding.intValue());
}
/**
* Sets the amount of space that will be reserved around the inside edges of
* the table pane.
*
* @param padding A string containing an integer or a JSON dictionary with
* keys left, top, bottom, and/or right.
*/
public final void setPadding(String padding) {
if (padding == null) {
throw new IllegalArgumentException("padding is null.");
}
setPadding(Insets.decode(padding));
}
/**
* @return The spacing that will be applied between the table pane's columns
* during layout.
*/
public int getHorizontalSpacing() {
return horizontalSpacing;
}
/**
* Sets the spacing that will be applied between the table pane's columns
* during layout.
*
* @param horizontalSpacing The new spacing value.
*/
public void setHorizontalSpacing(int horizontalSpacing) {
if (horizontalSpacing < 0) {
throw new IllegalArgumentException("horizontalSpacing is negative");
}
this.horizontalSpacing = horizontalSpacing;
invalidateComponent();
}
/**
* @return The spacing that will be applied in between the table pane's rows
* during layout.
*/
public int getVerticalSpacing() {
return verticalSpacing;
}
/**
* Sets the spacing that will be applied in between the table pane's rows
* during layout.
*
* @param verticalSpacing The new spacing value.
*/
public void setVerticalSpacing(int verticalSpacing) {
if (verticalSpacing < 0) {
throw new IllegalArgumentException("verticalSpacing is negative");
}
this.verticalSpacing = verticalSpacing;
invalidateComponent();
}
/**
* @return Whether or not horizontal grid lines will be painted in between the
* table pane's rows.
*/
public boolean getShowHorizontalGridLines() {
return showHorizontalGridLines;
}
/**
* Sets whether or not horizontal grid lines will be painted in between the
* table pane's rows.
*
* @param showHorizontalGridLines Whether to show the horizontal lines.
*/
public void setShowHorizontalGridLines(boolean showHorizontalGridLines) {
this.showHorizontalGridLines = showHorizontalGridLines;
repaintComponent();
}
/**
* @return Whether or not vertical grid lines will be painted in between the
* table pane's columns.
*/
public boolean getShowVerticalGridLines() {
return showVerticalGridLines;
}
/**
* Sets whether or not vertical grid lines will be painted in between the
* table pane's columns.
*
* @param showVerticalGridLines Whether to show the vertical lines.
*/
public void setShowVerticalGridLines(boolean showVerticalGridLines) {
this.showVerticalGridLines = showVerticalGridLines;
repaintComponent();
}
/**
* @return The color used to paint the table pane's horizontal grid lines.
*/
public Color getHorizontalGridColor() {
return horizontalGridColor;
}
/**
* Sets the color used to paint the table pane's horizontal grid lines.
*
* @param horizontalGridColor The new grid line color.
*/
public void setHorizontalGridColor(Color horizontalGridColor) {
if (horizontalGridColor == null) {
throw new IllegalArgumentException("horizontalGridColor is null.");
}
this.horizontalGridColor = horizontalGridColor;
if (showHorizontalGridLines || showVerticalGridLines) {
repaintComponent();
}
}
/**
* Sets the color used to paint the table pane's horizontal grid lines.
*
* @param horizontalGridColor Any of the
* {@linkplain GraphicsUtilities#decodeColor color values recognized by
* Pivot}.
*/
public final void setHorizontalGridColor(String horizontalGridColor) {
if (horizontalGridColor == null) {
throw new IllegalArgumentException("horizontalGridColor is null.");
}
setHorizontalGridColor(GraphicsUtilities.decodeColor(horizontalGridColor));
}
/**
* @return The color used to paint the table pane's vertical grid lines.
*/
public Color getVerticalGridColor() {
return verticalGridColor;
}
/**
* Sets the color used to paint the table pane's vertical grid lines.
*
* @param verticalGridColor The new grid line color.
*/
public void setVerticalGridColor(Color verticalGridColor) {
if (verticalGridColor == null) {
throw new IllegalArgumentException("verticalGridColor is null.");
}
this.verticalGridColor = verticalGridColor;
if (showHorizontalGridLines || showVerticalGridLines) {
repaintComponent();
}
}
/**
* Sets the color used to paint the table pane's vertical grid lines.
*
* @param verticalGridColor Any of the
* {@linkplain GraphicsUtilities#decodeColor color values recognized by
* Pivot}.
*/
public final void setVerticalGridColor(String verticalGridColor) {
if (verticalGridColor == null) {
throw new IllegalArgumentException("verticalGridColor is null.");
}
setVerticalGridColor(GraphicsUtilities.decodeColor(verticalGridColor));
}
/**
* @return The background color used to paint the highlighted rows and columns.
*/
public Color getHighlightBackgroundColor() {
return highlightBackgroundColor;
}
/**
* Sets the background color used to paint the highlighted rows and columns.
*
* @param highlightBackgroundColor The new highlight color.
*/
public void setHighlightBackgroundColor(Color highlightBackgroundColor) {
if (highlightBackgroundColor == null) {
throw new IllegalArgumentException("highlightBackgroundColor is null.");
}
this.highlightBackgroundColor = highlightBackgroundColor;
repaintComponent();
}
/**
* Sets the background color used to paint the highlighted rows and columns.
*
* @param highlightBackgroundColor Any of the
* {@linkplain GraphicsUtilities#decodeColor color values recognized by
* Pivot}.
*/
public final void setHighlightBackgroundColor(String highlightBackgroundColor) {
if (highlightBackgroundColor == null) {
throw new IllegalArgumentException("highlightBackgroundColor is null.");
}
setHighlightBackgroundColor(GraphicsUtilities.decodeColor(highlightBackgroundColor));
}
/**
* Returns a grid indicating which cells are occupied. A component is said
* to occupy a cell if it is visible and either lives in the cell directly
* or spans the cell. Conversely, vacant cells do not have visible
* components within them or spanning them.
*
* @return A grid of booleans, where occupied cells are denoted by
* <tt>true</tt>, and vacant cells are denoted by <tt>false</tt>
*/
private boolean[][] getOccupiedCells() {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
boolean[][] occupiedCells = new boolean[rowCount][columnCount];
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int rowSpan = TablePane.getRowSpan(component);
int columnSpan = TablePane.getColumnSpan(component);
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
for (int l = 0; l < columnSpan && j + l < columnCount; l++) {
occupiedCells[i + k][j + l] = true;
}
}
}
}
}
return occupiedCells;
}
/**
* Gets the preferred width of a table pane column, which is defined as the
* maximum preferred width of the column's visible components. <p> Because
* their preferred width relates to the preferred widths of other columns,
* components that span multiple columns will not be considered in this
* calculation (even if they live in the column directly). It is up to the
* caller to factor such components into the column widths calculation.
*
* @param columnIndex The index of the column whose preferred width we're
* calculating
*/
private int getPreferredColumnWidth(int columnIndex) {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
int preferredWidth = 0;
for (int i = 0, n = rows.getLength(); i < n; i++) {
TablePane.Row row = rows.get(i);
if (row.getLength() > columnIndex) {
Component component = row.get(columnIndex);
if (component != null && component.isVisible()
&& TablePane.getColumnSpan(component) == 1) {
preferredWidth = Math.max(preferredWidth, component.getPreferredWidth(-1));
}
}
}
return preferredWidth;
}
/**
* Tells whether or not the specified column is visible. A column is visible
* if and only if one or more visible components occupies it. A component is
* said to occupy a cell if it either lives in the cell directly or spans
* the cell.
*
* @param columnIndex The index of the column within the table pane
* @return <tt>true</tt> if the column is visible; <tt>false</tt> otherwise
*/
private boolean isColumnVisible(int columnIndex) {
boolean visible = false;
boolean[][] occupiedCells = getOccupiedCells();
for (int i = 0; i < occupiedCells.length; i++) {
if (occupiedCells[i][columnIndex]) {
visible = true;
break;
}
}
return visible;
}
/**
* Gets the preferred height of a table pane row, which is defined as the
* maximum preferred height of the row's visible components. The preferred
* height of each constituent component will be constrained by the width of
* the column that the component occupies (as specified in the array of
* column widths). <p> Because their preferred height relates to the
* preferred heights of other rows, components that span multiple rows will
* not be considered in this calculation (even if they live in the column
* directly). It is up to the caller to factor such components into the row
* heights calculation.
*
* @param rowIndex The index of the row whose preferred height we're
* calculating
* @param columnWidthsArgument An array of column width values corresponding
* to the columns of the table pane
*/
private int getPreferredRowHeight(int rowIndex, int[] columnWidthsArgument) {
if (columnWidthsArgument == null) {
throw new IllegalArgumentException("columnWidths is null");
}
TablePane tablePane = (TablePane) getComponent();
TablePane.ColumnSequence columns = tablePane.getColumns();
TablePane.Row row = tablePane.getRows().get(rowIndex);
int preferredHeight = 0;
for (int j = 0, n = row.getLength(), m = columns.getLength(); j < n && j < m; j++) {
Component component = row.get(j);
if (component != null && component.isVisible() && TablePane.getRowSpan(component) == 1) {
preferredHeight = Math.max(preferredHeight,
component.getPreferredHeight(columnWidthsArgument[j]));
}
}
return preferredHeight;
}
/**
* Tells whether or not the specified row is visible. A row is visible if
* and only if one or more visible components occupies it. A component is
* said to occupy a cell if it either lives in the cell directly or spans
* the cell.
*
* @param rowIndex The index of the row within the table pane
* @return <tt>true</tt> if the row is visible; <tt>false</tt> otherwise
*/
private boolean isRowVisible(int rowIndex) {
boolean visible = false;
boolean[][] occupiedCells = getOccupiedCells();
for (int j = 0; j < occupiedCells[rowIndex].length; j++) {
if (occupiedCells[rowIndex][j]) {
visible = true;
break;
}
}
return visible;
}
/**
* Gets the width of each table pane column given the specified table pane
* width.
*
* @param width The width constraint of the table pane
* @return An array containing the width of each column in the table pane
* given the specified constraint
*/
private int[] getColumnWidths(int width) {
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
TablePane.ColumnSequence columns = tablePane.getColumns();
int rowCount = rows.getLength();
int columnCount = columns.getLength();
int[] columnWidthsLocal = new int[columnCount];
boolean[] defaultWidthColumns = new boolean[columnCount];
int totalRelativeWeight = 0;
int visibleColumnCount = 0;
int reservedWidth = padding.left + padding.right;
// First, we allocate the widths of non-relative columns. We store the
// widths of relative columns as negative values for later processing
for (int j = 0; j < columnCount; j++) {
if (isColumnVisible(j)) {
TablePane.Column column = columns.get(j);
int columnWidth = column.getWidth();
if (column.isRelative()) {
columnWidthsLocal[j] = -columnWidth;
totalRelativeWeight += columnWidth;
} else {
if (columnWidth < 0) {
// Default width column; we must calculate the width
columnWidth = getPreferredColumnWidth(j);
defaultWidthColumns[j] = true;
}
columnWidthsLocal[j] = columnWidth;
reservedWidth += columnWidth;
}
visibleColumnCount++;
} else {
columnWidthsLocal[j] = 0;
}
}
if (visibleColumnCount > 1) {
reservedWidth += (visibleColumnCount - 1) * horizontalSpacing;
}
// Next, we we account for default-width columns containing spanning
// cells, which have been ignored thus far. We ensure that the sum of
// the widths of the spanned cells is enough to satisfy the preferred
// width of the spanning content.
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int columnSpan = TablePane.getColumnSpan(component);
if (columnSpan > 1) {
// We might need to adjust column widths to accomodate
// this spanning cell. First, we find out if any of the
// spanned cells are default width and how much space
// we've allocated thus far for those cells
boolean adjustCells = true;
int spannedDefaultWidthCellCount = 0;
int spannedWidth = 0;
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
if (columnWidthsLocal[j + k] < 0) {
adjustCells = false;
break;
}
if (defaultWidthColumns[j + k]) {
spannedDefaultWidthCellCount++;
}
spannedWidth += columnWidthsLocal[j + k];
}
// If we span any relative-width columns, we assume
// that we'll achieve the desired spanning width when
// we divvy up the remaining space, so there's no need
// to make an adjustment here. This assumption is safe
// because our preferred width policy is to *either*
// divide the adjustment among the relative-width
// columns *or* among the default-width columns if we
// don't span any relative-width columns
if (adjustCells && spannedDefaultWidthCellCount > 0) {
int componentPreferredWidth = component.getPreferredWidth(-1);
if (componentPreferredWidth > spannedWidth) {
// The component's preferred width is larger
// than the width we've allocated thus far, so
// an adjustment is necessary
int adjustment = componentPreferredWidth - spannedWidth;
// We'll distribute the adjustment evenly
// among the default-width columns
for (int k = 0; k < columnSpan && j + k < columnCount; k++) {
if (defaultWidthColumns[j + k]) {
int columnAdjustment = adjustment
/ spannedDefaultWidthCellCount;
columnWidthsLocal[j + k] += columnAdjustment;
reservedWidth += columnAdjustment;
// Adjust these to avoid rounding errors
adjustment -= columnAdjustment;
spannedDefaultWidthCellCount--;
}
}
}
}
}
}
}
}
// Finally, we allocate the widths of the relative columns by divvying
// up the remaining width
int remainingWidth = Math.max(width - reservedWidth, 0);
if (totalRelativeWeight > 0 && remainingWidth > 0) {
for (int j = 0; j < columnCount; j++) {
if (columnWidthsLocal[j] < 0) {
int relativeWeight = -columnWidthsLocal[j];
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
int columnWidth = (int) (remainingWidth * weightPercentage);
columnWidthsLocal[j] = columnWidth;
// NOTE we adjust remainingWidth and totalRelativeWeight as
// we go
// to avoid potential rounding errors in the columnWidth
// calculation
remainingWidth -= columnWidth;
totalRelativeWeight -= relativeWeight;
}
}
}
return columnWidthsLocal;
}
/**
* Gets the height of each row of a table pane given the specified
* constraints.
*
* @param height The height constraint of the table pane
* @param columnWidthsArgument The widths of the table pane's columns, which
* will be used as width constraints to the row heights when necessary, or
* <tt>null</tt> if the column widths are not yet known (the row heights
* will be unconstrained)
* @return An array containing the height of each row in the table pane given
* the specified constraints
*/
private int[] getRowHeights(int height, int[] columnWidthsArgument) {
if (columnWidthsArgument == null) {
throw new IllegalArgumentException("columnWidths is null");
}
TablePane tablePane = (TablePane) getComponent();
TablePane.RowSequence rows = tablePane.getRows();
int rowCount = rows.getLength();
int columnCount = tablePane.getColumns().getLength();
int rowHeightsLocal[] = new int[rowCount];
boolean[] defaultHeightRows = new boolean[rowCount];
int totalRelativeWeight = 0;
int visibleRowCount = 0;
int reservedHeight = padding.top + padding.bottom;
// First, we allocate the heights of non-relative rows. We store the
// heights of relative rows as negative values for later processing
for (int i = 0; i < rowCount; i++) {
if (isRowVisible(i)) {
TablePane.Row row = rows.get(i);
int rowHeight = row.getHeight();
if (row.isRelative()) {
rowHeightsLocal[i] = -rowHeight;
totalRelativeWeight += rowHeight;
} else {
if (rowHeight < 0) {
// Default height row; we must calculate the height
rowHeight = getPreferredRowHeight(i, columnWidthsArgument);
defaultHeightRows[i] = true;
}
rowHeightsLocal[i] = rowHeight;
reservedHeight += rowHeight;
}
visibleRowCount++;
} else {
rowHeightsLocal[i] = 0;
}
}
if (visibleRowCount > 1) {
reservedHeight += (visibleRowCount - 1) * verticalSpacing;
}
// Next, we we account for default-width columns containing spanning
// cells, which have been ignored thus far. We ensure that the sum of
// the widths of the spanned cells is enough to satisfy the preferred
// width of the spanning content.
for (int i = 0; i < rowCount; i++) {
TablePane.Row row = rows.get(i);
for (int j = 0, n = row.getLength(); j < n && j < columnCount; j++) {
Component component = row.get(j);
if (component != null && component.isVisible()) {
int rowSpan = TablePane.getRowSpan(component);
if (rowSpan > 1) {
// We might need to adjust row heights to accommodate
// this spanning cell. First, we find out if any of the
// spanned cells are default height and how much space
// we've allocated thus far for those cells
boolean adjustCells = true;
int spannedDefaultHeightCellCount = 0;
int spannedHeight = 0;
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
if (rowHeightsLocal[i + k] < 0) {
adjustCells = false;
break;
}
if (defaultHeightRows[i + k]) {
spannedDefaultHeightCellCount++;
}
spannedHeight += rowHeightsLocal[i + k];
}
// If we span any relative-height rows, we assume
// that we'll achieve the desired spanning height when
// we divvy up the remaining space, so there's no need
// to make an adjustment here. This assumption is safe
// because our preferred height policy is to *either*
// divide the adjustment among the relative-height
// rows *or* among the default-height rows if we
// don't span any relative-height rows
if (adjustCells && spannedDefaultHeightCellCount > 0) {
int componentPreferredHeight = component.getPreferredHeight(columnWidthsArgument[j]);
if (componentPreferredHeight > spannedHeight) {
// The component's preferred height is larger
// than the height we've allocated thus far, so
// an adjustment is necessary
int adjustment = componentPreferredHeight - spannedHeight;
// We'll distribute the adjustment evenly
// among the default-height rows
for (int k = 0; k < rowSpan && i + k < rowCount; k++) {
if (defaultHeightRows[i + k]) {
int rowAdjustment = adjustment
/ spannedDefaultHeightCellCount;
rowHeightsLocal[i + k] += rowAdjustment;
reservedHeight += rowAdjustment;
// Adjust these to avoid rounding errors
adjustment -= rowAdjustment;
spannedDefaultHeightCellCount--;
}
}
}
}
}
}
}
}
// Finally, we allocate the heights of the relative rows by divvying
// up the remaining height
int remainingHeight = height - reservedHeight;
if (totalRelativeWeight > 0 && remainingHeight > 0) {
for (int i = 0; i < rowCount; i++) {
if (rowHeightsLocal[i] < 0) {
int relativeWeight = -rowHeightsLocal[i];
float weightPercentage = relativeWeight / (float) totalRelativeWeight;
int rowHeight = (int) (remainingHeight * weightPercentage);
rowHeightsLocal[i] = rowHeight;
// NOTE we adjust remainingHeight and totalRelativeWeight as
// we
// go to avoid potential rounding errors in the rowHeight
// calculation
remainingHeight -= rowHeight;
totalRelativeWeight -= relativeWeight;
}
}
}
// If we have don't actually have enough height available
boolean progress = true; // prevent infinite loop
while (remainingHeight < 0 && progress) {
progress = false;
for (int i = 0; i < rowCount; i++) {
if (isRowVisible(i)) {
TablePane.Row row = rows.get(i);
if (!row.isRelative()) {
if (rowHeightsLocal[i] > 0) {
rowHeightsLocal[i]--;
remainingHeight++;
progress = true;
if (remainingHeight >= 0) {
break;
}
}
}
}
}
}
return rowHeightsLocal;
}
// TablePane.Skin methods
@Override
public int getRowAt(int y) {
if (rowHeights == null) {
return -1;
}
int rowIndex = -1;
for (int i = 0, rowY = padding.top; rowY <= y && i < rowHeights.length; i++) {
int rowHeight = rowHeights[i];
if (y < rowY + rowHeight) {
rowIndex = i;
break;
}
rowY += rowHeight + verticalSpacing;
}
return rowIndex;
}
@Override
public Bounds getRowBounds(int row) {
if (rowHeights == null) {
return new Bounds(0, 0, 0, 0);
}
if (row < 0 || row >= rowHeights.length) {
throw new IndexOutOfBoundsException(String.valueOf(row));
}
int rowY = padding.top;
for (int i = 0; i < row; i++) {
rowY += rowHeights[i] + verticalSpacing;
}
return new Bounds(0, rowY, getWidth(), rowHeights[row]);
}
@Override
public int getColumnAt(int x) {
if (columnWidths == null) {
return -1;
}
int columnIndex = -1;
for (int j = 0, columnX = padding.left; columnX <= x && j < columnWidths.length; j++) {
int columnWidth = columnWidths[j];
if (x < columnX + columnWidth) {
columnIndex = j;
break;
}
columnX += columnWidth + horizontalSpacing;
}
return columnIndex;
}
@Override
public Bounds getColumnBounds(int column) {
if (columnWidths == null) {
return new Bounds(0, 0, 0, 0);
}
if (column < 0 || column >= columnWidths.length) {
throw new IndexOutOfBoundsException(String.valueOf(column));
}
int columnX = padding.left;
for (int j = 0; j < column; j++) {
columnX += columnWidths[j] + horizontalSpacing;
}
return new Bounds(columnX, 0, columnWidths[column], getHeight());
}
// TablePaneListener methods
@Override
public void rowInserted(TablePane tablePane, int index) {
invalidateComponent();
}
@Override
public void rowsRemoved(TablePane tablePane, int index, Sequence<TablePane.Row> rows) {
invalidateComponent();
}
@Override
public void rowHeightChanged(TablePane.Row row, int previousHeight, boolean previousRelative) {
invalidateComponent();
}
@Override
public void rowHighlightedChanged(TablePane.Row row) {
TablePane tablePane = row.getTablePane();
repaintComponent(getRowBounds(tablePane.getRows().indexOf(row)));
}
@Override
public void columnInserted(TablePane tablePane, int index) {
invalidateComponent();
}
@Override
public void columnsRemoved(TablePane tablePane, int index, Sequence<TablePane.Column> columns) {
invalidateComponent();
}
@Override
public void columnWidthChanged(TablePane.Column column, int previousWidth,
boolean previousRelative) {
invalidateComponent();
}
@Override
public void columnHighlightedChanged(TablePane.Column column) {
TablePane tablePane = column.getTablePane();
repaintComponent(getColumnBounds(tablePane.getColumns().indexOf(column)));
}
@Override
public void cellInserted(TablePane.Row row, int column) {
invalidateComponent();
}
@Override
public void cellsRemoved(TablePane.Row row, int column, Sequence<Component> removed) {
invalidateComponent();
}
@Override
public void cellUpdated(TablePane.Row row, int column, Component previousComponent) {
invalidateComponent();
}
// TablePaneAttribute events
@Override
public void rowSpanChanged(TablePane tablePane, Component component, int previousRowSpan) {
invalidateComponent();
}
@Override
public void columnSpanChanged(TablePane tablePane, Component component, int previousColumnSpan) {
invalidateComponent();
}
}