/*
* 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(" " + 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;
}
}