com.google.gwt.gen2.table.client.FixedWidthFlexTable.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.gen2.table.client.FixedWidthFlexTable.java

Source

/*
 * 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));
            }
        }
    }
}