/*
* Copyright 2008 Google Inc.
*
* Licensed 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 com.google.gwt.gen2.table.client;
import com.google.gwt.gen2.table.client.FixedWidthTableImpl.IdealColumnWidthInfo;
import com.google.gwt.gen2.table.override.client.FlexTable;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Widget;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A variation of the {@link FlexTable} that supports smarter column resizing
* options. Unlike the {@link FlexTable}, columns resized in the
* {@link FixedWidthFlexTable} class are guaranteed to be resized correctly.
*/
public class FixedWidthFlexTable extends FlexTable {
/**
* FlexTable-specific implementation of
* {@link com.google.gwt.user.client.ui.HTMLTable.CellFormatter}.
*/
public class FixedWidthFlexCellFormatter extends FlexCellFormatter {
/**
* Sets the column span for the given cell. This is the number of logical
* columns covered by the cell.
*
* @param row the cell's row
* @param column the cell's column
* @param colSpan the cell's column span
* @throws IndexOutOfBoundsException
*/
@Override
public void setColSpan(int row, int column, int colSpan) {
colSpan = Math.max(1, colSpan);
int colSpanDelta = colSpan - getColSpan(row, column);
super.setColSpan(row, column, colSpan);
// Update the column counts
int rowSpan = getRowSpan(row, column);
for (int i = row; i < row + rowSpan; i++) {
setNumColumnsPerRow(i, getNumColumnsPerRow(i) + colSpanDelta);
}
}
/**
* Sets the row span for the given cell. This is the number of logical rows
* covered by the cell.
*
* @param row the cell's row
* @param column the cell's column
* @param rowSpan the cell's row span
* @throws IndexOutOfBoundsException
*/
@Override
public void setRowSpan(int row, int column, int rowSpan) {
rowSpan = Math.max(1, rowSpan);
int curRowSpan = getRowSpan(row, column);
super.setRowSpan(row, column, rowSpan);
// Update the column counts
int colSpan = getColSpan(row, column);
if (rowSpan > curRowSpan) {
for (int i = row + curRowSpan; i < row + rowSpan; i++) {
setNumColumnsPerRow(i, getNumColumnsPerRow(i) + colSpan);
}
} else if (rowSpan < curRowSpan) {
for (int i = row + rowSpan; i < row + curRowSpan; i++) {
setNumColumnsPerRow(i, getNumColumnsPerRow(i) - colSpan);
}
}
}
/**
* UnsupportedOperation. Use ExtendedGrid.setColumnWidth(column, width)
* instead.
*
* @param row the row of the cell whose width is to be set
* @param column the cell whose width is to be set
* @param width the cell's new width, in CSS units
* @throws UnsupportedOperationException
*/
@Override
public void setWidth(int row, int column, String width) {
throw new UnsupportedOperationException("setWidth is not supported. "
+ "Use ExtendedGrid.setColumnWidth(int, int) instead.");
}
/**
* Gets the TD element representing the specified cell unsafely (meaning
* that it doesn't ensure that the row and column are valid).
*
* @param row the row of the cell to be retrieved
* @param column the column of the cell to be retrieved
* @return the column's TD element
*/
@Override
protected Element getRawElement(int row, int column) {
return super.getRawElement(row + 1, column);
}
}
/**
* This class contains methods used to format a table's columns. It is limited
* by the support cross-browser HTML support for column formatting.
*/
public class FixedWidthFlexColumnFormatter extends ColumnFormatter {
/**
* UnsupportedOperation. Use ExtendedGrid.setColumnWidth(column, width)
* instead.
*
* @param column the column of the cell whose width is to be set
* @param width the cell's new width, in percentage or pixel units
* @throws UnsupportedOperationException
*/
@Override
public void setWidth(int column, String width) {
throw new UnsupportedOperationException("setWidth is not supported. "
+ "Use ExtendedGrid.setColumnWidth(int, int) instead.");
}
}
/**
* This class contains methods used to format a table's rows.
*/
public class FixedWidthFlexRowFormatter extends RowFormatter {
/**
* Unsafe method to get a row element.
*
* @param row the row to get
* @return the row element
*/
@Override
protected Element getRawElement(int row) {
return super.getRawElement(row + 1);
}
}
/**
* The default width of a column in pixels.
*/
public static final int DEFAULT_COLUMN_WIDTH = 80;
/**
* A mapping of column indexes to their widths in pixels.
*
* key = column index
*
* value = column width in pixels
*/
private Map<Integer, Integer> colWidths = new HashMap<Integer, Integer>();
/**
* A mapping of rows to the number of raw columns (not cells) that they
* contain.
*
* key = the row index
*
* value = the number of raw columns in the row
*/
private List<Integer> columnsPerRow = new ArrayList<Integer>();
/**
* A mapping of raw columns counts to the number of rows with that column
* count. For example, if three rows contain a raw column count of five, one
* of the entries will be (5, 3);
*
* key = number of raw columns
*
* value = number of rows with that many raw columns
*/
private Map<Integer, Integer> columnCountMap = new HashMap<Integer, Integer>();
/**
* The maximum number of raw columns in any single row.
*/
private int maxRawColumnCount = 0;
/**
* The hidden, zero row used for sizing the columns.
*/
private Element ghostRow;
/**
* The ideal widths of all columns (that are available).
*/
private int[] idealWidths;
/**
* Info used to calculate ideal column width.
*/
private IdealColumnWidthInfo idealColumnWidthInfo;
/**
* Constructor.
*/
public FixedWidthFlexTable() {
super();
Element tableElem = getElement();
DOM.setStyleAttribute(tableElem, "tableLayout", "fixed");
DOM.setStyleAttribute(tableElem, "width", "0px");
setCellFormatter(new FixedWidthFlexCellFormatter());
setColumnFormatter(new FixedWidthFlexColumnFormatter());
setRowFormatter(new FixedWidthFlexRowFormatter());
// Create the zero row for sizing
ghostRow = FixedWidthTableImpl.get().createGhostRow();
DOM.insertChild(getBodyElement(), ghostRow, 0);
}
@Override
public void clear() {
super.clear();
clearIdealWidths();
}
/**
* @return the raw number of columns in this table.
*/
public int getColumnCount() {
return maxRawColumnCount;
}
/**
* Return the column width for a given column index. If a width has not been
* assigned, the default width is returned.
*
* @param column the column index
* @return the column width in pixels
*/
public int getColumnWidth(int column) {
Object colWidth = colWidths.get(new Integer(column));
if (colWidth == null) {
return DEFAULT_COLUMN_WIDTH;
} else {
return ((Integer) colWidth).intValue();
}
}
/**
* <p>
* Calculate the ideal width required to tightly wrap the specified column. If
* the ideal column width cannot be calculated (eg. if the table is not
* attached), -1 is returned.
* </p>
* <p>
* Note that this method requires an expensive operation whenever the content
* of the table is changed, so you should only call it after you've completely
* modified the contents of your table.
* </p>
*
* @return the ideal column width, or -1 if it is not applicable
*/
public int getIdealColumnWidth(int column) {
maybeRecalculateIdealColumnWidths();
if (idealWidths.length > column) {
return idealWidths[column];
}
return -1;
}
/**
* @see FlexTable
*/
@Override
public Element insertCell(int beforeRow, int beforeColumn) {
clearIdealWidths();
Element td = super.insertCell(beforeRow, beforeColumn);
DOM.setStyleAttribute(td, "overflow", "hidden");
setNumColumnsPerRow(beforeRow, getNumColumnsPerRow(beforeRow) + 1);
return td;
}
/**
* @see FlexTable
*/
@Override
public int insertRow(int beforeRow) {
// Get the affected colSpan, which is the number of raw cells created by
// row spanning cells in rows above the new row.
FlexCellFormatter formatter = getFlexCellFormatter();
int affectedColSpan = getNumColumnsPerRow(beforeRow);
if (beforeRow != getRowCount()) {
int numCellsInRow = getCellCount(beforeRow);
for (int cell = 0; cell < numCellsInRow; cell++) {
affectedColSpan -= formatter.getColSpan(beforeRow, cell);
}
}
// Specifically allow the row count as an insert position.
if (beforeRow != getRowCount()) {
checkRowBounds(beforeRow);
}
Element tr = DOM.createTR();
DOM.insertChild(getBodyElement(), tr, beforeRow + 1);
columnsPerRow.add(beforeRow, new Integer(0));
// Make sure the column counts are still correct by iterating backward
// through the rows looking for cells that span down into the newly
// inserted row. Stop iterating when every raw column is accounted for.
for (int curRow = beforeRow - 1; curRow >= 0; curRow--) {
// No more affect of rowspan, so we are done
if (affectedColSpan <= 0) {
break;
}
// Look for cells that span into the new row
int numCells = getCellCount(curRow);
for (int curCell = 0; curCell < numCells; curCell++) {
int affectedRow = curRow + formatter.getRowSpan(curRow, curCell);
if (affectedRow > beforeRow) {
int colSpan = formatter.getColSpan(curRow, curCell);
affectedColSpan -= colSpan;
setNumColumnsPerRow(beforeRow, getNumColumnsPerRow(beforeRow)
+ colSpan);
setNumColumnsPerRow(affectedRow, getNumColumnsPerRow(affectedRow)
- colSpan);
}
}
}
// Return the new row index
return beforeRow;
}
@Override
public boolean remove(Widget widget) {
if (super.remove(widget)) {
clearIdealWidths();
return true;
}
return false;
}
/**
* @see FlexTable
*/
@Override
public void removeCell(int row, int column) {
clearIdealWidths();
int colSpan = getFlexCellFormatter().getColSpan(row, column);
int rowSpan = getFlexCellFormatter().getRowSpan(row, column);
super.removeCell(row, column);
// Update the column counts
for (int i = row; i < row + rowSpan; i++) {
setNumColumnsPerRow(i, getNumColumnsPerRow(i) - colSpan);
}
}
/**
* @see FlexTable
*/
@Override
public void removeRow(int row) {
// Set the rowspan of everything in this row to 1
FlexCellFormatter formatter = getFlexCellFormatter();
int affectedColSpan = getNumColumnsPerRow(row);
int numCellsInRow = getCellCount(row);
for (int cell = 0; cell < numCellsInRow; cell++) {
formatter.setRowSpan(row, cell, 1);
affectedColSpan -= formatter.getColSpan(row, cell);
}
// Actually remove the row
super.removeRow(row);
clearIdealWidths();
setNumColumnsPerRow(row, -1);
columnsPerRow.remove(row);
// Make sure the column counts are still correct by iterating backward
// through the rows looking for cells that span down into the newly
// inserted row. Stop iterating when every raw column is accounted for.
for (int curRow = row - 1; curRow >= 0; curRow--) {
// No more affects from rowspan, so we are done
if (affectedColSpan <= 0) {
break;
}
// Look for cells that span into the removed row
int numCells = getCellCount(curRow);
for (int curCell = 0; curCell < numCells; curCell++) {
int affectedRow = curRow + formatter.getRowSpan(curRow, curCell) - 1;
if (affectedRow >= row) {
int colSpan = formatter.getColSpan(curRow, curCell);
affectedColSpan -= colSpan;
setNumColumnsPerRow(affectedRow, getNumColumnsPerRow(affectedRow)
+ colSpan);
}
}
}
}
@Override
public void setCellPadding(int padding) {
super.setCellPadding(padding);
// Reset the width of all columns
for (Map.Entry<Integer, Integer> entry : colWidths.entrySet()) {
setColumnWidth(entry.getKey(), entry.getValue());
}
}
@Override
public void setCellSpacing(int spacing) {
super.setCellSpacing(spacing);
// Reset the width of all columns
for (Map.Entry<Integer, Integer> entry : colWidths.entrySet()) {
setColumnWidth(entry.getKey(), entry.getValue());
}
}
/**
* Set the width of a column.
*
* @param column the index of the column
* @param width the width in pixels
* @throws IndexOutOfBoundsException
*/
public void setColumnWidth(int column, int width) {
// Ensure that the indices are not negative.
if (column < 0) {
throw new IndexOutOfBoundsException(
"Cannot access a column with a negative index: " + column);
}
// Add the width to the map
width = Math.max(1, width);
colWidths.put(new Integer(column), new Integer(width));
// Update the cell width if possible
int numGhosts = getGhostColumnCount();
if (column >= numGhosts) {
return;
}
// Set the actual column width
FixedWidthTableImpl.get().setColumnWidth(this, ghostRow, column, width);
}
@Override
public void setHTML(int row, int column, String html) {
super.setHTML(row, column, html);
clearIdealWidths();
}
@Override
public void setText(int row, int column, String text) {
super.setText(row, column, text);
clearIdealWidths();
}
@Override
public void setWidget(int row, int column, Widget widget) {
super.setWidget(row, column, widget);
clearIdealWidths();
}
/**
* @see FlexTable
*/
@Override
protected void addCells(int row, int num) {
// Account for ghost row
super.addCells(row + 1, num);
clearIdealWidths();
}
@Override
protected int getDOMCellCount(int row) {
// Account for ghost row
return super.getDOMCellCount(row + 1);
}
@Override
protected int getDOMRowCount() {
// Account for ghost row
return super.getDOMRowCount() - 1;
}
/**
* Returns the current number of ghost columns in existence.
*
* @return the number of ghost columns
*/
protected int getGhostColumnCount() {
return super.getDOMCellCount(0);
}
/**
* @return the ghost row element
*/
protected Element getGhostRow() {
return ghostRow;
}
@Override
protected int getRowIndex(Element rowElem) {
int rowIndex = super.getRowIndex(rowElem);
if (rowIndex < 0) {
return rowIndex;
}
return rowIndex - 1;
}
@Override
protected boolean internalClearCell(Element td, boolean clearInnerHTML) {
clearIdealWidths();
return super.internalClearCell(td, clearInnerHTML);
}
@Override
protected void onAttach() {
super.onAttach();
clearIdealWidths();
}
@Override
protected void prepareCell(int row, int column) {
int curNumCells = 0;
if (getRowCount() > row) {
curNumCells = getCellCount(row);
}
super.prepareCell(row, column);
// Add ghost columns as needed
if (column >= curNumCells) {
// Add ghost cells
int cellsAdded = column - curNumCells + 1;
setNumColumnsPerRow(row, getNumColumnsPerRow(row) + cellsAdded);
// Set the new cells to hide overflow
for (int cell = curNumCells; cell < column; cell++) {
Element td = getCellFormatter().getElement(row, cell);
DOM.setStyleAttribute(td, "overflow", "hidden");
}
}
}
/**
* Recalculate the ideal column widths of each column in the data table.
*/
protected void recalculateIdealColumnWidths() {
// We need at least one cell to do any calculations
int columnCount = getColumnCount();
if (!isAttached() || getRowCount() == 0 || columnCount < 1) {
idealWidths = new int[0];
return;
}
recalculateIdealColumnWidthsSetup();
recalculateIdealColumnWidthsImpl();
recalculateIdealColumnWidthsTeardown();
}
/**
* Clear the idealWidths field when the ideal widths change.
*/
void clearIdealWidths() {
idealWidths = null;
}
/**
* @return true if the ideal column widths have already been calculated
*/
boolean isIdealColumnWidthsCalculated() {
return idealWidths != null;
}
/**
* Recalculate the ideal column widths of each column in the data table. This
* method assumes that the tableLayout has already been changed.
*/
void recalculateIdealColumnWidthsImpl() {
idealWidths = FixedWidthTableImpl.get().recalculateIdealColumnWidths(
idealColumnWidthInfo);
}
/**
* Setup to recalculate column widths.
*/
void recalculateIdealColumnWidthsSetup() {
idealColumnWidthInfo = FixedWidthTableImpl.get().recalculateIdealColumnWidthsSetup(
this, getColumnCount(), 0);
}
/**
* Tear down after recalculating column widths.
*/
void recalculateIdealColumnWidthsTeardown() {
FixedWidthTableImpl.get().recalculateIdealColumnWidthsTeardown(
idealColumnWidthInfo);
idealColumnWidthInfo = null;
}
/**
* Get the number of columns in a row.
*
* @return the number of columns
*/
private int getNumColumnsPerRow(int row) {
if (columnsPerRow.size() <= row) {
return 0;
} else {
return columnsPerRow.get(row).intValue();
}
}
/**
* Recalculate the ideal column widths of each column in the data table if
* they have changed since the last calculation.
*/
private void maybeRecalculateIdealColumnWidths() {
if (idealWidths == null) {
recalculateIdealColumnWidths();
}
}
/**
* Set the number of columns in a row. A negative value in numColumns means
* the row should be removed.
*
* @param row the row index
* @param numColumns the number of columns in the row
*/
private void setNumColumnsPerRow(int row, int numColumns) {
// Get the old number of raw columns in the row
int oldNumColumns = getNumColumnsPerRow(row);
if (oldNumColumns == numColumns) {
return;
}
// Update the list of columns per row
Integer numColumnsI = new Integer(numColumns);
Integer oldNumColumnsI = new Integer(oldNumColumns);
if (row < columnsPerRow.size()) {
columnsPerRow.set(row, numColumnsI);
} else {
columnsPerRow.add(numColumnsI);
}
// Decrement the old number of columns
boolean oldNumColumnsRemoved = false;
if (columnCountMap.containsKey(oldNumColumnsI)) {
int numRows = columnCountMap.get(oldNumColumnsI).intValue();
if (numRows == 1) {
columnCountMap.remove(oldNumColumnsI);
oldNumColumnsRemoved = true;
} else {
columnCountMap.put(oldNumColumnsI, new Integer(numRows - 1));
}
}
// Increment the new number of columns
if (numColumns > 0) {
if (columnCountMap.containsKey(numColumnsI)) {
int numRows = columnCountMap.get(numColumnsI).intValue();
columnCountMap.put(numColumnsI, new Integer(numRows + 1));
} else {
columnCountMap.put(numColumnsI, new Integer(1));
}
}
// Update the maximum number of rows
if (numColumns > maxRawColumnCount) {
maxRawColumnCount = numColumns;
} else if ((numColumns < oldNumColumns)
&& (oldNumColumns == maxRawColumnCount) && oldNumColumnsRemoved) {
// Column count decreased from max count
maxRawColumnCount = 0;
for (Integer curNumColumns : columnCountMap.keySet()) {
maxRawColumnCount = Math.max(maxRawColumnCount,
curNumColumns.intValue());
}
}
// Update the ghost row
updateGhostRow();
}
/**
* Add or remove ghost cells when the table size changes.
*/
private void updateGhostRow() {
int curNumGhosts = getGhostColumnCount();
if (maxRawColumnCount > curNumGhosts) {
// Add ghosts as needed
super.addCells(0, maxRawColumnCount - curNumGhosts);
for (int i = curNumGhosts; i < maxRawColumnCount; i++) {
Element td = FixedWidthTableImpl.get().getGhostCell(ghostRow, i);
FixedWidthTableImpl.get().createGhostCell(td);
setColumnWidth(i, getColumnWidth(i));
}
} else if (maxRawColumnCount < curNumGhosts) {
// Remove ghost rows as needed
int cellsToRemove = curNumGhosts - maxRawColumnCount;
for (int i = 0; i < cellsToRemove; i++) {
DOM.removeChild(ghostRow, FixedWidthTableImpl.get().getGhostCell(
ghostRow, maxRawColumnCount));
}
}
}
}