Package org.gwt.mosaic.ui.client.table

Source Code of org.gwt.mosaic.ui.client.table.AbstractScrollTable$TableWidthInfo

/*
* Copyright (c) 2008-2010 GWT Mosaic Georgios J. Georgopoulos
*
* 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.
*/
/*
* This is derived work from GWT Incubator project:
* http://code.google.com/p/google-web-toolkit-incubator/
*
* 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 org.gwt.mosaic.ui.client.table;

import java.util.ArrayList;
import java.util.List;

import org.gwt.mosaic.core.client.DOM;
import org.gwt.mosaic.core.client.Dimension;
import org.gwt.mosaic.override.client.OverrideDOM;
import org.gwt.mosaic.override.client.HTMLTable.CellFormatter;
import org.gwt.mosaic.ui.client.event.ColumnSortEvent;
import org.gwt.mosaic.ui.client.event.ColumnSortHandler;
import org.gwt.mosaic.ui.client.layout.HasLayoutManager;
import org.gwt.mosaic.ui.client.table.ColumnResizer.ColumnWidthInfo;
import org.gwt.mosaic.ui.client.table.TableModelHelper.ColumnSortList;
import org.gwt.mosaic.ui.client.table.property.MaximumWidthProperty;
import org.gwt.mosaic.ui.client.util.WidgetHelper;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.HasScrollHandlers;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.i18n.client.Messages;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

/**
* <p>
* A ScrollTable consists of a fixed header and footer (optional) that remain
* visible and a scrollable body that contains the data.
* </p>
*
* <p>
* In order for the columns in the header table and data table to line up, the
* two table must have the same margin, padding, and border widths. You can use
* CSS style sheets to manipulate the colors and styles of the cell's, but you
* must keep the actual sizes consistent (especially with respect to the left
* and right side of the cells).
* </p>
*
* <p>
* NOTE: AbstractScrollTable does not resize correctly in older versions of
* Mozilla (specifically, Linux hosted mode). In use, the PagingScrollTable will
* expand horizontally, but it will not contract when you reduce the screen
* size. However, the AbstractScrollTable resizes naturally (you can set width
* in percentages) on all modern browsers including IE6+, FF2+, Safari2+,
* Chrome, Opera 9.6.
* </p>
*
* <h3>CSS Style Rules</h3>
*
* <dl>
* <dt>.gwt-ScrollTable</dt>
* <dd>applied to the entire widget</dd>
* <dt>.gwt-ScrollTable .headerTable</dt>
* <dd>applied to the header table</dd>
* <dt>.gwt-ScrollTable .dataTable</dt>
* <dd>applied to the data table</dd>
* <dt>.gwt-ScrollTable .footerTable</dt>
* <dd>applied to the footer table</dd>
* <dt>.gwt-ScrollTable .headerWrapper</dt>
* <dd>wrapper around the header table</dd>
* <dt>.gwt-ScrollTable .dataWrapper</dt>
* <dd>wrapper around the data table</dd>
* <dt>.gwt-ScrollTable .footerWrapper</dt>
* <dd>wrapper around the footer table</dd>
* </dl>
*
* @author Derived work from GWT Incubator project
* @author georgopoulos.georgios(at)gmail.com
*
*/
public abstract class AbstractScrollTable extends ComplexPanel implements
    HasLayoutManager, HasScrollHandlers {
  /**
   * Browser specific implementation class for {@link AbstractScrollTable}.
   */
  private static class Impl {
    /**
     * Create a spacer element that allows the header table to scroll over the
     * vertical scroll bar of the data table.
     *
     * @param wrapper the wrapper that contains the header table
     * @return the spacer element
     */
    public Element createSpacer(FixedWidthFlexTable table, Element wrapper) {
      resizeSpacer(table, null, 15);
      return null;
    }

    /**
     * Returns the width of a table, minus any padding, in pixels.
     *
     * @param table the table
     * @param includeSpacer true to include the spacer width
     * @return the width
     */
    public int getTableWidth(FixedWidthFlexTable table, boolean includeSpacer) {
      int scrollWidth = table.getElement().getScrollWidth();
      if (!includeSpacer) {
        int spacerWidth = getSpacerWidth(table);
        if (spacerWidth > 0) {
          scrollWidth -= spacerWidth;
        }
      }
      return scrollWidth;
    }

    /**
     * Recalculate the ideal widths of columns.
     *
     * @param scrollTable the scroll table
     * @param command an optional command to execute while recalculating
     */
    public void recalculateIdealColumnWidths(AbstractScrollTable scrollTable,
        Command command) {
      FixedWidthFlexTable headerTable = scrollTable.getHeaderTable();
      FixedWidthFlexTable footerTable = scrollTable.getFooterTable();
      FixedWidthGrid dataTable = scrollTable.getDataTable();

      // Setup all inner tables
      dataTable.recalculateIdealColumnWidthsSetup();
      headerTable.recalculateIdealColumnWidthsSetup();
      if (footerTable != null) {
        footerTable.recalculateIdealColumnWidthsSetup();
      }

      // Perform operations
      dataTable.recalculateIdealColumnWidthsImpl();
      headerTable.recalculateIdealColumnWidthsImpl();
      if (footerTable != null) {
        footerTable.recalculateIdealColumnWidthsImpl();
      }

      // Execute the optional command
      if (command != null) {
        command.execute();
      }

      // Teardown all inner tables
      dataTable.recalculateIdealColumnWidthsTeardown();
      headerTable.recalculateIdealColumnWidthsTeardown();
      if (footerTable != null) {
        footerTable.recalculateIdealColumnWidthsTeardown();
      }
    }

    /**
     * Reposition the header spacer as needed.
     *
     * @param scrollTable the scroll table
     * @param force if true, ignore the scroll policy
     */
    public void repositionSpacer(AbstractScrollTable scrollTable, boolean force) {
      // Only ScrollPolicy.BOTH has a vertical scroll bar
      if (!force && scrollTable.scrollPolicy != ScrollPolicy.BOTH) {
        return;
      }

      Element dataWrapper = scrollTable.dataWrapper;
      int spacerWidth = dataWrapper.getOffsetWidth()
          - dataWrapper.getPropertyInt("clientWidth");
      resizeSpacer(scrollTable.headerTable, scrollTable.headerSpacer,
          spacerWidth);
      if (scrollTable.footerTable != null) {
        resizeSpacer(scrollTable.footerTable, scrollTable.footerSpacer,
            spacerWidth);
      }
    }

    /**
     * @return true if the scroll bar is on the right
     */
    boolean isScrollBarOnRight() {
      return true;
    }

    void resizeSpacer(FixedWidthFlexTable table, Element spacer, int spacerWidth) {
      // Exit early if the spacer is already the correct size
      if (spacerWidth == getSpacerWidth(table)) {
        return;
      }

      if (isScrollBarOnRight()) {
        table.getElement().getStyle().setPropertyPx("paddingRight", spacerWidth);
      } else {
        table.getElement().getStyle().setPropertyPx("paddingLeft", spacerWidth);
      }
    }

    /**
     * Get the current width of the spacer element.
     *
     * @param table the table to check
     * @return the current width
     */
    private int getSpacerWidth(FixedWidthFlexTable table) {
      // Get the padding string
      String paddingStr;
      if (isScrollBarOnRight()) {
        paddingStr = table.getElement().getStyle().getProperty("paddingRight");
      } else {
        paddingStr = table.getElement().getStyle().getProperty("paddingLeft");
      }

      // Check the padding string
      if (paddingStr == null || paddingStr.length() < 3) {
        return -1;
      }

      // Parse the int from the padding
      try {
        return Integer.parseInt(paddingStr.substring(0, paddingStr.length() - 2));
      } catch (NumberFormatException e) {
        return -1;
      }
    }
  }

  /**
   * Opera and Old Mozilla put the scroll bar on the left side in RTL mode.
   */
  private static class ImplLeftScrollBar extends Impl {
    @Override
    boolean isScrollBarOnRight() {
      return !LocaleInfo.getCurrentLocale().isRTL();
    }
  }

  /**
   * IE puts the scroll bar on the left side in RTL mode. The padding trick
   * doesn't work, so we use a separate element.
   */
  @SuppressWarnings("unused")
  private static class ImplIE6 extends ImplLeftScrollBar {
    /**
     * Adding padding to a table in IE will mess up the layout, so we use an
     * absolutely positioned div to add padding. In RTL mode, the div needs to
     * be exactly the right width and position or scrollLeft will be affected.
     * In LTR mode, we can position it anywhere and set the width to a high
     * number, improving performance.
     */
    @Override
    public Element createSpacer(FixedWidthFlexTable table, Element wrapper) {
      Element spacer = DOM.createDiv();
      spacer.getStyle().setPropertyPx("height", 1);
      spacer.getStyle().setPropertyPx("top", 1);
      spacer.getStyle().setProperty("position", "absolute");
      if (!LocaleInfo.getCurrentLocale().isRTL()) {
        spacer.getStyle().setPropertyPx("left", 1);
        spacer.getStyle().setPropertyPx("width", 10000);
      }
      wrapper.appendChild(spacer);
      return spacer;
    }

    @Override
    public int getTableWidth(FixedWidthFlexTable table, boolean includeSpacer) {
      return table.getElement().getScrollWidth();
    }

    /**
     * IE allows the table to resize as widely as needed unless we restrict the
     * width of a parent element.
     */
    @Override
    public void recalculateIdealColumnWidths(AbstractScrollTable scrollTable,
        Command command) {
      scrollTable.getAbsoluteElement().getStyle().setPropertyPx("width", 1);
      super.recalculateIdealColumnWidths(scrollTable, command);
      scrollTable.getAbsoluteElement().getStyle().setProperty("width", "100%");
    }

    @Override
    void resizeSpacer(FixedWidthFlexTable table, Element spacer, int width) {
      if (LocaleInfo.getCurrentLocale().isRTL()) {
        int headerWidth = table.getOffsetWidth();
        spacer.getStyle().setPropertyPx("width", width);
        spacer.getStyle().setPropertyPx("right", headerWidth);
      }
    }
  }

  /**
   * A helper class that handles some of the mouse events associated with
   * resizing columns.
   */
  private static class MouseResizeWorker {
    /**
     * The width of the area that is available for resize.
     */
    private static final int RESIZE_CURSOR_WIDTH = 15;

    /**
     * The current header cell that the mouse is affecting.
     */
    private Element curCell = null;

    /**
     * The columns under the colSpan of the current cell.
     */
    private List<ColumnWidthInfo> curCells = new ArrayList<ColumnWidthInfo>();

    /**
     * The index of the current header cell.
     */
    private int curCellIndex = 0;

    /**
     * The current x position of the mouse.
     */
    private int mouseXCurrent = 0;

    /**
     * The last x position of the mouse when we resized.
     */
    private int mouseXLast = 0;

    /**
     * The starting x position of the mouse when resizing a column.
     */
    private int mouseXStart = 0;

    /**
     * A timer used to resize the columns. As long as the timer is active, it
     * will poll for the new row size and resize the columns.
     */
    private Timer resizeTimer = new Timer() {
      @Override
      public void run() {
        resizeColumn();
        schedule(100);
      }
    };

    /**
     * A boolean indicating that we are resizing the current header cell.
     */
    private boolean resizing = false;

    /**
     * The index of the first column that will be sacrificed.
     */
    private int sacrificeCellIndex = -1;

    /**
     * The cells that will be sacrificed so the current cells can be resized.
     */
    private List<ColumnWidthInfo> sacrificeCells = new ArrayList<ColumnWidthInfo>();

    /**
     * The table that this worker affects.
     */
    private AbstractScrollTable table = null;

    /**
     * @return the current cell
     */
    public Element getCurrentCell() {
      return curCell;
    }

    /**
     * @return true if a header is currently being resized
     */
    public boolean isResizing() {
      return resizing;
    }

    /**
     * Resize the column on a mouse event. This method also marks the client as
     * busy so we do not try to change the size repeatedly.
     *
     * @param event the mouse event
     */
    public void resizeColumn(Event event) {
      mouseXCurrent = DOM.eventGetClientX(event);
    }

    /**
     * Set the current cell that will be resized based on the mouse event.
     *
     * @param event the event that triggered the new cell
     * @return true if the cell was actually changed
     */
    public boolean setCurrentCell(Event event) {
      // Check the resize policy of the table
      Element cell = null;
      if (table.columnResizePolicy == ColumnResizePolicy.MULTI_CELL) {
        cell = table.headerTable.getEventTargetCell(event);
      } else if (table.columnResizePolicy == ColumnResizePolicy.SINGLE_CELL) {
        cell = table.headerTable.getEventTargetCell(event);
        if (cell != null && cell.getPropertyInt("colSpan") > 1) {
          cell = null;
        }
      }

      // See if we are near the edge of the cell
      int clientX = event.getClientX();
      if (cell != null) {
        int absLeft = cell.getAbsoluteLeft() - Window.getScrollLeft();
        if (LocaleInfo.getCurrentLocale().isRTL()) {
          if (clientX < absLeft || clientX > absLeft + RESIZE_CURSOR_WIDTH) {
            cell = null;
          }
        } else {
          int absRight = absLeft + cell.getOffsetWidth();
          if (clientX < absRight - RESIZE_CURSOR_WIDTH || clientX > absRight) {
            cell = null;
          }
        }
      }

      // Change out the current cell
      if (cell != curCell) {
        // Clear the old cell
        if (curCell != null) {
          curCell.getStyle().setProperty("cursor", "");
        }

        // Set the new cell
        curCell = cell;
        if (curCell != null) {
          // Check the cell index
          curCellIndex = getCellIndex(curCell);
          if (curCellIndex < 0) {
            curCell = null;
            return false;
          }

          // Check for resizable columns within one of the cells in the colspan
          boolean resizable = false;
          int colSpan = cell.getPropertyInt("colSpan");
          curCells = table.getColumnWidthInfo(curCellIndex, colSpan);
          for (ColumnWidthInfo info : curCells) {
            if (!info.hasMaximumWidth() || !info.hasMinimumWidth()
                || info.getMaximumWidth() != info.getMinimumWidth()) {
              resizable = true;
            }
          }
          if (!resizable) {
            curCell = null;
            curCells = null;
            return false;
          }

          // Update the cursor on the cell
          curCell.getStyle().setProperty("cursor", "e-resize");
        }
        return true;
      }

      // The cell did not change
      return false;
    }

    /**
     * Set the ScrollTable table that this worker affects.
     *
     * @param table the scroll table
     */
    public void setScrollTable(AbstractScrollTable table) {
      this.table = table;
    }

    /**
     * Start resizing the current cell when the user clicks on the right edge of
     * the cell.
     *
     * @param event the mouse event
     */
    public void startResizing(Event event) {
      if (curCell != null) {
        resizing = true;
        mouseXStart = event.getClientX();
        mouseXLast = mouseXStart;
        mouseXCurrent = mouseXStart;

        // Add the sacrifice cells
        int numColumns = table.getDataTable().getColumnCount();
        int colSpan = curCell.getPropertyInt("colSpan");
        sacrificeCellIndex = curCellIndex + colSpan;
        sacrificeCells = table.getColumnWidthInfo(sacrificeCellIndex,
            numColumns - sacrificeCellIndex);

        // Start the timer and listen for changes
        DOM.setCapture(table.headerWrapper);
        resizeTimer.schedule(20);
      }
    }

    /**
     * Stop resizing the current cell.
     *
     * @param event the mouse event
     */
    public void stopResizing(Event event) {
      if (curCell != null && resizing) {
        curCell.getStyle().setProperty("cursor", "");
        curCell = null;
        resizing = false;
        DOM.releaseCapture(table.headerWrapper);
        resizeTimer.cancel();
        resizeColumn();
        curCells = null;
        sacrificeCells = null;
        table.resizeTablesVertically();
      }
    }

    /**
     * Get the scroll table.
     *
     * @return the scroll table
     */
    protected AbstractScrollTable getScrollTable() {
      return table;
    }

    /**
     * Get the actual cell index of a cell in the header table.
     *
     * @param cell the cell element
     * @return the cell index
     */
    private int getCellIndex(Element cell) {
      int row = OverrideDOM.getRowIndex(DOM.getParent(cell)) - 1;
      int column = OverrideDOM.getCellIndex(cell);
      return table.headerTable.getColumnIndex(row, column)
          - table.getHeaderOffset();
    }

    /**
     * Helper method that actually sets the column size. This method is called
     * periodically by a timer.
     */
    private void resizeColumn() {
      if (mouseXLast != mouseXCurrent) {
        mouseXLast = mouseXCurrent;

        // Distribute to the cells being resized
        int totalDelta = mouseXCurrent - mouseXStart;
        if (LocaleInfo.getCurrentLocale().isRTL()) {
          totalDelta *= -1;
        }
        totalDelta -= table.columnResizer.distributeWidth(curCells, totalDelta);

        // Distribute to the sacrifice cells
        if (table.resizePolicy.isSacrificial()) {
          int remaining = table.columnResizer.distributeWidth(sacrificeCells,
              -totalDelta);

          // We don't have enough to sacrifice, redistribute the width
          if (remaining != 0 && table.resizePolicy.isFixedWidth()) {
            totalDelta += remaining;
            table.columnResizer.distributeWidth(curCells, totalDelta);
          }

          // Apply the widths to the sacrifice column
          table.applyNewColumnWidths(sacrificeCellIndex, sacrificeCells, true);
        }

        // Set the new widths
        table.applyNewColumnWidths(curCellIndex, curCells, true);

        // Scroll to table back into alignment
        table.scrollTables(false);
      }
    }
  }

  /**
   * The Opera version of the mouse worker fixes an Opera bug where the cursor
   * isn't updated if the mouse is hovering over an element DOM object when its
   * cursor style is changed.
   */
  @SuppressWarnings("unused")
  private static class MouseResizeWorkerOpera extends MouseResizeWorker {
    /**
     * A div used to force the cursor to update.
     */
    private Element cursorUpdateDiv;

    /**
     * Constructor.
     */
    public MouseResizeWorkerOpera() {
      cursorUpdateDiv = DOM.createDiv();
      DOM.setStyleAttribute(cursorUpdateDiv, "position", "absolute");
    }

    /**
     * Set the current cell that will be resized based on the mouse event.
     *
     * @param event the event that triggered the new cell
     * @return true if the cell was actually changed
     */
    @Override
    public boolean setCurrentCell(Event event) {
      // Check if cursor update div is active
      if (DOM.eventGetTarget(event) == cursorUpdateDiv) {
        removeCursorUpdateDiv();
        return false;
      }

      // Use the parent method
      boolean cellChanged = super.setCurrentCell(event);

      // Position a div that forces a cursor redraw in Opera
      if (cellChanged) {
        DOM.setCapture(getScrollTable().headerWrapper);
        DOM.setStyleAttribute(cursorUpdateDiv, "height",
            (Window.getClientHeight() - 1) + "px");
        DOM.setStyleAttribute(cursorUpdateDiv, "width",
            (Window.getClientWidth() - 1) + "px");
        DOM.setStyleAttribute(cursorUpdateDiv, "left", "0px");
        DOM.setStyleAttribute(cursorUpdateDiv, "top", "0px");
        DOM.appendChild(RootPanel.getBodyElement(), cursorUpdateDiv);
      }
      return cellChanged;
    }

    /**
     * Start resizing the current cell.
     *
     * @param event the mouse event
     */
    @Override
    public void startResizing(Event event) {
      removeCursorUpdateDiv();
      super.startResizing(event);
    }

    /**
     * Remove the cursor update div from the page.
     */
    private void removeCursorUpdateDiv() {
      if (DOM.getCaptureElement() != null) {
        DOM.removeChild(RootPanel.getBodyElement(), cursorUpdateDiv);
        DOM.releaseCapture(getScrollTable().headerWrapper);
      }
    }
  }

  /**
   * Information about the height of the inner tables.
   */
  private class TableHeightInfo {
    private int headerTableHeight;
    private int dataTableHeight;
    private int footerTableHeight;

    /**
     * Construct a new {@link TableHeightInfo}.
     */
    public TableHeightInfo() {
      int totalHeight = DOM.getElementPropertyInt(getElement(), "clientHeight");
      headerTableHeight = headerTable.getOffsetHeight();
      if (footerTable != null) {
        footerTableHeight = footerTable.getOffsetHeight();
      }
      dataTableHeight = totalHeight - headerTableHeight - footerTableHeight;
    }
  }

  /**
   * Information about the width of the inner tables.
   */
  private class TableWidthInfo {
    private int headerTableWidth;
    private int dataTableWidth;
    private int footerTableWidth;
    private int availableWidth;

    /**
     * Construct a new {@link TableWidthInfo}.
     *
     * @param includeSpacer true to include spacer in calculations
     */
    public TableWidthInfo(boolean includeSpacer) {
      availableWidth = getAvailableWidth();
      headerTableWidth = impl.getTableWidth(headerTable, includeSpacer);
      dataTableWidth = dataTable.getElement().getScrollWidth();
      if (footerTable != null) {
        footerTableWidth = impl.getTableWidth(footerTable, includeSpacer);
      }
    }
  }

  /** Resources used. */
  public interface ScrollTableStyle extends ClientBundle {
    /**
     * An image used to fill the available width.
     *
     * @return an image resource of this image
     */
    // @Source("scrollTableFillWidth.gif")
    @Source("scrollTableFillWidth.png")
    ImageResource scrollTableFillWidth();

    /**
     * An image indicating that a column is sorted in ascending order.
     *
     * @return an image resource of this image
     */
    @Source("scrollTableAscending.gif")
    ImageResource scrollTableAscending();

    /**
     * An image indicating a column is sorted in descending order.
     *
     * @return an image resource of this image
     */
    @Source("scrollTableDescending.gif")
    ImageResource scrollTableDescending();

    @Source("headerBackground.png")
    @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
    ImageResource headerBackground();
  }

  public interface ScrollTableMessages extends Messages {
    @DefaultMessage("Shrink/Expand to fill visible area")
    String shrinkExpandTooltip();

    @DefaultMessage("Shows only dates that are equal")
    String dateOperatorEqualTooltip();

    @DefaultMessage("Shows only dates that not equal")
    String dateOperatorUnequalTooltip();

    @DefaultMessage("Show only dates before the given date")
    String dateOperatorBeforeTooltip();

    @DefaultMessage("Show only dates after the given date")
    String dateOperatorAfterTooltip();

    @DefaultMessage("Show only dates between the given dates")
    String dateOperatorBetweenTooltip();
  }

  public interface ScrollTableResources {
    ScrollTableStyle getStyle();

    ScrollTableMessages getMessages();
  }

  protected static class DefaultScrollTableResources implements
      ScrollTableResources {
    private ScrollTableStyle style;
    private ScrollTableMessages messages;

    public ScrollTableStyle getStyle() {
      if (style == null) {
        style = GWT.create(ScrollTableStyle.class);
      }
      return style;
    }

    public ScrollTableMessages getMessages() {
      if (messages == null) {
        messages = GWT.create(ScrollTableMessages.class);
      }
      return messages;
    }
  }

  /**
   * The resize policies related to user resizing.
   *
   * <ul>
   * <li>DISABLED - Columns cannot be resized by the user</li>
   * <li>SINGLE_CELL - Only cells with a colspan of 1 can be resized</li>
   * <li>MULTI_CELL - All cells can be resized by the user</li>
   * </ul>
   */
  public static enum ColumnResizePolicy {
    DISABLED, SINGLE_CELL, MULTI_CELL
  }

  /**
   * The resize policies of table cells.
   *
   * <ul>
   * <li>UNCONSTRAINED - Columns shrink and expand independently of each other</li>
   * <li>FLOW - As one column expands or shrinks, the columns to the right will
   * do the opposite, trying to maintain the same size. The user can still
   * expand the grid if there is no more space to take from the columns on the
   * right.</li>
   * <li>FIXED_WIDTH - As one column expands or shrinks, the columns to the
   * right will do the opposite, trying to maintain the same size. The width of
   * the grid will remain constant, ignoring column resizing that would result
   * in the grid growing in size.</li>
   * <li>FILL_WIDTH - Same as FIXED_WIDTH, but the grid will always fill the
   * available width, even if the widget is resized.</li>
   * </ul>
   */
  public static enum ResizePolicy {
    UNCONSTRAINED(false, false), FLOW(false, true), FIXED_WIDTH(true, true), FILL_WIDTH(
        true, true);

    private boolean isSacrificial;
    private boolean isFixedWidth;

    private ResizePolicy(boolean isFixedWidth, boolean isSacrificial) {
      this.isFixedWidth = isFixedWidth;
      this.isSacrificial = isSacrificial;
    }

    private boolean isFixedWidth() {
      return isFixedWidth;
    }

    private boolean isSacrificial() {
      return isSacrificial;
    }
  }

  /**
   * The scroll policy of the table.
   *
   * <ul>
   * <li>HORIZONTAL - Only a horizontal scrollbar will be present.</li>
   * <li>BOTH - Both a vertical and horizontal scrollbar will be present.</li>
   * <li>DISABLED - Scrollbars will not appear, even if content doesn't fit</li>
   * </ul>
   */
  public static enum ScrollPolicy {
    HORIZONTAL, BOTH, DISABLED
  }

  /**
   * The sorting policies related to user column sorting.
   *
   * <ul>
   * <li>DISABLED - Columns cannot be sorted by the user</li>
   * <li>SINGLE_CELL - Only cells with a colspan of 1 can be sorted</li>
   * <li>MULTI_CELL - All cells can be sorted by the user</li>
   * </ul>
   */
  public static enum SortPolicy {
    DISABLED, SINGLE_CELL, MULTI_CELL
  }

  /**
   * The div that wraps the table wrappers.
   */
  private Element absoluteElem;

  /**
   * The helper class used to resize columns.
   */
  private ColumnResizer columnResizer = new ColumnResizer();

  /**
   * The policy applied to user actions that resize columns.
   */
  private ColumnResizePolicy columnResizePolicy = ColumnResizePolicy.MULTI_CELL;

  /**
   * The data table.
   */
  private FixedWidthGrid dataTable;

  /**
   * The scrollable wrapper div around the data table.
   */
  private Element dataWrapper;

  /**
   * An image used to show a fill width button.
   */
  private Image fillWidthImage;

  /**
   * A spacer used to stretch the footerTable area so we can scroll past the
   * edge of the footer table.
   */
  private Element footerSpacer = null;

  /**
   * The footer table.
   */
  private FixedWidthFlexTable footerTable = null;

  /**
   * The non-scrollable wrapper div around the footer table.
   */
  private Element footerWrapper = null;

  /**
   * A spacer used to stretch the headerTable area so we can scroll past the
   * edge of the header table.
   */
  private Element headerSpacer;

  /**
   * The header table.
   */

  private FixedWidthFlexTable headerTable = null;
  /**
   * The non-scrollable wrapper div around the header table.
   */
  private Element headerWrapper;

  /**
   * The resources applied to the table.
   */
  private ScrollTableResources resources;

  /**
   * The implementation class for this widget.
   */
  private Impl impl = GWT.create(Impl.class);

  /**
   * The last known height of this widget that the user set.
   */
  private String lastHeight = null;

  /**
   * The last scrollLeft position.
   */
  private int lastScrollLeft;

  /**
   * An element used to determine the width of the scroll bar.
   */
  private com.google.gwt.dom.client.Element mockScrollable;

  /**
   * A boolean indicating whether or not the grid should try to maintain its
   * width as much as possible.
   */
  private ResizePolicy resizePolicy = ResizePolicy.FLOW;

  /**
   * The worker that helps with mouse resize events.
   */
  private MouseResizeWorker resizeWorker = GWT.create(MouseResizeWorker.class);

  /**
   * The scrolling policy.
   */
  private ScrollPolicy scrollPolicy = ScrollPolicy.BOTH;

  /**
   * The current {@link SortPolicy}.
   */
  private SortPolicy sortPolicy = SortPolicy.SINGLE_CELL;

  /**
   * The cell index of the TD cell that initiated a column sort operation.
   */
  private int sortedCellIndex = -1;

  /**
   * The row index of the TD cell that initiated a column sort operation.
   */
  private int sortedRowIndex = -1;

  /**
   * The wrapper around the image indicator.
   */
  private Element sortedColumnWrapper = null;

  /**
   * Constructor.
   *
   * @param dataTable the data table
   * @param headerTable the header table
   */
  public AbstractScrollTable(FixedWidthGrid dataTable,
      FixedWidthFlexTable headerTable) {
    this(dataTable, headerTable, null,
        (ScrollTableResources) GWT.create(DefaultScrollTableResources.class));
  }

  /**
   * Constructor.
   *
   * @param dataTable the data table
   * @param headerTable the header table
   * @param images the images to use in the table
   */
  public AbstractScrollTable(FixedWidthGrid dataTable,
      final FixedWidthFlexTable headerTable, ScrollTableResources resources) {
    this(dataTable, headerTable, null, resources);
  }

  /**
   * Constructor.
   *
   * @param dataTable the data table
   * @param headerTable the header table
   * @param images the images to use in the table
   */
  protected AbstractScrollTable(FixedWidthGrid dataTable,
      final FixedWidthFlexTable headerTable,
      TableDefinition<?> tableDefinition, ScrollTableResources resources) {
    super();
    this.dataTable = dataTable;
    this.headerTable = headerTable;
    this.resources = resources;
    resizeWorker.setScrollTable(this);

    setCellPadding(3);
    setCellSpacing(0);

    // Prepare the header and data tables
    prepareTable(dataTable, "dataTable");
    prepareTable(headerTable, "headerTable");
    if (dataTable.getSelectionPolicy().hasInputColumn()) {
      headerTable.setColumnWidth(0, dataTable.getInputColumnWidth());
    }

    // Create the main div container
    Element mainElem = DOM.createDiv();
    setElement(mainElem);
    setStylePrimaryName("gwt-ScrollTable");
    DOM.setStyleAttribute(mainElem, "padding", "0px");
    DOM.setStyleAttribute(mainElem, "overflow", "hidden");
    DOM.setStyleAttribute(mainElem, "position", "relative");

    // Wrap the table wrappers in another div
    absoluteElem = DOM.createDiv();
    absoluteElem.getStyle().setProperty("position", "absolute");
    absoluteElem.getStyle().setProperty("top", "0px");
    absoluteElem.getStyle().setProperty("left", "0px");
    absoluteElem.getStyle().setProperty("width", "100%");
    absoluteElem.getStyle().setProperty("padding", "0px");
    absoluteElem.getStyle().setProperty("margin", "0px");
    absoluteElem.getStyle().setProperty("border", "0px");
    absoluteElem.getStyle().setProperty("overflow", "hidden");
    mainElem.appendChild(absoluteElem);

    // Create the table wrapper and spacer
    headerWrapper = createWrapper("headerWrapper");
    headerSpacer = impl.createSpacer(headerTable, headerWrapper);
    dataWrapper = createWrapper("dataWrapper");

    // Create an element to determine the scroll bar width
    mockScrollable = com.google.gwt.dom.client.Element.as(dataWrapper.cloneNode(false));
    mockScrollable.getStyle().setProperty("position", "absolute");
    mockScrollable.getStyle().setProperty("top", "0px");
    mockScrollable.getStyle().setProperty("left", "0px");
    mockScrollable.getStyle().setProperty("width", "100px");
    mockScrollable.getStyle().setProperty("height", "100px");
    mockScrollable.getStyle().setProperty("visibility", "hidden");
    mockScrollable.getStyle().setProperty("overflow", "scroll");
    mockScrollable.getStyle().setProperty("zIndex", "-1");
    absoluteElem.appendChild(mockScrollable);

    // Create image to fill width
    fillWidthImage = new Image() {
      @Override
      public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);
        if (DOM.eventGetType(event) == Event.ONCLICK) {
          fillWidth();
        }
      }
    };
    fillWidthImage.setTitle(resources.getMessages().shrinkExpandTooltip());
    ImageResource imageResource = resources.getStyle().scrollTableFillWidth();
    fillWidthImage.setUrlAndVisibleRect(imageResource.getURL(),
        imageResource.getLeft(), imageResource.getTop(),
        imageResource.getWidth(), imageResource.getHeight());
    Element fillWidthImageElem = fillWidthImage.getElement();
    DOM.setStyleAttribute(fillWidthImageElem, "cursor", "pointer");
    DOM.setStyleAttribute(fillWidthImageElem, "position", "absolute");
    DOM.setStyleAttribute(fillWidthImageElem, "top", "0px");
    DOM.setStyleAttribute(fillWidthImageElem, "right", "0px");
    DOM.setStyleAttribute(fillWidthImageElem, "zIndex", "1");
    add(fillWidthImage, getElement());

    // Adopt the header and data tables into the panel
    adoptTable(headerTable, headerWrapper, 0);
    adoptTable(dataTable, dataWrapper, 1);

    // Create the sort indicator Image
    sortedColumnWrapper = DOM.createSpan();

    // Add some event handling
    sinkEvents(Event.ONMOUSEOUT);
    DOM.setEventListener(dataWrapper, this);
    DOM.sinkEvents(dataWrapper, Event.ONSCROLL);
    DOM.setEventListener(headerWrapper, this);
    DOM.sinkEvents(headerWrapper, Event.ONMOUSEMOVE | Event.ONMOUSEDOWN
        | Event.ONMOUSEUP | Event.ONCLICK);

    // Listen for sorting events in the data table
    dataTable.addColumnSortHandler(new ColumnSortHandler() {
      public void onColumnSorted(ColumnSortEvent event) {
        // Get the primary column and sort order
        int column = -1;
        boolean ascending = true;
        ColumnSortList sortList = event.getColumnSortList();
        if (sortList != null) {
          column = sortList.getPrimaryColumn();
          ascending = sortList.isPrimaryAscending();
        }

        // Remove the sorted column indicator
        if (isColumnSortable(column)) {
          Element parent = DOM.getParent(sortedColumnWrapper);
          if (parent != null) {
            parent.removeChild(sortedColumnWrapper);
          }

          // Re-add the sorted column indicator
          if (column < 0) {
            sortedCellIndex = -1;
            sortedRowIndex = -1;
          } else if (sortedCellIndex >= 0 && sortedRowIndex >= 0
              && headerTable.getRowCount() > sortedRowIndex
              && headerTable.getCellCount(sortedRowIndex) > sortedCellIndex) {
            CellFormatter formatter = headerTable.getCellFormatter();
            Element td = formatter.getElement(sortedRowIndex, sortedCellIndex);
            applySortedColumnIndicator(td, ascending);
          }
        }
      }
    });
  }

  public HandlerRegistration addScrollHandler(ScrollHandler handler) {
    return addHandler(handler, ScrollEvent.getType());
  }

  /**
   * Adjust all column widths so they take up the maximum amount of space
   * without needing a horizontal scroll bar. The distribution will be
   * proportional to the current width of each column.
   *
   * The {@link AbstractScrollTable} must be visible on the page for this method
   * to work.
   */
  public void fillWidth() {
    List<ColumnWidthInfo> colWidths = getFillColumnWidths(null);
    applyNewColumnWidths(0, colWidths, false);
    scrollTables(false);
  }

  /**
   * @return the cell padding of the tables, in pixels
   */
  public int getCellPadding() {
    return dataTable.getCellPadding();
  }

  /**
   * @return the cell spacing of the tables, in pixels
   */
  public int getCellSpacing() {
    return dataTable.getCellSpacing();
  }

  /**
   * @return the column resize policy
   */
  public ColumnResizePolicy getColumnResizePolicy() {
    return columnResizePolicy;
  }

  /**
   * Return the column width for a given column index.
   *
   * @param column the column index
   * @return the column width in pixels
   */
  public int getColumnWidth(int column) {
    return dataTable.getColumnWidth(column);
  }

  /**
   * @return the data table
   */
  public FixedWidthGrid getDataTable() {
    return dataTable;
  }

  /**
   * @return the footer table
   */
  public FixedWidthFlexTable getFooterTable() {
    return footerTable;
  }

  /**
   * @return the header table
   */
  public FixedWidthFlexTable getHeaderTable() {
    return headerTable;
  }

  /**
   * Get the absolute maximum width of a column.
   *
   * @param column the column index
   * @return the maximum allowable width of the column
   */
  public abstract int getMaximumColumnWidth(int column);

  /**
   * Get the absolute minimum width of a column.
   *
   * @param column the column index
   * @return the minimum allowable width of the column
   */
  public abstract int getMinimumColumnWidth(int column);

  /**
   * Get the minimum offset width of the largest inner table given the
   * constraints on the minimum and ideal column widths. Note that this does not
   * account for the vertical scroll bar.
   *
   * @return the tables minimum offset width, or -1 if it cannot be calculated
   */
  public int getMinimumOffsetWidth() {
    if (!isAttached()) {
      return -1;
    }

    // Determine the width and column count of the largest table
    TableWidthInfo redrawInfo = new TableWidthInfo(true);
    maybeRecalculateIdealColumnWidths(null);
    if (redrawInfo.availableWidth < 1) {
      return -1;
    }

    int scrollWidth = 0;
    int numColumns = 0;
    {
      int numHeaderCols = headerTable.getColumnCount() - getHeaderOffset();
      int numDataCols = dataTable.getColumnCount();
      int numFooterCols = (footerTable == null) ? -1
          : footerTable.getColumnCount() - getHeaderOffset();
      if (numHeaderCols >= numDataCols && numHeaderCols >= numFooterCols) {
        numColumns = numHeaderCols;
        scrollWidth = redrawInfo.headerTableWidth;
      } else if (numFooterCols >= numDataCols && numFooterCols >= numHeaderCols) {
        numColumns = numFooterCols;
        scrollWidth = redrawInfo.footerTableWidth;
      } else if (numDataCols > 0) {
        numColumns = numDataCols;
        scrollWidth = redrawInfo.dataTableWidth;
      }
    }
    if (numColumns <= 0) {
      return -1;
    }

    // Calculate the available diff
    List<ColumnWidthInfo> colWidthInfos = getColumnWidthInfo(0, numColumns);
    return -columnResizer.distributeWidth(colWidthInfos, -scrollWidth);
  }

  /**
   * Get the preferred width of a column.
   *
   * @param column the column index
   * @return the preferred width of the column
   */
  public abstract int getPreferredColumnWidth(int column);

  /**
   * @return the resize policy
   */
  public ResizePolicy getResizePolicy() {
    return resizePolicy;
  }

  /**
   * @return the current scroll policy
   */
  public ScrollPolicy getScrollPolicy() {
    return scrollPolicy;
  }

  /**
   * @return the current sort policy
   */
  public SortPolicy getSortPolicy() {
    return sortPolicy;
  }

  /**
   * Returns true if the specified column is sortable.
   *
   * @param column the column index
   * @return true if the column is sortable, false if it is not sortable
   */
  public abstract boolean isColumnSortable(int column);

  /**
   * Returns true if the specified column can be truncated. If it cannot be
   * truncated, its minimum width will be adjusted to ensure the cell content is
   * visible.
   *
   * @param column the column index
   * @return true if the column is truncatable, false if it is not
   */
  public abstract boolean isColumnTruncatable(int column);

  /**
   * Returns true if the specified column in the footer table can be truncated.
   * If it cannot be truncated, its minimum width will be adjusted to ensure the
   * cell content is visible.
   *
   * @param column the column index
   * @return true if the column is truncatable, false if it is not
   */
  public abstract boolean isFooterColumnTruncatable(int column);

  /**
   * Returns true if the specified column in the header table can be truncated.
   * If it cannot be truncated, its minimum width will be adjusted to ensure the
   * cell content is visible.
   *
   * @param column the column index
   * @return true if the column is truncatable, false if it is not
   */
  public abstract boolean isHeaderColumnTruncatable(int column);

  @Override
  public void onBrowserEvent(Event event) {
    super.onBrowserEvent(event);
    Element target = DOM.eventGetTarget(event);
    switch (DOM.eventGetType(event)) {
      case Event.ONSCROLL:
        // Reposition the tables on scroll
        lastScrollLeft = dataWrapper.getScrollLeft();
        scrollTables(false);
        if (dataWrapper.isOrHasChild(target)) {
          ScrollEvent.fireNativeEvent(event, AbstractScrollTable.this);
        }
        break;

      case Event.ONMOUSEDOWN:
        // Start resizing a header column
        if (DOM.eventGetButton(event) != Event.BUTTON_LEFT) {
          return;
        }
        if (resizeWorker.getCurrentCell() != null) {
          DOM.eventPreventDefault(event);
          DOM.eventCancelBubble(event, true);
          resizeWorker.startResizing(event);
        }
        break;
      case Event.ONMOUSEUP:
        if (DOM.eventGetButton(event) != Event.BUTTON_LEFT) {
          return;
        }
        // Stop resizing the header column
        if (resizeWorker.isResizing()) {
          resizeWorker.stopResizing(event);
        } else {
          // Scroll tables if needed
          if (DOM.isOrHasChild(headerWrapper, target)) {
            scrollTables(true);
          } else {
            scrollTables(false);
          }

          // Get the actual column index
          Element cellElem = headerTable.getEventTargetCell(event);
          if (cellElem != null) {
            // Sorting is disabled
            if (sortPolicy == SortPolicy.DISABLED) {
              return;
            }

            // Check the colSpan
            int colSpan = cellElem.getPropertyInt("colSpan");
            if (colSpan > 1 && getSortPolicy() != SortPolicy.MULTI_CELL) {
              return;
            }

            // Sort the column
            sortedRowIndex = OverrideDOM.getRowIndex(DOM.getParent(cellElem)) - 1;
            sortedCellIndex = OverrideDOM.getCellIndex(cellElem);
            int column = headerTable.getColumnIndex(sortedRowIndex,
                sortedCellIndex)
                - getHeaderOffset();
            if (column >= 0 && isColumnSortable(column)) {
              if (dataTable.getColumnCount() > column
                  && onHeaderSort(sortedRowIndex, column)) {
                dataTable.sortColumn(column);
              }
            }
          }
        }
        break;
      case Event.ONMOUSEMOVE:
        // Resize the header column
        if (resizeWorker.isResizing()) {
          resizeWorker.resizeColumn(event);
        } else {
          resizeWorker.setCurrentCell(event);
        }
        break;
      case Event.ONMOUSEOUT:
        // Unhighlight if the mouse leaves the table
        Element toElem = DOM.eventGetToElement(event);
        if (toElem == null || !dataWrapper.isOrHasChild(toElem)) {
          // Check that the coordinates are not directly over the table
          int clientX = event.getClientX() + Window.getScrollLeft();
          int clientY = event.getClientY() + Window.getScrollTop();
          int tableLeft = dataWrapper.getAbsoluteLeft();
          int tableTop = dataWrapper.getAbsoluteTop();
          int tableWidth = dataWrapper.getOffsetWidth();
          int tableHeight = dataWrapper.getOffsetHeight();
          int tableBottom = tableTop + tableHeight;
          int tableRight = tableLeft + tableWidth;
          if (clientX > tableLeft && clientX < tableRight && clientY > tableTop
              && clientY < tableBottom) {
            return;
          }

          dataTable.highlightCell(null);
        }
        break;
    }
  }

  /**
   * This method is called when the dimensions of the parent element change.
   * Subclasses should override this method as needed.
   *
   * @param width the new client width of the element
   * @param height the new client height of the element
   */
  public void onResize(int width, int height) {
    redraw();
  }

  /**
   * Redraw the table.
   */
  public void redraw() {
    if (!isAttached()) {
      return;
    }

    // Create a command to execute while recalculating widths. Using this
    // command prevents an extra browser layout by grouping read operations.
    TableWidthInfo redrawInfo = new TableWidthInfo(false);
    Command command = new Command() {
      public void execute() {
        // We update the ResizableWidgetCollection before changing the size of
        // the ScrollTable, because change the size of the scroll table could
        // require an additional layout (ex. if window scroll bars show up).
        // ResizableWidgetCollection.get().updateWidgetSize(
        // AbstractScrollTable.this);
      }
    };

    // Recalculate the ideal table widths of each column.
    maybeRecalculateIdealColumnWidths(command);

    // Calculate the new widths of the columns
    List<ColumnWidthInfo> colWidths = null;
    if (resizePolicy == ResizePolicy.FILL_WIDTH) {
      colWidths = getFillColumnWidths(redrawInfo);
    } else {
      colWidths = getBoundedColumnWidths(true);
    }
    applyNewColumnWidths(0, colWidths, true);

    // Update the overall height of the scroll table. This can only happen
    // after the widths have been set because setting the width of cells can
    // cause word wrap, which increases the height of the inner tables.
    resizeTablesVertically();

    // Reset the scroll position, which might be lost when we change the layout.
    scrollTables(false);
  }

  /**
   * Unsupported.
   *
   * @param child the widget to be removed
   * @return false
   * @throws UnsupportedOperationException
   */
  @Override
  public boolean remove(Widget child) {
    throw new UnsupportedOperationException(
        "This panel does not support remove()");
  }

  /**
   * Reset the widths of all columns to their preferred sizes.
   */
  public void resetColumnWidths() {
    applyNewColumnWidths(0, getBoundedColumnWidths(false), false);
    scrollTables(false);
  }

  /**
   * Sets the amount of padding to be added around all cells.
   *
   * @param padding the cell padding, in pixels
   */
  public void setCellPadding(int padding) {
    headerTable.setCellPadding(padding);
    dataTable.setCellPadding(padding);
    if (footerTable != null) {
      footerTable.setCellPadding(padding);
    }
    redraw();
  }

  /**
   * Sets the amount of spacing to be added around all cells.
   *
   * @param spacing the cell spacing, in pixels
   */
  public void setCellSpacing(int spacing) {
    headerTable.setCellSpacing(spacing);
    dataTable.setCellSpacing(spacing);
    if (footerTable != null) {
      footerTable.setCellSpacing(spacing);
    }
    redraw();
  }

  /**
   * Set the resize policy applied to user actions that resize columns.
   *
   * @param columnResizePolicy the resize policy
   */
  public void setColumnResizePolicy(ColumnResizePolicy columnResizePolicy) {
    this.columnResizePolicy = columnResizePolicy;
    updateFillWidthImage();
  }

  /**
   * Set the width of a column.
   *
   * @param column the index of the column
   * @param width the width in pixels
   * @return the new column width
   */
  public int setColumnWidth(int column, int width) {
    // Constrain the size of the column
    ColumnWidthInfo info = getColumnWidthInfo(column);
    if (info.hasMaximumWidth()) {
      width = Math.min(width, info.getMaximumWidth());
    }
    if (info.hasMinimumWidth()) {
      width = Math.max(width, info.getMinimumWidth());
    }

    // Try to constrain the size of the grid
    if (resizePolicy.isSacrificial()) {
      // Get the sacrifice columns
      int sacrificeColumn = column + 1;
      int numColumns = dataTable.getColumnCount();
      int remainingColumns = numColumns - sacrificeColumn;
      List<ColumnWidthInfo> infos = getColumnWidthInfo(sacrificeColumn,
          remainingColumns);

      // Distribute the width over the sacrifice columns
      int diff = width - getColumnWidth(column);
      int undistributed = columnResizer.distributeWidth(infos, -diff);

      // Set the new column widths
      applyNewColumnWidths(sacrificeColumn, infos, false);

      // Prevent over resizing
      if (resizePolicy.isFixedWidth()) {
        width += undistributed;
      }
    }

    // Resize the column
    int offset = getHeaderOffset();
    dataTable.setColumnWidth(column, width);
    headerTable.setColumnWidth(column + offset, width);
    if (footerTable != null) {
      footerTable.setColumnWidth(column + offset, width);
    }

    // Reposition things as needed
    impl.repositionSpacer(this, false);
    resizeTablesVertically();
    scrollTables(false);
    return width;
  }

  /**
   * Set the footer table that appears under the data table. If set to null, the
   * footer table will not be shown.
   *
   * @param footerTable the table to use in the footer
   */
  public void setFooterTable(FixedWidthFlexTable footerTable) {
    // Disown the old footer table
    if (this.footerTable != null) {
      super.remove(this.footerTable);
      DOM.removeChild(absoluteElem, footerWrapper);
    }

    // Set the new footer table
    this.footerTable = footerTable;
    if (footerTable != null) {
      footerTable.setCellSpacing(getCellSpacing());
      footerTable.setCellPadding(getCellPadding());
      prepareTable(footerTable, "footerTable");
      if (dataTable.getSelectionPolicy().hasInputColumn()) {
        footerTable.setColumnWidth(0, dataTable.getInputColumnWidth());
      }

      // Create the footer wrapper and spacer
      if (footerWrapper == null) {
        footerWrapper = createWrapper("footerWrapper");
        footerSpacer = impl.createSpacer(footerTable, footerWrapper);
        DOM.setEventListener(footerWrapper, this);
        DOM.sinkEvents(footerWrapper, Event.ONMOUSEUP);
      }

      // Adopt the header table into the panel
      adoptTable(footerTable, footerWrapper,
          absoluteElem.getChildNodes().getLength());
    }
    redraw();
  }

  @Override
  public void setHeight(String height) {
    this.lastHeight = height;
    super.setHeight(height);
    resizeTablesVertically();
  }

  /**
   * Set the resize policy of the table.
   *
   * @param resizePolicy the resize policy
   */
  public void setResizePolicy(ResizePolicy resizePolicy) {
    this.resizePolicy = resizePolicy;
    updateFillWidthImage();
    redraw();
  }

  /**
   * Set the scroll policy of the table.
   *
   * @param scrollPolicy the new scroll policy
   */
  public void setScrollPolicy(ScrollPolicy scrollPolicy) {
    if (scrollPolicy == this.scrollPolicy) {
      return;
    }
    this.scrollPolicy = scrollPolicy;

    // Clear the heights of the wrappers
    headerWrapper.getStyle().setProperty("height", "");
    dataWrapper.getStyle().setProperty("height", "");
    if (footerWrapper != null) {
      footerWrapper.getStyle().setProperty("height", "");
    }

    if (scrollPolicy == ScrollPolicy.DISABLED) {
      // Disabled scroll bars
      dataWrapper.getStyle().setProperty("height", "auto");
      dataWrapper.getStyle().setProperty("overflow", "");
    } else if (scrollPolicy == ScrollPolicy.HORIZONTAL) {
      // Only show horizontal scroll bar
      dataWrapper.getStyle().setProperty("height", "auto");
      dataWrapper.getStyle().setProperty("overflow", "auto");
    } else if (scrollPolicy == ScrollPolicy.BOTH) {
      // Show both scroll bars
      if (lastHeight != null) {
        super.setHeight(lastHeight);
      } else {
        super.setHeight("");
      }
      dataWrapper.getStyle().setProperty("overflow", "auto");
    }

    // Resize the tables
    impl.repositionSpacer(this, true);
    redraw();
  }

  /**
   * Set the {@link SortPolicy} that defines what columns users can sort.
   *
   * @param sortPolicy the {@link SortPolicy}
   */
  public void setSortPolicy(SortPolicy sortPolicy) {
    this.sortPolicy = sortPolicy;

    // Remove the sorted indicator image
    applySortedColumnIndicator(null, true);
  }

  /**
   * Apply the sorted column indicator to a specific table cell in the header
   * table.
   *
   * @param tdElem the cell in the header table, or null to remove it
   * @param ascending true to apply the ascending indicator, false for
   *          descending
   */
  protected void applySortedColumnIndicator(Element tdElem, boolean ascending) {
    // Remove the sort indicator
    if (tdElem == null) {
      Element parent = DOM.getParent(sortedColumnWrapper);
      if (parent != null) {
        parent.removeChild(sortedColumnWrapper);
        headerTable.clearIdealWidths();
      }
      return;
    }

    tdElem.appendChild(sortedColumnWrapper);
    ImageResource imageResource;
    if (ascending) {
      imageResource = resources.getStyle().scrollTableAscending();
    } else {
      imageResource = resources.getStyle().scrollTableDescending();
    }
    Image sortIndicator = new Image();
    sortIndicator.setTitle(resources.getMessages().shrinkExpandTooltip());
    sortIndicator.setUrlAndVisibleRect(imageResource.getURL(),
        imageResource.getLeft(), imageResource.getTop(),
        imageResource.getWidth(), imageResource.getHeight());
    sortedColumnWrapper.setInnerHTML("&nbsp;" + sortIndicator.toString());
    sortedRowIndex = -1;
    sortedCellIndex = -1;

    // The column with the indicator now has a new ideal width
    headerTable.clearIdealWidths();
    redraw();
  }

  /**
   * Create a wrapper element that will hold a table.
   *
   * @param cssName the style name added to the base name
   * @return a new wrapper element
   */
  protected Element createWrapper(String cssName) {
    Element wrapper = DOM.createDiv();
    wrapper.getStyle().setProperty("width", "100%");
    wrapper.getStyle().setProperty("overflow", "hidden");
    wrapper.getStyle().setPropertyPx("padding", 0);
    wrapper.getStyle().setPropertyPx("margin", 0);
    wrapper.getStyle().setPropertyPx("border", 0);
    if (cssName != null) {
      setStyleName(wrapper, cssName);
    }
    return wrapper;
  }

  /**
   * @return the wrapper element around the data table
   */
  protected Element getDataWrapper() {
    return dataWrapper;
  }

  /**
   * Extend the columns to exactly fill the available space, if the current
   * {@link ResizePolicy} requires it.
   *
   * @deprecated use {@link #redraw()} instead
   */
  @Deprecated
  protected void maybeFillWidth() {
    redraw();
  }

  /**
   * Called just before a column is sorted because of a user click on the header
   * row.
   *
   * @param row the row index that was clicked
   * @param column the column index that was clicked
   * @return true to sort, false to ignore
   */
  protected boolean onHeaderSort(int row, int column) {
    return true;
  }

  @Override
  protected void onLoad() {
    redraw();
  }

  /**
   * Fixes the table heights so the header is visible and the data takes up the
   * remaining vertical space.
   */
  protected void resizeTablesVertically() {
    if (scrollPolicy == ScrollPolicy.DISABLED) {
      dataWrapper.getStyle().setProperty("overflow", "auto");
      dataWrapper.getStyle().setProperty("overflow", "");
      int height = Math.max(1, absoluteElem.getOffsetHeight());
      super.setHeight(height + "px");
    } else if (scrollPolicy == ScrollPolicy.HORIZONTAL) {
      dataWrapper.getStyle().setProperty("overflow", "hidden");
      dataWrapper.getStyle().setProperty("overflow", "auto");
      int height = Math.max(1, absoluteElem.getOffsetHeight());
      super.setHeight(height + "px");
    } else {
      applyTableWrapperSizes(getTableWrapperSizes());
      dataWrapper.getStyle().setProperty("width", "100%");
    }
  }

  /**
   * Helper method that actually performs the vertical resizing.
   *
   * @deprecated use {@link #redraw()} instead
   */
  @Deprecated
  protected void resizeTablesVerticallyNow() {
    redraw();
  }

  /**
   * Sets the scroll property of the header and footers wrappers when scrolling
   * so that the header, footer, and data tables line up.
   *
   * @param baseHeader true to scroll the data table as well
   */
  protected void scrollTables(boolean baseHeader) {
    if (scrollPolicy == ScrollPolicy.DISABLED) {
      return;
    }

    if (lastScrollLeft >= 0) {
      headerWrapper.setScrollLeft(lastScrollLeft);
      if (baseHeader) {
        dataWrapper.setScrollLeft(lastScrollLeft);
      }
      if (footerWrapper != null) {
        footerWrapper.setScrollLeft(lastScrollLeft);
      }
    }
  }

  /**
   * @return the absolutely positioned wrapper element
   */
  Element getAbsoluteElement() {
    return absoluteElem;
  }

  /**
   * Adopt a table into this {@link AbstractScrollTable} within its wrapper.
   *
   * @param table the table to adopt
   * @param wrapper the wrapper element
   * @param index the index to insert the wrapper in the main element
   */
  private void adoptTable(Widget table, Element wrapper, int index) {
    DOM.insertChild(absoluteElem, wrapper, index);
    add(table, wrapper);
  }

  /**
   * Apply the new widths to a list of columns.
   *
   * @param startIndex the index of the first column
   * @param infos the new column width info
   * @param forced if false, only set column widths that have changed
   */
  private void applyNewColumnWidths(int startIndex,
      List<ColumnWidthInfo> infos, boolean forced) {
    // Infos can be null if the widths cannot be calculated
    if (infos == null) {
      return;
    }

    int offset = getHeaderOffset();
    int numColumns = infos.size();
    for (int i = 0; i < numColumns; i++) {
      ColumnWidthInfo info = infos.get(i);
      int newWidth = info.getNewWidth();
      if (forced || info.getCurrentWidth() != newWidth) {
        dataTable.setColumnWidth(startIndex + i, newWidth);
        headerTable.setColumnWidth(startIndex + i + offset, newWidth);
        if (footerTable != null) {
          footerTable.setColumnWidth(startIndex + i + offset, newWidth);
        }
      }
    }
    impl.repositionSpacer(this, false);
  }

  /**
   * Apply the new sizes to the table wrappers.
   *
   * @param sizes the sizes to apply
   */
  private void applyTableWrapperSizes(TableHeightInfo sizes) {
    if (sizes == null) {
      return;
    }

    headerWrapper.getStyle().setPropertyPx("height", sizes.headerTableHeight);
    if (footerWrapper != null) {
      footerWrapper.getStyle().setPropertyPx("height", sizes.footerTableHeight);
    }
    dataWrapper.getStyle().setPropertyPx("height",
        Math.max(sizes.dataTableHeight, 0));
    dataWrapper.getStyle().setProperty("overflow", "hidden");
    dataWrapper.getStyle().setProperty("overflow", "auto");
  }

  /**
   * Get the width available for the tables.
   *
   * @return the available width, or -1 if not defined
   */
  private int getAvailableWidth() {
    int clientWidth = absoluteElem.getPropertyInt("clientWidth");
    if (scrollPolicy == ScrollPolicy.BOTH) {
      int scrollbarWidth = mockScrollable.getOffsetWidth()
          - mockScrollable.getPropertyInt("clientWidth");
      clientWidth = absoluteElem.getPropertyInt("clientWidth") - scrollbarWidth
          - 1;
    }
    return Math.max(clientWidth, -1);
  }

  /**
   * Get the widths of all columns, either to their preferred sizes or just
   * ensure that they are within their min/max boundaries.
   *
   * @param boundsOnly true to only ensure the widths are within the bounds
   * @return the column widths
   */
  private List<ColumnWidthInfo> getBoundedColumnWidths(boolean boundsOnly) {
    if (!isAttached()) {
      return null;
    }

    // Calculate the new column widths
    int numColumns = dataTable.getColumnCount();
    int totalWidth = 0;
    List<ColumnWidthInfo> colWidthInfos = getColumnWidthInfo(0, numColumns);

    // If we are reseting to original widths, set all widths to 0
    if (!boundsOnly) {
      for (ColumnWidthInfo info : colWidthInfos) {
        totalWidth += info.getCurrentWidth();
        info.setCurrentWidth(0);
      }
    }

    // Run the resize algorithm
    columnResizer.distributeWidth(colWidthInfos, totalWidth);

    // Set the new column widths
    return colWidthInfos;
  }

  /**
   * Get info about the width of a column.
   *
   * @param column the column index
   * @return the info about the column width
   */
  private ColumnWidthInfo getColumnWidthInfo(int column) {
    int minWidth = getMinimumColumnWidth(column);
    int maxWidth = getMaximumColumnWidth(column);
    int preferredWidth = getPreferredColumnWidth(column);
    int curWidth = getColumnWidth(column);

    // Adjust the widths if the columns are not truncatable, up to maxWidth
    if (!isColumnTruncatable(column)) {
      maybeRecalculateIdealColumnWidths(null);
      int idealWidth = getDataTable().getIdealColumnWidth(column);
      if (maxWidth != MaximumWidthProperty.NO_MAXIMUM_WIDTH) {
        idealWidth = Math.min(idealWidth, maxWidth);
      }
      minWidth = Math.max(minWidth, idealWidth);
    }
    if (!isHeaderColumnTruncatable(column)) {
      maybeRecalculateIdealColumnWidths(null);
      int idealWidth = getHeaderTable().getIdealColumnWidth(
          column + getHeaderOffset());
      if (maxWidth != MaximumWidthProperty.NO_MAXIMUM_WIDTH) {
        idealWidth = Math.min(idealWidth, maxWidth);
      }
      minWidth = Math.max(minWidth, idealWidth);
    }
    if (footerTable != null && !isFooterColumnTruncatable(column)) {
      maybeRecalculateIdealColumnWidths(null);
      int idealWidth = getFooterTable().getIdealColumnWidth(
          column + getHeaderOffset());
      if (maxWidth != MaximumWidthProperty.NO_MAXIMUM_WIDTH) {
        idealWidth = Math.min(idealWidth, maxWidth);
      }
      minWidth = Math.max(minWidth, idealWidth);
    }

    return new ColumnWidthInfo(minWidth, maxWidth, preferredWidth, curWidth);
  }

  /**
   * Get info about the width of multiple columns.
   *
   * @param column the start column index
   * @param numColumns the number of columns
   * @return the info about the column widths of the columns
   */
  private List<ColumnWidthInfo> getColumnWidthInfo(int column, int numColumns) {
    List<ColumnWidthInfo> infos = new ArrayList<ColumnWidthInfo>();
    for (int i = 0; i < numColumns; i++) {
      infos.add(getColumnWidthInfo(column + i));
    }
    return infos;
  }

  /**
   * Get the column widths needed to fill with available ScrollTable width.
   *
   * @param info the optional precomputed sizes
   * @return the column widths
   */
  private List<ColumnWidthInfo> getFillColumnWidths(TableWidthInfo info) {
    if (!isAttached()) {
      return null;
    }

    // Precompute some sizes
    if (info == null) {
      info = new TableWidthInfo(false);
    }

    // Calculate how much room we have to work with
    int clientWidth = info.availableWidth;
    if (clientWidth <= 0) {
      return null;
    }

    // Calculate the difference and number of column to resize
    int diff = 0;
    int numColumns = 0;
    {
      // Calculate the number of columns in each table
      int numHeaderCols = 0;
      int numDataCols = 0;
      int numFooterCols = 0;
      if (info.headerTableWidth > 0) {
        numHeaderCols = headerTable.getColumnCount() - getHeaderOffset();
      }
      if (info.dataTableWidth > 0) {
        numDataCols = dataTable.getColumnCount();
      }
      if (footerTable != null && info.footerTableWidth > 0) {
        numFooterCols = footerTable.getColumnCount() - getHeaderOffset();
      }

      // Determine the largest table
      if (numHeaderCols >= numDataCols && numHeaderCols >= numFooterCols) {
        numColumns = numHeaderCols;
        diff = clientWidth - info.headerTableWidth;
      } else if (numFooterCols >= numDataCols && numFooterCols >= numHeaderCols) {
        numColumns = numFooterCols;
        diff = clientWidth - info.footerTableWidth;
      } else if (numDataCols > 0) {
        numColumns = numDataCols;
        diff = clientWidth - info.dataTableWidth;
      }
    }
    if (numColumns <= 0) {
      return null;
    }

    // Calculate the new column widths
    List<ColumnWidthInfo> colWidthInfos = getColumnWidthInfo(0, numColumns);
    columnResizer.distributeWidth(colWidthInfos, diff);
    return colWidthInfos;
  }

  /**
   * Get the offset between the data and header and footer tables. An offset of
   * one means that the header and footer table indexes are one greater than the
   * data table indexes, probably because the data table contains a checkbox
   * column.
   *
   * @return the offset
   */
  private int getHeaderOffset() {
    if (dataTable.getSelectionPolicy().hasInputColumn()) {
      return 1;
    }
    return 0;
  }

  /**
   * Returns the new heights of the header, data, and footer tables based on the
   * {@link ScrollPolicy}.
   *
   * @return the new table heights, or null
   */
  private TableHeightInfo getTableWrapperSizes() {
    // If we aren't attached, return immediately
    if (!isAttached()) {
      return null;
    }

    // Heights only apply with vertical scrolling
    if (scrollPolicy == ScrollPolicy.DISABLED
        || scrollPolicy == ScrollPolicy.HORIZONTAL) {
      return null;
    }

    // Give the data wrapper all remaining height
    return new TableHeightInfo();
  }

  /**
   * Recalculate the ideal columns widths of all inner tables.
   *
   * @param command an optional command to execute while recalculating
   */
  private void maybeRecalculateIdealColumnWidths(Command command) {
    // Calculations require that we are attached
    if (!isAttached()) {
      return;
    }

    // Check if a recalculation is needed.
    if (headerTable.isIdealColumnWidthsCalculated()
        && dataTable.isIdealColumnWidthsCalculated()
        && (footerTable == null || footerTable.isIdealColumnWidthsCalculated())) {
      if (command != null) {
        command.execute();
      }
      return;
    }

    impl.recalculateIdealColumnWidths(this, command);
  }

  /**
   * Prepare a table to be added to the {@link AbstractScrollTable}.
   *
   * @param table the table to prepare
   * @param cssName the style name added to the base name
   */
  private void prepareTable(Widget table, String cssName) {
    Element tableElem = table.getElement();
    DOM.setStyleAttribute(tableElem, "margin", "0px");
    DOM.setStyleAttribute(tableElem, "border", "0px");
    table.addStyleName(cssName);
  }

  /**
   * Show or hide to fillWidthImage depending on current policies.
   */
  private void updateFillWidthImage() {
    if (columnResizePolicy == ColumnResizePolicy.DISABLED
        || resizePolicy.isFixedWidth()) {
      fillWidthImage.setVisible(false);
    } else {
      fillWidthImage.setVisible(true);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see org.gwt.mosaic.ui.client.layout.HasLayoutManager#getPreferredSize()
   */
  public Dimension getPreferredSize() {
    int width = getHeaderTable().getOffsetWidth();
    int height = getHeaderTable().getOffsetHeight()
        + getDataTable().getOffsetHeight();
    if (getFooterTable() != null) {
      height += getFooterTable().getOffsetHeight();
    }
    final int[] m = DOM.getMarginSizes(getElement());
    return new Dimension(width + m[1] + m[3], height + m[0] + m[2]);
  }

  /**
   * {@inheritDoc}
   *
   * @see org.gwt.mosaic.ui.client.layout.HasLayoutManager#invalidate()
   */
  public void invalidate() {
    invalidate(null);
  }

  /**
   * {@inheritDoc}
   *
   * @see org.gwt.mosaic.ui.client.layout.HasLayoutManager#invalidate(com.google.gwt.user.client.ui.Widget)
   */
  public void invalidate(Widget widget) {
    WidgetHelper.invalidate(getParent());
  }

  /**
   * {@inheritDoc}
   *
   * @see org.gwt.mosaic.ui.client.layout.HasLayoutManager#layout()
   */
  public void layout() {
    redraw();
  }

  /**
   * {@inheritDoc}
   *
   * @see com.google.gwt.user.client.ui.RequiresResize#onResize()
   */
  public void onResize() {
    layout();
  }

  /**
   * {@inheritDoc}
   *
   * @see org.gwt.mosaic.ui.client.layout.HasLayoutManager#needsLayout()
   */
  public boolean needsLayout() {
    return false;
  }

}
TOP

Related Classes of org.gwt.mosaic.ui.client.table.AbstractScrollTable$TableWidthInfo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.