com.vaadin.v7.client.widgets.Grid Maven / Gradle / Ivy
Show all versions of vaadin-compatibility-client Show documentation
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.v7.client.widgets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyEvent;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.touch.client.Point;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ResizeComposite;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.DeferredWorker;
import com.vaadin.client.Focusable;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.data.AbstractRemoteDataSource;
import com.vaadin.client.data.DataChangeHandler;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.ui.FocusUtil;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.dd.DragAndDropHandler;
import com.vaadin.client.ui.dd.DragAndDropHandler.DragAndDropCallback;
import com.vaadin.client.ui.dd.DragHandle;
import com.vaadin.client.ui.dd.DragHandle.DragHandleCallback;
import com.vaadin.client.widgets.Overlay;
import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.v7.client.renderers.ComplexRenderer;
import com.vaadin.v7.client.renderers.ProgressBarRenderer;
import com.vaadin.v7.client.renderers.Renderer;
import com.vaadin.v7.client.renderers.TextRenderer;
import com.vaadin.v7.client.renderers.WidgetRenderer;
import com.vaadin.v7.client.widget.escalator.Cell;
import com.vaadin.v7.client.widget.escalator.ColumnConfiguration;
import com.vaadin.v7.client.widget.escalator.EscalatorUpdater;
import com.vaadin.v7.client.widget.escalator.FlyweightCell;
import com.vaadin.v7.client.widget.escalator.Row;
import com.vaadin.v7.client.widget.escalator.RowContainer;
import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeEvent;
import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeHandler;
import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.Direction;
import com.vaadin.v7.client.widget.escalator.Spacer;
import com.vaadin.v7.client.widget.escalator.SpacerUpdater;
import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent;
import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedHandler;
import com.vaadin.v7.client.widget.escalator.events.SpacerVisibilityChangedEvent;
import com.vaadin.v7.client.widget.escalator.events.SpacerVisibilityChangedHandler;
import com.vaadin.v7.client.widget.grid.AutoScroller;
import com.vaadin.v7.client.widget.grid.AutoScroller.AutoScrollerCallback;
import com.vaadin.v7.client.widget.grid.AutoScroller.ScrollAxis;
import com.vaadin.v7.client.widget.grid.CellReference;
import com.vaadin.v7.client.widget.grid.CellStyleGenerator;
import com.vaadin.v7.client.widget.grid.DataAvailableEvent;
import com.vaadin.v7.client.widget.grid.DataAvailableHandler;
import com.vaadin.v7.client.widget.grid.DefaultEditorEventHandler;
import com.vaadin.v7.client.widget.grid.DetailsGenerator;
import com.vaadin.v7.client.widget.grid.EditorHandler;
import com.vaadin.v7.client.widget.grid.EditorHandler.EditorRequest;
import com.vaadin.v7.client.widget.grid.EventCellReference;
import com.vaadin.v7.client.widget.grid.GridEventHandler;
import com.vaadin.v7.client.widget.grid.HeightAwareDetailsGenerator;
import com.vaadin.v7.client.widget.grid.RendererCellReference;
import com.vaadin.v7.client.widget.grid.RowReference;
import com.vaadin.v7.client.widget.grid.RowStyleGenerator;
import com.vaadin.v7.client.widget.grid.datasources.ListDataSource;
import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler;
import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler;
import com.vaadin.v7.client.widget.grid.events.BodyClickHandler;
import com.vaadin.v7.client.widget.grid.events.BodyDoubleClickHandler;
import com.vaadin.v7.client.widget.grid.events.BodyKeyDownHandler;
import com.vaadin.v7.client.widget.grid.events.BodyKeyPressHandler;
import com.vaadin.v7.client.widget.grid.events.BodyKeyUpHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnReorderEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnReorderHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnResizeEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnResizeHandler;
import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeEvent;
import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeHandler;
import com.vaadin.v7.client.widget.grid.events.FooterClickHandler;
import com.vaadin.v7.client.widget.grid.events.FooterDoubleClickHandler;
import com.vaadin.v7.client.widget.grid.events.FooterKeyDownHandler;
import com.vaadin.v7.client.widget.grid.events.FooterKeyPressHandler;
import com.vaadin.v7.client.widget.grid.events.FooterKeyUpHandler;
import com.vaadin.v7.client.widget.grid.events.GridClickEvent;
import com.vaadin.v7.client.widget.grid.events.GridDoubleClickEvent;
import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent;
import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler;
import com.vaadin.v7.client.widget.grid.events.GridKeyDownEvent;
import com.vaadin.v7.client.widget.grid.events.GridKeyPressEvent;
import com.vaadin.v7.client.widget.grid.events.GridKeyUpEvent;
import com.vaadin.v7.client.widget.grid.events.HeaderClickHandler;
import com.vaadin.v7.client.widget.grid.events.HeaderDoubleClickHandler;
import com.vaadin.v7.client.widget.grid.events.HeaderKeyDownHandler;
import com.vaadin.v7.client.widget.grid.events.HeaderKeyPressHandler;
import com.vaadin.v7.client.widget.grid.events.HeaderKeyUpHandler;
import com.vaadin.v7.client.widget.grid.events.ScrollEvent;
import com.vaadin.v7.client.widget.grid.events.ScrollHandler;
import com.vaadin.v7.client.widget.grid.events.SelectAllEvent;
import com.vaadin.v7.client.widget.grid.events.SelectAllHandler;
import com.vaadin.v7.client.widget.grid.selection.HasSelectionHandlers;
import com.vaadin.v7.client.widget.grid.selection.HasUserSelectionAllowed;
import com.vaadin.v7.client.widget.grid.selection.MultiSelectionRenderer;
import com.vaadin.v7.client.widget.grid.selection.SelectionEvent;
import com.vaadin.v7.client.widget.grid.selection.SelectionHandler;
import com.vaadin.v7.client.widget.grid.selection.SelectionModel;
import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi;
import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Single;
import com.vaadin.v7.client.widget.grid.selection.SelectionModelMulti;
import com.vaadin.v7.client.widget.grid.selection.SelectionModelNone;
import com.vaadin.v7.client.widget.grid.selection.SelectionModelSingle;
import com.vaadin.v7.client.widget.grid.sort.Sort;
import com.vaadin.v7.client.widget.grid.sort.SortEvent;
import com.vaadin.v7.client.widget.grid.sort.SortHandler;
import com.vaadin.v7.client.widget.grid.sort.SortOrder;
import com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer;
import com.vaadin.v7.client.widgets.Escalator.SubPartArguments;
import com.vaadin.v7.client.widgets.Grid.Editor.State;
import com.vaadin.v7.client.widgets.Grid.StaticSection.StaticRow;
import com.vaadin.v7.shared.ui.grid.ColumnResizeMode;
import com.vaadin.v7.shared.ui.grid.GridConstants;
import com.vaadin.v7.shared.ui.grid.GridConstants.Section;
import com.vaadin.v7.shared.ui.grid.GridStaticCellType;
import com.vaadin.v7.shared.ui.grid.HeightMode;
import com.vaadin.v7.shared.ui.grid.ScrollDestination;
/**
* A data grid view that supports columns and lazy loading of data rows from a
* data source.
*
* Columns
*
* Each column in Grid is represented by a {@link Column}. Each
* {@code GridColumn} has a custom implementation for
* {@link Column#getValue(Object)} that gets the row object as an argument, and
* returns the value for that particular column, extracted from the row object.
*
* Each column also has a Renderer. Its function is to take the value that is
* given by the {@code GridColumn} and display it to the user. A simple column
* might have a {@link TextRenderer} that simply takes in a {@code String} and
* displays it as the cell's content. A more complex renderer might be
* {@link ProgressBarRenderer} that takes in a floating point number, and
* displays a progress bar instead, based on the given number.
*
* See: {@link #addColumn(Column)}, {@link #addColumn(Column, int)} and
* {@link #addColumns(Column...)}. Also
* {@link Column#setRenderer(Renderer)}.
*
*
Data Sources
*
* Grid gets its data from a {@link DataSource}, providing row objects to Grid
* from a user-defined endpoint. It can be either a local in-memory data source
* (e.g. {@link ListDataSource}) or even a remote one, retrieving data from e.g.
* a REST API (see {@link AbstractRemoteDataSource}).
*
*
* @param
* The row type of the grid. The row type is the POJO type from where
* the data is retrieved into the column cells.
* @since 7.4
* @author Vaadin Ltd
*/
public class Grid extends ResizeComposite implements HasSelectionHandlers,
SubPartAware, DeferredWorker, Focusable,
com.google.gwt.user.client.ui.Focusable, HasWidgets, HasEnabled {
private static final String STYLE_NAME = "v-grid";
private static final String SELECT_ALL_CHECKBOX_CLASSNAME = "-select-all-checkbox";
/**
* Abstract base class for Grid header and footer sections.
*
* @since 7.5.0
*
* @param
* the type of the rows in the section
*/
public abstract static class StaticSection> {
/**
* A header or footer cell. Has a simple textual caption.
*
*/
public static class StaticCell {
private Object content = null;
private int colspan = 1;
private StaticSection> section;
private GridStaticCellType type = GridStaticCellType.TEXT;
private String styleName = null;
/**
* Sets the text displayed in this cell.
*
* @param text
* a plain text caption
*/
public void setText(String text) {
content = text;
type = GridStaticCellType.TEXT;
section.requestSectionRefresh();
}
/**
* Returns the text displayed in this cell.
*
* @return the plain text caption
*/
public String getText() {
if (type != GridStaticCellType.TEXT) {
throw new IllegalStateException(
"Cannot fetch Text from a cell with type " + type);
}
return (String) content;
}
protected StaticSection> getSection() {
assert section != null;
return section;
}
protected void setSection(StaticSection> section) {
this.section = section;
}
/**
* Returns the amount of columns the cell spans. By default is 1.
*
* @return The amount of columns the cell spans.
*/
public int getColspan() {
return colspan;
}
/**
* Sets the amount of columns the cell spans. Must be more or equal
* to 1. By default is 1.
*
* @param colspan
* the colspan to set
*/
public void setColspan(int colspan) {
if (colspan < 1) {
throw new IllegalArgumentException(
"Colspan cannot be less than 1");
}
this.colspan = colspan;
section.requestSectionRefresh();
}
/**
* Returns the html inside the cell.
*
* @throws IllegalStateException
* if trying to retrive HTML from a cell with a type
* other than {@link GridStaticCellType#HTML}.
* @return the html content of the cell.
*/
public String getHtml() {
if (type != GridStaticCellType.HTML) {
throw new IllegalStateException(
"Cannot fetch HTML from a cell with type " + type);
}
return (String) content;
}
/**
* Sets the content of the cell to the provided html. All previous
* content is discarded and the cell type is set to
* {@link GridStaticCellType#HTML}.
*
* @param html
* The html content of the cell
*/
public void setHtml(String html) {
content = html;
type = GridStaticCellType.HTML;
section.requestSectionRefresh();
}
/**
* Returns the widget in the cell.
*
* @throws IllegalStateException
* if the cell is not {@link GridStaticCellType#WIDGET}
*
* @return the widget in the cell
*/
public Widget getWidget() {
if (type != GridStaticCellType.WIDGET) {
throw new IllegalStateException(
"Cannot fetch Widget from a cell with type "
+ type);
}
return (Widget) content;
}
/**
* Set widget as the content of the cell. The type of the cell
* becomes {@link GridStaticCellType#WIDGET}. All previous content
* is discarded.
*
* @param widget
* The widget to add to the cell. Should not be
* previously attached anywhere (widget.getParent ==
* null).
*/
public void setWidget(Widget widget) {
if (content == widget) {
return;
}
if (content instanceof Widget) {
// Old widget in the cell, detach it first
section.getGrid().detachWidget((Widget) content);
}
content = widget;
type = GridStaticCellType.WIDGET;
section.requestSectionRefresh();
}
/**
* Returns the type of the cell.
*
* @return the type of content the cell contains.
*/
public GridStaticCellType getType() {
return type;
}
/**
* Returns the custom style name for this cell.
*
* @return the style name or null if no style name has been set
*/
public String getStyleName() {
return styleName;
}
/**
* Sets a custom style name for this cell.
*
* @param styleName
* the style name to set or null to not use any style
* name
*/
public void setStyleName(String styleName) {
this.styleName = styleName;
section.requestSectionRefresh();
}
/**
* Called when the cell is detached from the row
*
* @since 7.6.3
*/
void detach() {
if (content instanceof Widget) {
// Widget in the cell, detach it
section.getGrid().detachWidget((Widget) content);
}
}
}
/**
* Abstract base class for Grid header and footer rows.
*
* @param
* the type of the cells in the row
*/
public abstract static class StaticRow {
private Map, CELLTYPE> cells = new HashMap, CELLTYPE>();
private StaticSection> section;
/**
* Map from set of spanned columns to cell meta data.
*/
private Map>, CELLTYPE> cellGroups = new HashMap>, CELLTYPE>();
/**
* A custom style name for the row or null if none is set.
*/
private String styleName = null;
/**
* Returns the cell on given GridColumn. If the column is merged
* returned cell is the cell for the whole group.
*
* @param column
* the column in grid
* @return the cell on given column, merged cell for merged columns,
* null if not found
*/
public CELLTYPE getCell(Column, ?> column) {
Set> cellGroup = getCellGroupForColumn(column);
if (cellGroup != null) {
return cellGroups.get(cellGroup);
}
return cells.get(column);
}
/**
* Returns true
if this row contains spanned cells.
*
* @since 7.5.0
* @return does this row contain spanned cells
*/
public boolean hasSpannedCells() {
return !cellGroups.isEmpty();
}
/**
* Merges columns cells in a row.
*
* @param columns
* the columns which header should be merged
* @return the remaining visible cell after the merge, or the cell
* on first column if all are hidden
*/
public CELLTYPE join(Column, ?>... columns) {
if (columns.length <= 1) {
throw new IllegalArgumentException(
"You can't merge less than 2 columns together.");
}
HashSet> columnGroup = new HashSet>();
// NOTE: this doesn't care about hidden columns, those are
// filtered in calculateColspans()
for (Column, ?> column : columns) {
if (!cells.containsKey(column)) {
throw new IllegalArgumentException(
"Given column does not exists on row "
+ column);
} else if (getCellGroupForColumn(column) != null) {
throw new IllegalStateException(
"Column is already in a group.");
}
columnGroup.add(column);
}
CELLTYPE joinedCell = createCell();
cellGroups.put(columnGroup, joinedCell);
joinedCell.setSection(getSection());
calculateColspans();
return joinedCell;
}
/**
* Merges columns cells in a row.
*
* @param cells
* The cells to merge. Must be from the same row.
* @return The remaining visible cell after the merge, or the first
* cell if all columns are hidden
*/
public CELLTYPE join(CELLTYPE... cells) {
if (cells.length <= 1) {
throw new IllegalArgumentException(
"You can't merge less than 2 cells together.");
}
Column, ?>[] columns = new Column, ?>[cells.length];
int j = 0;
for (Column, ?> column : this.cells.keySet()) {
CELLTYPE cell = this.cells.get(column);
if (!this.cells.containsValue(cells[j])) {
throw new IllegalArgumentException(
"Given cell does not exists on row");
} else if (cell.equals(cells[j])) {
columns[j++] = column;
if (j == cells.length) {
break;
}
}
}
return join(columns);
}
private Set> getCellGroupForColumn(
Column, ?> column) {
for (Set> group : cellGroups.keySet()) {
if (group.contains(column)) {
return group;
}
}
return null;
}
void calculateColspans() {
// Reset all cells
for (CELLTYPE cell : this.cells.values()) {
cell.setColspan(1);
}
// Set colspan for grouped cells
for (Set> group : cellGroups.keySet()) {
if (!checkMergedCellIsContinuous(group)) {
// on error simply break the merged cell
cellGroups.get(group).setColspan(1);
} else {
int colSpan = 0;
for (Column, ?> column : group) {
if (!column.isHidden()) {
colSpan++;
}
}
// colspan can't be 0
cellGroups.get(group).setColspan(Math.max(1, colSpan));
}
}
}
private boolean checkMergedCellIsContinuous(
Set> mergedCell) {
// no matter if hidden or not, just check for continuous order
final List> columnOrder = new ArrayList>(
section.grid.getColumns());
if (!columnOrder.containsAll(mergedCell)) {
return false;
}
for (int i = 0; i < columnOrder.size(); ++i) {
if (!mergedCell.contains(columnOrder.get(i))) {
continue;
}
for (int j = 1; j < mergedCell.size(); ++j) {
if (!mergedCell.contains(columnOrder.get(i + j))) {
return false;
}
}
return true;
}
return false;
}
protected void addCell(Column, ?> column) {
CELLTYPE cell = createCell();
cell.setSection(getSection());
cells.put(column, cell);
}
protected void removeCell(Column, ?> column) {
cells.remove(column);
}
protected abstract CELLTYPE createCell();
protected StaticSection> getSection() {
return section;
}
protected void setSection(StaticSection> section) {
this.section = section;
}
/**
* Returns the custom style name for this row.
*
* @return the style name or null if no style name has been set
*/
public String getStyleName() {
return styleName;
}
/**
* Sets a custom style name for this row.
*
* @param styleName
* the style name to set or null to not use any style
* name
*/
public void setStyleName(String styleName) {
this.styleName = styleName;
section.requestSectionRefresh();
}
/**
* Called when the row is detached from the grid
*
* @since 7.6.3
*/
void detach() {
// Avoid calling detach twice for a merged cell
HashSet cells = new HashSet();
for (Column, ?> column : getSection().grid.getColumns()) {
cells.add(getCell(column));
}
for (CELLTYPE cell : cells) {
cell.detach();
}
}
}
private Grid> grid;
private List rows = new ArrayList();
private boolean visible = true;
/**
* Creates and returns a new instance of the row type.
*
* @return the created row
*/
protected abstract ROWTYPE createRow();
/**
* Informs the grid that this section should be re-rendered.
*
* Note that re-render means calling update() on each cell,
* preAttach()/postAttach()/preDetach()/postDetach() is not called as
* the cells are not removed from the DOM.
*/
protected abstract void requestSectionRefresh();
/**
* Sets the visibility of the whole section.
*
* @param visible
* true to show this section, false to hide
*/
public void setVisible(boolean visible) {
this.visible = visible;
requestSectionRefresh();
}
/**
* Returns the visibility of this section.
*
* @return true if visible, false otherwise.
*/
public boolean isVisible() {
return visible;
}
/**
* Inserts a new row at the given position. Shifts the row currently at
* that position and any subsequent rows down (adds one to their
* indices).
*
* @param index
* the position at which to insert the row
* @return the new row
*
* @throws IndexOutOfBoundsException
* if the index is out of bounds
* @see #appendRow()
* @see #prependRow()
* @see #removeRow(int)
* @see #removeRow(StaticRow)
*/
public ROWTYPE addRowAt(int index) {
ROWTYPE row = createRow();
row.setSection(this);
for (int i = 0; i < getGrid().getColumnCount(); ++i) {
row.addCell(grid.getColumn(i));
}
rows.add(index, row);
requestSectionRefresh();
return row;
}
/**
* Adds a new row at the top of this section.
*
* @return the new row
* @see #appendRow()
* @see #addRowAt(int)
* @see #removeRow(int)
* @see #removeRow(StaticRow)
*/
public ROWTYPE prependRow() {
return addRowAt(0);
}
/**
* Adds a new row at the bottom of this section.
*
* @return the new row
* @see #prependRow()
* @see #addRowAt(int)
* @see #removeRow(int)
* @see #removeRow(StaticRow)
*/
public ROWTYPE appendRow() {
return addRowAt(rows.size());
}
/**
* Removes the row at the given position.
*
* @param index
* the position of the row
*
* @throws IndexOutOfBoundsException
* if the index is out of bounds
* @see #addRowAt(int)
* @see #appendRow()
* @see #prependRow()
* @see #removeRow(StaticRow)
*/
public void removeRow(int index) {
ROWTYPE row = rows.remove(index);
row.detach();
requestSectionRefresh();
}
/**
* Removes the given row from the section.
*
* @param row
* the row to be removed
*
* @throws IllegalArgumentException
* if the row does not exist in this section
* @see #addRowAt(int)
* @see #appendRow()
* @see #prependRow()
* @see #removeRow(int)
*/
public void removeRow(ROWTYPE row) {
try {
removeRow(rows.indexOf(row));
} catch (IndexOutOfBoundsException e) {
throw new IllegalArgumentException(
"Section does not contain the given row");
}
}
/**
* Returns the row at the given position.
*
* @param index
* the position of the row
* @return the row with the given index
*
* @throws IndexOutOfBoundsException
* if the index is out of bounds
*/
public ROWTYPE getRow(int index) {
try {
return rows.get(index);
} catch (IndexOutOfBoundsException e) {
throw new IllegalArgumentException(
"Row with index " + index + " does not exist");
}
}
/**
* Returns the number of rows in this section.
*
* @return the number of rows
*/
public int getRowCount() {
return rows.size();
}
protected List getRows() {
return rows;
}
protected int getVisibleRowCount() {
return isVisible() ? getRowCount() : 0;
}
protected void addColumn(Column, ?> column) {
for (ROWTYPE row : rows) {
row.addCell(column);
}
}
protected void removeColumn(Column, ?> column) {
for (ROWTYPE row : rows) {
row.removeCell(column);
}
}
protected void setGrid(Grid> grid) {
this.grid = grid;
}
protected Grid> getGrid() {
assert grid != null;
return grid;
}
protected void updateColSpans() {
for (ROWTYPE row : rows) {
if (row.hasSpannedCells()) {
row.calculateColspans();
}
}
}
}
/**
* Represents the header section of a Grid. A header consists of a single
* header row containing a header cell for each column. Each cell has a
* simple textual caption.
*/
protected static class Header extends StaticSection {
private HeaderRow defaultRow;
private boolean markAsDirty = false;
@Override
public void removeRow(int index) {
HeaderRow removedRow = getRow(index);
super.removeRow(index);
if (removedRow == defaultRow) {
setDefaultRow(null);
}
}
/**
* Sets the default row of this header. The default row is a special
* header row providing a user interface for sorting columns.
*
* @param row
* the new default row, or null for no default row
*
* @throws IllegalArgumentException
* this header does not contain the row
*/
public void setDefaultRow(HeaderRow row) {
if (row == defaultRow) {
return;
}
if (row != null && !getRows().contains(row)) {
throw new IllegalArgumentException(
"Cannot set a default row that does not exist in the container");
}
if (defaultRow != null) {
defaultRow.setDefault(false);
}
if (row != null) {
row.setDefault(true);
}
defaultRow = row;
requestSectionRefresh();
}
/**
* Returns the current default row of this header. The default row is a
* special header row providing a user interface for sorting columns.
*
* @return the default row or null if no default row set
*/
public HeaderRow getDefaultRow() {
return defaultRow;
}
@Override
protected HeaderRow createRow() {
return new HeaderRow();
}
@Override
protected void requestSectionRefresh() {
markAsDirty = true;
/*
* Defer the refresh so if we multiple times call refreshSection()
* (for example when updating cell values) we only get one actual
* refresh in the end.
*/
Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
if (markAsDirty) {
markAsDirty = false;
getGrid().refreshHeader();
}
}
});
}
/**
* Returns the events consumed by the header.
*
* @return a collection of BrowserEvents
*/
public Collection getConsumedEvents() {
return Arrays.asList(BrowserEvents.TOUCHSTART,
BrowserEvents.TOUCHMOVE, BrowserEvents.TOUCHEND,
BrowserEvents.TOUCHCANCEL, BrowserEvents.CLICK);
}
@Override
protected void addColumn(Column, ?> column) {
super.addColumn(column);
// Add default content for new columns.
if (defaultRow != null) {
column.setDefaultHeaderContent(defaultRow.getCell(column));
}
}
}
/**
* A single row in a grid header section.
*
*/
public static class HeaderRow extends StaticSection.StaticRow {
private boolean isDefault = false;
protected void setDefault(boolean isDefault) {
this.isDefault = isDefault;
if (isDefault) {
for (Column, ?> column : getSection().grid.getColumns()) {
column.setDefaultHeaderContent(getCell(column));
}
}
}
public boolean isDefault() {
return isDefault;
}
@Override
protected HeaderCell createCell() {
return new HeaderCell();
}
}
/**
* A single cell in a grid header row. Has a caption and, if it's in a
* default row, a drag handle.
*/
public static class HeaderCell extends StaticSection.StaticCell {
}
/**
* Represents the footer section of a Grid. The footer is always empty.
*/
protected static class Footer extends StaticSection {
private boolean markAsDirty = false;
@Override
protected FooterRow createRow() {
return new FooterRow();
}
@Override
protected void requestSectionRefresh() {
markAsDirty = true;
/*
* Defer the refresh so if we multiple times call refreshSection()
* (for example when updating cell values) we only get one actual
* refresh in the end.
*/
Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
if (markAsDirty) {
markAsDirty = false;
getGrid().refreshFooter();
}
}
});
}
}
/**
* A single cell in a grid Footer row. Has a textual caption.
*
*/
public static class FooterCell extends StaticSection.StaticCell {
}
/**
* A single row in a grid Footer section.
*
*/
public static class FooterRow extends StaticSection.StaticRow {
@Override
protected FooterCell createCell() {
return new FooterCell();
}
}
private static class EditorRequestImpl implements EditorRequest {
/**
* A callback interface used to notify the invoker of the editor handler
* of completed editor requests.
*
* @param
* the row data type
*/
public static interface RequestCallback {
/**
* The method that must be called when the request has been
* processed correctly.
*
* @param request
* the original request object
*/
public void onSuccess(EditorRequest request);
/**
* The method that must be called when processing the request has
* produced an aborting error.
*
* @param request
* the original request object
*/
public void onError(EditorRequest request);
}
private Grid grid;
private final int rowIndex;
private final int columnIndexDOM;
private RequestCallback callback;
private boolean completed = false;
public EditorRequestImpl(Grid grid, int rowIndex, int columnIndexDOM,
RequestCallback callback) {
this.grid = grid;
this.rowIndex = rowIndex;
this.columnIndexDOM = columnIndexDOM;
this.callback = callback;
}
@Override
public int getRowIndex() {
return rowIndex;
}
@Override
public int getColumnIndex() {
return columnIndexDOM;
}
@Override
public T getRow() {
return grid.getDataSource().getRow(rowIndex);
}
@Override
public Grid getGrid() {
return grid;
}
@Override
public Widget getWidget(Grid.Column, T> column) {
Widget w = grid.getEditorWidget(column);
assert w != null;
return w;
}
private void complete(String errorMessage,
Collection> errorColumns) {
if (completed) {
throw new IllegalStateException(
"An EditorRequest must be completed exactly once");
}
completed = true;
if (errorColumns == null) {
errorColumns = Collections.emptySet();
}
grid.getEditor().setEditorError(errorMessage, errorColumns);
}
@Override
public void success() {
complete(null, null);
if (callback != null) {
callback.onSuccess(this);
}
}
@Override
public void failure(String errorMessage,
Collection> errorColumns) {
complete(errorMessage, errorColumns);
if (callback != null) {
callback.onError(this);
}
}
@Override
public boolean isCompleted() {
return completed;
}
}
/**
* A wrapper for native DOM events originating from Grid. In addition to the
* native event, contains a {@link CellReference} instance specifying which
* cell the event originated from.
*
* @since 7.6
* @param
* The row type of the grid
*/
public static class GridEvent {
private Event event;
private EventCellReference cell;
private boolean handled = false;
protected GridEvent(Event event, EventCellReference cell) {
this.event = event;
this.cell = cell;
}
/**
* Returns the wrapped DOM event.
*
* @return the DOM event
*/
public Event getDomEvent() {
return event;
}
/**
* Returns the Grid cell this event originated from.
*
* @return the event cell
*/
public EventCellReference getCell() {
return cell;
}
/**
* Returns the Grid instance this event originated from.
*
* @return the grid
*/
public Grid getGrid() {
return cell.getGrid();
}
/**
* Check whether this event has already been marked as handled.
*
* @return whether this event has already been marked as handled
*/
public boolean isHandled() {
return handled;
}
/**
* Set the status of this event. Setting to {@code true} effectively
* marks this event as having already been handled.
*
* @param handled
*/
public void setHandled(boolean handled) {
this.handled = handled;
}
}
/**
* A wrapper for native DOM events related to the {@link Editor Grid editor}
* .
*
* @since 7.6
* @param
* the row type of the grid
*/
public static class EditorDomEvent extends GridEvent {
private final Widget editorWidget;
protected EditorDomEvent(Event event, EventCellReference cell,
Widget editorWidget) {
super(event, cell);
this.editorWidget = editorWidget;
}
/**
* Returns the editor of the Grid this event originated from.
*
* @return the related editor instance
*/
public Editor getEditor() {
return getGrid().getEditor();
}
/**
* Returns the currently focused editor widget.
*
* @return the focused editor widget or {@code null} if not editable
*/
public Widget getEditorWidget() {
return editorWidget;
}
/**
* Returns the row index the editor is open at. If the editor is not
* open, returns -1.
*
* @return the index of the edited row or -1 if editor is not open
*/
public int getRowIndex() {
return getEditor().rowIndex;
}
/**
* Returns the DOM column index (excluding hidden columns) the editor
* was opened at. If the editor is not open, returns -1.
*
* @return the column index or -1 if editor is not open
*/
public int getFocusedColumnIndex() {
return getEditor().focusedColumnIndexDOM;
}
}
/**
* An editor UI for Grid rows. A single Grid row at a time can be opened for
* editing.
*
* @since 7.6
* @param
* the row type of the grid
*/
public static class Editor implements DeferredWorker {
public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER;
public static final int KEYCODE_HIDE = KeyCodes.KEY_ESCAPE;
private static final String ERROR_CLASS_NAME = "error";
private static final String NOT_EDITABLE_CLASS_NAME = "not-editable";
ScheduledCommand fieldFocusCommand = new ScheduledCommand() {
private int count = 0;
@Override
public void execute() {
Element focusedElement = WidgetUtil.getFocusedElement();
if (focusedElement == grid.getElement()
|| focusedElement == Document.get().getBody()
|| count > 2) {
focusColumn(focusedColumnIndexDOM);
} else {
++count;
Scheduler.get().scheduleDeferred(this);
}
}
};
/**
* A handler for events related to the Grid editor. Responsible for
* opening, moving or closing the editor based on the received event.
*
* @since 7.6
* @author Vaadin Ltd
* @param
* the row type of the grid
*/
public interface EventHandler {
/**
* Handles editor-related events in an appropriate way. Opens,
* moves, or closes the editor based on the given event.
*
* @param event
* the received event
* @return true if the event was handled and nothing else should be
* done, false otherwise
*/
boolean handleEvent(EditorDomEvent event);
}
protected enum State {
INACTIVE, ACTIVATING, BINDING, ACTIVE, SAVING
}
private Grid grid;
private EditorHandler handler;
private EventHandler eventHandler = GWT
.create(DefaultEditorEventHandler.class);
private DivElement editorOverlay = DivElement.as(DOM.createDiv());
private DivElement cellWrapper = DivElement.as(DOM.createDiv());
private DivElement frozenCellWrapper = DivElement.as(DOM.createDiv());
private DivElement messageAndButtonsWrapper = DivElement
.as(DOM.createDiv());
private DivElement messageWrapper = DivElement.as(DOM.createDiv());
private DivElement buttonsWrapper = DivElement.as(DOM.createDiv());
// Element which contains the error message for the editor
// Should only be added to the DOM when there's a message to show
private DivElement message = DivElement.as(DOM.createDiv());
private Map, Widget> columnToWidget = new HashMap, Widget>();
private List focusHandlers = new ArrayList();
private boolean enabled = false;
private State state = State.INACTIVE;
private int rowIndex = -1;
private int focusedColumnIndexDOM = -1;
private String styleName = null;
private HandlerRegistration hScrollHandler;
private HandlerRegistration vScrollHandler;
private final Button saveButton;
private final Button cancelButton;
private static final int SAVE_TIMEOUT_MS = 5000;
private final Timer saveTimeout = new Timer() {
@Override
public void run() {
getLogger().warning(
"Editor save action is taking longer than expected ("
+ SAVE_TIMEOUT_MS + "ms). Does your "
+ EditorHandler.class.getSimpleName()
+ " remember to call success() or fail()?");
}
};
private final EditorRequestImpl.RequestCallback saveRequestCallback = new EditorRequestImpl.RequestCallback() {
@Override
public void onSuccess(EditorRequest request) {
if (state == State.SAVING) {
cleanup();
cancel();
grid.clearSortOrder();
}
}
@Override
public void onError(EditorRequest request) {
if (state == State.SAVING) {
cleanup();
}
}
private void cleanup() {
state = State.ACTIVE;
setButtonsEnabled(true);
saveTimeout.cancel();
}
};
private static final int BIND_TIMEOUT_MS = 5000;
private final Timer bindTimeout = new Timer() {
@Override
public void run() {
getLogger().warning(
"Editor bind action is taking longer than expected ("
+ BIND_TIMEOUT_MS + "ms). Does your "
+ EditorHandler.class.getSimpleName()
+ " remember to call success() or fail()?");
}
};
private final EditorRequestImpl.RequestCallback bindRequestCallback = new EditorRequestImpl.RequestCallback() {
@Override
public void onSuccess(EditorRequest request) {
if (state == State.BINDING) {
state = State.ACTIVE;
bindTimeout.cancel();
rowIndex = request.getRowIndex();
focusedColumnIndexDOM = request.getColumnIndex();
if (focusedColumnIndexDOM >= 0) {
// Update internal focus of Grid
grid.focusCell(rowIndex, focusedColumnIndexDOM);
}
showOverlay();
}
}
@Override
public void onError(EditorRequest request) {
if (state == State.BINDING) {
if (rowIndex == -1) {
doCancel();
} else {
state = State.ACTIVE;
// TODO: Maybe restore focus?
}
bindTimeout.cancel();
}
}
};
/** A set of all the columns that display an error flag. */
private final Set> columnErrors = new HashSet>();
private boolean buffered = true;
/** Original position of editor */
private double originalTop;
/** Original scroll position of grid when editor was opened */
private double originalScrollTop;
private RowHandle pinnedRowHandle;
public Editor() {
saveButton = new Button();
saveButton.setText(GridConstants.DEFAULT_SAVE_CAPTION);
saveButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
save();
}
});
cancelButton = new Button();
cancelButton.setText(GridConstants.DEFAULT_CANCEL_CAPTION);
cancelButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cancel();
}
});
}
public void setEditorError(String errorMessage,
Collection> errorColumns) {
if (errorMessage == null) {
message.removeFromParent();
} else {
message.setInnerText(errorMessage);
if (message.getParentElement() == null) {
messageWrapper.appendChild(message);
}
}
// In unbuffered mode only show message wrapper if there is an error
if (!isBuffered()) {
setMessageAndButtonsWrapperVisible(errorMessage != null);
}
if (state == State.ACTIVE || state == State.SAVING) {
for (Column, T> c : grid.getColumns()) {
grid.getEditor().setEditorColumnError(c,
errorColumns.contains(c));
}
}
}
public int getRow() {
return rowIndex;
}
/**
* If a cell of this Grid had focus once this editRow call was
* triggered, the editor component at the previously focused column
* index will be focused.
*
* If a Grid cell was not focused prior to calling this method, it will
* be equivalent to {@code editRow(rowIndex, -1)}.
*
* @see #editRow(int, int)
*/
public void editRow(int rowIndex) {
// Focus the last focused column in the editor if grid or its child
// was focused before the edit request
Cell focusedCell = grid.cellFocusHandler.getFocusedCell();
Element focusedElement = WidgetUtil.getFocusedElement();
if (focusedCell != null && focusedElement != null
&& grid.getElement().isOrHasChild(focusedElement)) {
editRow(rowIndex, focusedCell.getColumn());
} else {
editRow(rowIndex, -1);
}
}
/**
* Opens the editor over the row with the given index and attempts to
* focus the editor widget in the given column index. Does not move
* focus if the widget is not focusable or if the column index is -1.
*
* @param rowIndex
* the index of the row to be edited
* @param columnIndexDOM
* the column index (excluding hidden columns) of the editor
* widget that should be initially focused or -1 to not set
* focus
*
* @throws IllegalStateException
* if this editor is not enabled
* @throws IllegalStateException
* if this editor is already in edit mode and in buffered
* mode
*
* @since 7.5
*/
public void editRow(final int rowIndex, final int columnIndexDOM) {
if (!enabled) {
throw new IllegalStateException(
"Cannot edit row: editor is not enabled");
}
if (isWorkPending()) {
// Request pending a response, don't move try to start another
// request.
return;
}
if (state != State.INACTIVE && this.rowIndex != rowIndex) {
if (isBuffered()) {
throw new IllegalStateException(
"Cannot edit row: editor already in edit mode");
} else if (!columnErrors.isEmpty()) {
// Don't move row if errors are present
// FIXME: Should attempt bind if error field values have
// changed.
return;
}
}
if (columnIndexDOM >= grid.getVisibleColumns().size()) {
throw new IllegalArgumentException(
"Edited column index " + columnIndexDOM
+ " was bigger than visible column count.");
}
if (this.rowIndex == rowIndex
&& focusedColumnIndexDOM == columnIndexDOM) {
// NO-OP
return;
}
if (this.rowIndex == rowIndex) {
if (focusedColumnIndexDOM != columnIndexDOM) {
if (columnIndexDOM >= grid.getFrozenColumnCount()) {
// Scroll to new focused column.
grid.getEscalator().scrollToColumn(columnIndexDOM,
ScrollDestination.ANY, 0);
}
focusedColumnIndexDOM = columnIndexDOM;
}
updateHorizontalScrollPosition();
// Update Grid internal focus and focus widget if possible
if (focusedColumnIndexDOM >= 0) {
grid.focusCell(rowIndex, focusedColumnIndexDOM);
focusColumn(focusedColumnIndexDOM);
}
// No need to request anything from the editor handler.
return;
}
state = State.ACTIVATING;
final Escalator escalator = grid.getEscalator();
if (escalator.getVisibleRowRange().contains(rowIndex)) {
show(rowIndex, columnIndexDOM);
} else {
vScrollHandler = grid.addScrollHandler(new ScrollHandler() {
@Override
public void onScroll(ScrollEvent event) {
if (escalator.getVisibleRowRange().contains(rowIndex)) {
show(rowIndex, columnIndexDOM);
vScrollHandler.removeHandler();
}
}
});
grid.scrollToRow(rowIndex,
isBuffered() ? ScrollDestination.MIDDLE
: ScrollDestination.ANY);
}
}
/**
* Cancels the currently active edit and hides the editor. Any changes
* that are not {@link #save() saved} are lost.
*
* @throws IllegalStateException
* if this editor is not enabled
* @throws IllegalStateException
* if this editor is not in edit mode
*/
public void cancel() {
if (!enabled) {
throw new IllegalStateException(
"Cannot cancel edit: editor is not enabled");
}
if (state == State.INACTIVE) {
throw new IllegalStateException(
"Cannot cancel edit: editor is not in edit mode");
}
handler.cancel(new EditorRequestImpl(grid, rowIndex,
focusedColumnIndexDOM, null));
doCancel();
}
private void doCancel() {
hideOverlay();
state = State.INACTIVE;
rowIndex = -1;
focusedColumnIndexDOM = -1;
grid.getEscalator().setScrollLocked(Direction.VERTICAL, false);
updateSelectionCheckboxesAsNeeded(true);
}
private void updateSelectionCheckboxesAsNeeded(boolean isEnabled) {
// FIXME: This is too much guessing. Define a better way to do this.
if (grid.selectionColumn != null && grid.selectionColumn
.getRenderer() instanceof MultiSelectionRenderer) {
grid.refreshBody();
CheckBox checkBox = (CheckBox) grid.getDefaultHeaderRow()
.getCell(grid.selectionColumn).getWidget();
checkBox.setEnabled(isEnabled);
}
}
/**
* Saves any unsaved changes to the data source and hides the editor.
*
* @throws IllegalStateException
* if this editor is not enabled
* @throws IllegalStateException
* if this editor is not in edit mode
*/
public void save() {
if (!enabled) {
throw new IllegalStateException(
"Cannot save: editor is not enabled");
}
if (state != State.ACTIVE) {
throw new IllegalStateException(
"Cannot save: editor is not in edit mode");
}
state = State.SAVING;
setButtonsEnabled(false);
saveTimeout.schedule(SAVE_TIMEOUT_MS);
EditorRequest request = new EditorRequestImpl(grid, rowIndex,
focusedColumnIndexDOM, saveRequestCallback);
handler.save(request);
updateSelectionCheckboxesAsNeeded(true);
}
/**
* Returns the handler responsible for binding data and editor widgets
* to this editor.
*
* @return the editor handler or null if not set
*/
public EditorHandler getHandler() {
return handler;
}
/**
* Sets the handler responsible for binding data and editor widgets to
* this editor.
*
* @param rowHandler
* the new editor handler
*
* @throws IllegalStateException
* if this editor is currently in edit mode
*/
public void setHandler(EditorHandler rowHandler) {
if (state != State.INACTIVE) {
throw new IllegalStateException(
"Cannot set EditorHandler: editor is currently in edit mode");
}
handler = rowHandler;
}
public boolean isEnabled() {
return enabled;
}
/**
* Sets the enabled state of this editor.
*
* @param enabled
* true if enabled, false otherwise
*
* @throws IllegalStateException
* if in edit mode and trying to disable
* @throws IllegalStateException
* if the editor handler is not set
*/
public void setEnabled(boolean enabled) {
if (!enabled && state != State.INACTIVE) {
throw new IllegalStateException(
"Cannot disable: editor is in edit mode");
} else if (enabled && getHandler() == null) {
throw new IllegalStateException(
"Cannot enable: EditorHandler not set");
}
this.enabled = enabled;
}
protected void show(int rowIndex, int columnIndex) {
if (state == State.ACTIVATING) {
state = State.BINDING;
bindTimeout.schedule(BIND_TIMEOUT_MS);
EditorRequest request = new EditorRequestImpl(grid,
rowIndex, columnIndex, bindRequestCallback);
handler.bind(request);
grid.getEscalator().setScrollLocked(Direction.VERTICAL,
isBuffered());
updateSelectionCheckboxesAsNeeded(false);
}
}
protected void setGrid(final Grid grid) {
assert grid != null : "Grid cannot be null";
assert this.grid == null : "Can only attach editor to Grid once";
this.grid = grid;
}
protected State getState() {
return state;
}
protected void setState(State state) {
this.state = state;
}
/**
* Returns the editor widget associated with the given column. If the
* editor is not active or the column is not
* {@link Grid.Column#isEditable() editable}, returns null.
*
* @param column
* the column
* @return the widget if the editor is open and the column is editable,
* null otherwise
*/
protected Widget getWidget(Column, T> column) {
return columnToWidget.get(column);
}
/**
* Equivalent to {@code showOverlay()}. The argument is ignored.
*
* @param unused
* ignored argument
*
* @deprecated As of 7.5, use {@link #showOverlay()} instead.
*/
@Deprecated
protected void showOverlay(TableRowElement unused) {
showOverlay();
}
/**
* Opens the editor overlay over the table row indicated by
* {@link #getRow()}.
*
* @since 7.5
*/
protected void showOverlay() {
// Ensure overlay is hidden initially
hideOverlay();
DivElement gridElement = DivElement.as(grid.getElement());
TableRowElement tr = grid.getEscalator().getBody()
.getRowElement(rowIndex);
hScrollHandler = grid.addScrollHandler(new ScrollHandler() {
@Override
public void onScroll(ScrollEvent event) {
updateHorizontalScrollPosition();
updateVerticalScrollPosition();
}
});
gridElement.appendChild(editorOverlay);
editorOverlay.appendChild(frozenCellWrapper);
editorOverlay.appendChild(cellWrapper);
editorOverlay.appendChild(messageAndButtonsWrapper);
updateBufferedStyleName();
int frozenColumns = grid.getVisibleFrozenColumnCount();
double frozenColumnsWidth = 0;
double cellHeight = 0;
for (int i = 0; i < tr.getCells().getLength(); i++) {
Element cell = createCell(tr.getCells().getItem(i));
cellHeight = Math.max(cellHeight,
WidgetUtil.getRequiredHeightBoundingClientRectDouble(
tr.getCells().getItem(i)));
Column, T> column = grid.getVisibleColumn(i);
if (i < frozenColumns) {
frozenCellWrapper.appendChild(cell);
frozenColumnsWidth += WidgetUtil
.getRequiredWidthBoundingClientRectDouble(
tr.getCells().getItem(i));
} else {
cellWrapper.appendChild(cell);
}
if (column.isEditable()) {
Widget editor = getHandler().getWidget(column);
if (editor != null) {
columnToWidget.put(column, editor);
grid.attachWidget(editor, cell);
}
if (i == focusedColumnIndexDOM) {
if (BrowserInfo.get().isIE8()) {
Scheduler.get().scheduleDeferred(fieldFocusCommand);
} else {
focusColumn(focusedColumnIndexDOM);
}
}
} else {
cell.addClassName(NOT_EDITABLE_CLASS_NAME);
cell.addClassName(tr.getCells().getItem(i).getClassName());
// If the focused or frozen stylename is present it should
// not be inherited by the editor cell as it is not useful
// in the editor and would look broken without additional
// style rules. This is a bit of a hack.
cell.removeClassName(grid.cellFocusStyleName);
cell.removeClassName("frozen");
if (column == grid.selectionColumn) {
// Duplicate selection column CheckBox
pinnedRowHandle = grid.getDataSource().getHandle(
grid.getDataSource().getRow(rowIndex));
pinnedRowHandle.pin();
// We need to duplicate the selection CheckBox for the
// editor overlay since the original one is hidden by
// the overlay
final CheckBox checkBox = GWT.create(CheckBox.class);
checkBox.setValue(
grid.isSelected(pinnedRowHandle.getRow()));
checkBox.sinkEvents(Event.ONCLICK);
checkBox.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (!grid.isUserSelectionAllowed()) {
return;
}
T row = pinnedRowHandle.getRow();
if (grid.isSelected(row)) {
grid.deselect(row);
} else {
grid.select(row);
}
}
});
grid.attachWidget(checkBox, cell);
columnToWidget.put(column, checkBox);
// Only enable CheckBox in non-buffered mode
checkBox.setEnabled(!isBuffered());
} else if (!(column
.getRenderer() instanceof WidgetRenderer)) {
// Copy non-widget content directly
cell.setInnerHTML(
tr.getCells().getItem(i).getInnerHTML());
}
}
}
setBounds(frozenCellWrapper, 0, 0, frozenColumnsWidth, 0);
setBounds(cellWrapper, frozenColumnsWidth, 0,
tr.getOffsetWidth() - frozenColumnsWidth, cellHeight);
// Only add these elements once
if (!messageAndButtonsWrapper.isOrHasChild(messageWrapper)) {
messageAndButtonsWrapper.appendChild(messageWrapper);
messageAndButtonsWrapper.appendChild(buttonsWrapper);
}
if (isBuffered()) {
grid.attachWidget(saveButton, buttonsWrapper);
grid.attachWidget(cancelButton, buttonsWrapper);
}
setMessageAndButtonsWrapperVisible(isBuffered());
updateHorizontalScrollPosition();
AbstractRowContainer body = (AbstractRowContainer) grid
.getEscalator().getBody();
double rowTop = body.getRowTop(tr);
int bodyTop = body.getElement().getAbsoluteTop();
int gridTop = gridElement.getAbsoluteTop();
double overlayTop = rowTop + bodyTop - gridTop;
originalScrollTop = grid.getScrollTop();
if (!isBuffered() || buttonsShouldBeRenderedBelow(tr)) {
// Default case, editor buttons are below the edited row
editorOverlay.getStyle().setTop(overlayTop, Unit.PX);
originalTop = overlayTop;
editorOverlay.getStyle().clearBottom();
} else {
// Move message and buttons wrapper on top of cell wrapper if
// there is not enough space visible space under and fix the
// overlay from the bottom
editorOverlay.insertFirst(messageAndButtonsWrapper);
int gridHeight = grid.getElement().getOffsetHeight();
editorOverlay.getStyle().setBottom(
gridHeight - overlayTop - tr.getOffsetHeight(),
Unit.PX);
editorOverlay.getStyle().clearTop();
}
// Do not render over the vertical scrollbar
editorOverlay.getStyle().setWidth(grid.escalator.getInnerWidth(),
Unit.PX);
}
private void focusColumn(int columnIndexDOM) {
if (columnIndexDOM < 0
|| columnIndexDOM >= grid.getVisibleColumns().size()) {
// NO-OP
return;
}
Widget editor = getWidget(grid.getVisibleColumn(columnIndexDOM));
if (editor instanceof Focusable) {
((Focusable) editor).focus();
} else if (editor instanceof com.google.gwt.user.client.ui.Focusable) {
((com.google.gwt.user.client.ui.Focusable) editor)
.setFocus(true);
} else {
grid.focus();
}
}
private boolean buttonsShouldBeRenderedBelow(TableRowElement tr) {
TableSectionElement tfoot = grid.escalator.getFooter().getElement();
double tfootPageTop = WidgetUtil.getBoundingClientRect(tfoot)
.getTop();
double trPageBottom = WidgetUtil.getBoundingClientRect(tr)
.getBottom();
int messageAndButtonsHeight = messageAndButtonsWrapper
.getOffsetHeight();
double bottomOfButtons = trPageBottom + messageAndButtonsHeight;
return bottomOfButtons < tfootPageTop;
}
protected void hideOverlay() {
if (editorOverlay.getParentElement() == null) {
return;
}
if (pinnedRowHandle != null) {
pinnedRowHandle.unpin();
pinnedRowHandle = null;
}
for (HandlerRegistration r : focusHandlers) {
r.removeHandler();
}
focusHandlers.clear();
for (Widget w : columnToWidget.values()) {
setParent(w, null);
}
columnToWidget.clear();
if (isBuffered()) {
grid.detachWidget(saveButton);
grid.detachWidget(cancelButton);
}
editorOverlay.removeAllChildren();
cellWrapper.removeAllChildren();
frozenCellWrapper.removeAllChildren();
editorOverlay.removeFromParent();
hScrollHandler.removeHandler();
clearEditorColumnErrors();
}
private void updateBufferedStyleName() {
if (isBuffered()) {
editorOverlay.removeClassName("unbuffered");
editorOverlay.addClassName("buffered");
} else {
editorOverlay.removeClassName("buffered");
editorOverlay.addClassName("unbuffered");
}
}
protected void setStylePrimaryName(String primaryName) {
if (styleName != null) {
editorOverlay.removeClassName(styleName);
cellWrapper.removeClassName(styleName + "-cells");
frozenCellWrapper.removeClassName(styleName + "-cells");
messageAndButtonsWrapper.removeClassName(styleName + "-footer");
messageWrapper.removeClassName(styleName + "-message");
buttonsWrapper.removeClassName(styleName + "-buttons");
saveButton.removeStyleName(styleName + "-save");
cancelButton.removeStyleName(styleName + "-cancel");
}
styleName = primaryName + "-editor";
editorOverlay.setClassName(styleName);
cellWrapper.setClassName(styleName + "-cells");
frozenCellWrapper.setClassName(styleName + "-cells frozen");
messageAndButtonsWrapper.setClassName(styleName + "-footer");
messageWrapper.setClassName(styleName + "-message");
buttonsWrapper.setClassName(styleName + "-buttons");
saveButton.setStyleName(styleName + "-save");
cancelButton.setStyleName(styleName + "-cancel");
}
/**
* Creates an editor cell corresponding to the given table cell. The
* returned element is empty and has the same dimensions and position as
* the table cell.
*
* @param td
* the table cell used as a reference
* @return an editor cell corresponding to the given cell
*/
protected Element createCell(TableCellElement td) {
DivElement cell = DivElement.as(DOM.createDiv());
double width = WidgetUtil
.getRequiredWidthBoundingClientRectDouble(td);
double height = WidgetUtil
.getRequiredHeightBoundingClientRectDouble(td);
setBounds(cell, td.getOffsetLeft(), td.getOffsetTop(), width,
height);
return cell;
}
private static void setBounds(Element e, double left, double top,
double width, double height) {
Style style = e.getStyle();
style.setLeft(left, Unit.PX);
style.setTop(top, Unit.PX);
style.setWidth(width, Unit.PX);
style.setHeight(height, Unit.PX);
}
private void updateHorizontalScrollPosition() {
double scrollLeft = grid.getScrollLeft();
int frozenWidth = frozenCellWrapper.getOffsetWidth();
double newLeft = frozenWidth - scrollLeft;
cellWrapper.getStyle().setLeft(newLeft, Unit.PX);
// sometimes focus handling twists the editor row out of alignment
// with the grid itself and the position needs to be compensated for
try {
TableRowElement rowElement = grid.getEscalator().getBody()
.getRowElement(grid.getEditor().getRow());
int rowLeft = rowElement.getAbsoluteLeft();
int editorLeft = cellWrapper.getAbsoluteLeft();
if (editorLeft != rowLeft + frozenWidth) {
cellWrapper.getStyle()
.setLeft(newLeft + rowLeft - editorLeft, Unit.PX);
}
} catch (IllegalStateException e) {
// IllegalStateException may occur if user has scrolled Grid so
// that Escalator has updated, and row under Editor is no longer
// there
}
}
/**
* Moves the editor overlay on scroll so that it stays on top of the
* edited row. This will also snap the editor to top or bottom of the
* row container if the edited row is scrolled out of the visible area.
*/
private void updateVerticalScrollPosition() {
if (isBuffered()) {
return;
}
double newScrollTop = grid.getScrollTop();
int gridTop = grid.getElement().getAbsoluteTop();
int editorHeight = editorOverlay.getOffsetHeight();
Escalator escalator = grid.getEscalator();
TableSectionElement header = escalator.getHeader().getElement();
int footerTop = escalator.getFooter().getElement().getAbsoluteTop();
int headerBottom = header.getAbsoluteBottom();
double newTop = originalTop - (newScrollTop - originalScrollTop);
if (newTop + gridTop < headerBottom) {
// Snap editor to top of the row container
newTop = header.getOffsetHeight();
} else if (newTop + gridTop > footerTop - editorHeight) {
// Snap editor to the bottom of the row container
newTop = footerTop - editorHeight - gridTop;
}
editorOverlay.getStyle().setTop(newTop, Unit.PX);
}
protected void setGridEnabled(boolean enabled) {
// TODO: This should be informed to handler as well so possible
// fields can be disabled.
setButtonsEnabled(enabled);
}
private void setButtonsEnabled(boolean enabled) {
saveButton.setEnabled(enabled);
cancelButton.setEnabled(enabled);
}
public void setSaveCaption(String saveCaption)
throws IllegalArgumentException {
if (saveCaption == null) {
throw new IllegalArgumentException(
"Save caption cannot be null");
}
saveButton.setText(saveCaption);
}
public String getSaveCaption() {
return saveButton.getText();
}
public void setCancelCaption(String cancelCaption)
throws IllegalArgumentException {
if (cancelCaption == null) {
throw new IllegalArgumentException(
"Cancel caption cannot be null");
}
cancelButton.setText(cancelCaption);
}
public String getCancelCaption() {
return cancelButton.getText();
}
public void setEditorColumnError(Column, T> column,
boolean hasError) {
if (state != State.ACTIVE && state != State.SAVING) {
throw new IllegalStateException("Cannot set cell error "
+ "status: editor is neither active nor saving.");
}
if (isEditorColumnError(column) == hasError) {
return;
}
Element editorCell = getWidget(column).getElement()
.getParentElement();
if (hasError) {
editorCell.addClassName(ERROR_CLASS_NAME);
columnErrors.add(column);
} else {
editorCell.removeClassName(ERROR_CLASS_NAME);
columnErrors.remove(column);
}
}
public void clearEditorColumnErrors() {
/*
* editorOverlay has no children if it's not active, effectively
* making this loop a NOOP.
*/
Element e = editorOverlay.getFirstChildElement();
while (e != null) {
e.removeClassName(ERROR_CLASS_NAME);
e = e.getNextSiblingElement();
}
columnErrors.clear();
}
public boolean isEditorColumnError(Column, T> column) {
return columnErrors.contains(column);
}
public void setBuffered(boolean buffered) {
this.buffered = buffered;
setMessageAndButtonsWrapperVisible(buffered);
}
public boolean isBuffered() {
return buffered;
}
private void setMessageAndButtonsWrapperVisible(boolean visible) {
if (visible) {
messageAndButtonsWrapper.getStyle().clearDisplay();
} else {
messageAndButtonsWrapper.getStyle().setDisplay(Display.NONE);
}
}
/**
* Sets the event handler for this Editor.
*
* @since 7.6
* @param handler
* the new event handler
*/
public void setEventHandler(EventHandler handler) {
eventHandler = handler;
}
/**
* Returns the event handler of this Editor.
*
* @since 7.6
* @return the current event handler
*/
public EventHandler getEventHandler() {
return eventHandler;
}
@Override
public boolean isWorkPending() {
return saveTimeout.isRunning() || bindTimeout.isRunning();
}
protected int getElementColumn(Element e) {
int frozenCells = frozenCellWrapper.getChildCount();
if (frozenCellWrapper.isOrHasChild(e)) {
for (int i = 0; i < frozenCells; ++i) {
if (frozenCellWrapper.getChild(i).isOrHasChild(e)) {
return i;
}
}
}
if (cellWrapper.isOrHasChild(e)) {
for (int i = 0; i < cellWrapper.getChildCount(); ++i) {
if (cellWrapper.getChild(i).isOrHasChild(e)) {
return i + frozenCells;
}
}
}
return -1;
}
}
public abstract static class AbstractGridKeyEvent
extends KeyEvent {
/**
* @since 7.7.9
*/
public AbstractGridKeyEvent() {
}
/**
* @deprecated This constructor's arguments are no longer used. Use the
* no-args constructor instead.
*/
@Deprecated
public AbstractGridKeyEvent(Grid> grid, CellReference> targetCell) {
}
protected abstract String getBrowserEventType();
/**
* Gets the Grid instance for this event, if it originated from a Grid.
*
* @return the grid this event originated from, or {@code null} if this
* event did not originate from a grid
*/
public Grid> getGrid() {
EventTarget target = getNativeEvent().getEventTarget();
if (!Element.is(target)) {
return null;
}
return WidgetUtil.findWidget(Element.as(target), Grid.class, false);
}
/**
* Gets the reference of target cell for this event, if this event
* originated from a Grid.
*
* @return target cell, or {@code null} if this event did not originate
* from a grid
*/
public CellReference> getFocusedCell() {
return getGrid().getEventCell();
}
@Override
protected void dispatch(HANDLER handler) {
EventTarget target = getNativeEvent().getEventTarget();
Grid> grid = getGrid();
if (Element.is(target) && grid != null
&& !grid.isElementInChildWidget(Element.as(target))) {
Section section = Section.FOOTER;
final RowContainer container = grid.cellFocusHandler.containerWithFocus;
if (container == grid.escalator.getHeader()) {
section = Section.HEADER;
} else if (container == getGrid().escalator.getBody()) {
section = Section.BODY;
}
doDispatch(handler, section);
}
}
protected abstract void doDispatch(HANDLER handler, Section section);
}
public abstract static class AbstractGridMouseEvent
extends MouseEvent {
/**
* @since 7.7.9
*/
public AbstractGridMouseEvent() {
}
/**
* @deprecated This constructor's arguments are no longer used. Use the
* no-args constructor instead.
*/
@Deprecated
public AbstractGridMouseEvent(Grid> grid,
CellReference> targetCell) {
}
protected abstract String getBrowserEventType();
/**
* Gets the Grid instance for this event, if it originated from a Grid.
*
* @return the grid this event originated from, or {@code null} if this
* event did not originate from a grid
*/
public Grid> getGrid() {
EventTarget target = getNativeEvent().getEventTarget();
if (!Element.is(target)) {
return null;
}
return WidgetUtil.findWidget(Element.as(target), Grid.class, false);
}
/**
* Gets the reference of target cell for this event, if this event
* originated from a Grid.
*
* @return target cell, or {@code null} if this event did not originate
* from a grid
*/
public CellReference> getTargetCell() {
Grid> grid = getGrid();
if (grid == null) {
return null;
}
return grid.getEventCell();
}
@Override
protected void dispatch(HANDLER handler) {
EventTarget target = getNativeEvent().getEventTarget();
if (!Element.is(target)) {
// Target is not an element
return;
}
Grid> grid = getGrid();
if (grid == null) {
// Target is not an element of a grid
return;
}
Element targetElement = Element.as(target);
if (grid.isElementInChildWidget(targetElement)) {
// Target is some widget inside of Grid
return;
}
final RowContainer container = grid.escalator
.findRowContainer(targetElement);
if (container == null) {
// No container for given element
return;
}
Section section = Section.FOOTER;
if (container == grid.escalator.getHeader()) {
section = Section.HEADER;
} else if (container == grid.escalator.getBody()) {
section = Section.BODY;
}
doDispatch(handler, section);
}
protected abstract void doDispatch(HANDLER handler, Section section);
}
private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle";
/**
* An initial height that is given to new details rows before rendering the
* appropriate widget that we then can be measure
*
* @see GridSpacerUpdater
*/
private static final double DETAILS_ROW_INITIAL_HEIGHT = 50;
private EventCellReference eventCell = new EventCellReference(this);
private class CellFocusHandler {
private RowContainer containerWithFocus = escalator.getBody();
private int rowWithFocus = 0;
private Range cellFocusRange = Range.withLength(0, 1);
private int lastFocusedBodyRow = 0;
private int lastFocusedHeaderRow = 0;
private int lastFocusedFooterRow = 0;
private TableCellElement cellWithFocusStyle = null;
private TableRowElement rowWithFocusStyle = null;
public CellFocusHandler() {
sinkEvents(getNavigationEvents());
}
private Cell getFocusedCell() {
return new Cell(rowWithFocus, cellFocusRange.getStart(),
cellWithFocusStyle);
}
/**
* Sets style names for given cell when needed.
*/
public void updateFocusedCellStyle(FlyweightCell cell,
RowContainer cellContainer) {
int cellRow = cell.getRow();
int cellColumn = cell.getColumn();
int colSpan = cell.getColSpan();
boolean columnHasFocus = Range.withLength(cellColumn, colSpan)
.intersects(cellFocusRange);
if (cellContainer == containerWithFocus) {
// Cell is in the current container
if (cellRow == rowWithFocus && columnHasFocus) {
if (cellWithFocusStyle != cell.getElement()) {
// Cell is correct but it does not have focused style
if (cellWithFocusStyle != null) {
// Remove old focus style
setStyleName(cellWithFocusStyle, cellFocusStyleName,
false);
}
cellWithFocusStyle = cell.getElement();
// Add focus style to correct cell.
setStyleName(cellWithFocusStyle, cellFocusStyleName,
true);
}
} else if (cellWithFocusStyle == cell.getElement()) {
// Due to escalator reusing cells, a new cell has the same
// element but is not the focused cell.
setStyleName(cellWithFocusStyle, cellFocusStyleName, false);
cellWithFocusStyle = null;
}
}
}
/**
* Sets focus style for the given row if needed.
*
* @param row
* a row object
*/
public void updateFocusedRowStyle(Row row) {
if (rowWithFocus == row.getRow()
&& containerWithFocus == escalator.getBody()) {
if (row.getElement() != rowWithFocusStyle) {
// Row should have focus style but does not have it.
if (rowWithFocusStyle != null) {
setStyleName(rowWithFocusStyle, rowFocusStyleName,
false);
}
rowWithFocusStyle = row.getElement();
setStyleName(rowWithFocusStyle, rowFocusStyleName, true);
}
} else if (rowWithFocusStyle == row.getElement()
|| containerWithFocus != escalator.getBody()
&& rowWithFocusStyle != null) {
// Remove focus style.
setStyleName(rowWithFocusStyle, rowFocusStyleName, false);
rowWithFocusStyle = null;
}
}
/**
* Sets the currently focused.
*
* NOTE: the column index is the index in DOM, not the logical
* column index which includes hidden columns.
*
* @param rowIndex
* the index of the row having focus
* @param columnIndexDOM
* the index of the cell having focus
* @param container
* the row container having focus
*/
private void setCellFocus(int rowIndex, int columnIndexDOM,
RowContainer container) {
if (container == null || rowIndex == rowWithFocus
&& cellFocusRange.contains(columnIndexDOM)
&& container == this.containerWithFocus) {
return;
}
int oldRow = rowWithFocus;
rowWithFocus = rowIndex;
Range oldRange = cellFocusRange;
if (container == escalator.getBody()) {
scrollToRow(rowWithFocus);
cellFocusRange = Range.withLength(columnIndexDOM, 1);
} else {
int i = 0;
Element cell = container.getRowElement(rowWithFocus)
.getFirstChildElement();
do {
int colSpan = cell
.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
Range cellRange = Range.withLength(i, colSpan);
if (cellRange.contains(columnIndexDOM)) {
cellFocusRange = cellRange;
break;
}
cell = cell.getNextSiblingElement();
++i;
} while (cell != null);
}
if (columnIndexDOM >= escalator.getColumnConfiguration()
.getFrozenColumnCount()) {
escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY,
10);
}
if (this.containerWithFocus == container) {
if (oldRange.equals(cellFocusRange) && oldRow != rowWithFocus) {
refreshRow(oldRow);
} else {
refreshHeader();
refreshFooter();
}
} else {
RowContainer oldContainer = this.containerWithFocus;
this.containerWithFocus = container;
if (oldContainer == escalator.getBody()) {
lastFocusedBodyRow = oldRow;
} else if (oldContainer == escalator.getHeader()) {
lastFocusedHeaderRow = oldRow;
} else {
lastFocusedFooterRow = oldRow;
}
if (!oldRange.equals(cellFocusRange)) {
refreshHeader();
refreshFooter();
if (oldContainer == escalator.getBody()) {
oldContainer.refreshRows(oldRow, 1);
}
} else {
oldContainer.refreshRows(oldRow, 1);
}
}
refreshRow(rowWithFocus);
}
/**
* Sets focus on a cell.
*
*
* Note: cell focus is not the same as JavaScript's
* {@code document.activeElement}.
*
* @param cell
* a cell object
*/
public void setCellFocus(CellReference cell) {
setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(),
escalator.findRowContainer(cell.getElement()));
}
/**
* Gets list of events that can be used for cell focusing.
*
* @return list of navigation related event types
*/
public Collection getNavigationEvents() {
return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK);
}
/**
* Handle events that can move the cell focus.
*/
public void handleNavigationEvent(Event event, CellReference cell) {
if (event.getType().equals(BrowserEvents.CLICK)) {
setCellFocus(cell);
// Grid should have focus when clicked.
getElement().focus();
} else if (event.getType().equals(BrowserEvents.KEYDOWN)) {
int newRow = rowWithFocus;
RowContainer newContainer = containerWithFocus;
int newColumn = cellFocusRange.getStart();
switch (event.getKeyCode()) {
case KeyCodes.KEY_DOWN:
++newRow;
break;
case KeyCodes.KEY_UP:
--newRow;
break;
case KeyCodes.KEY_RIGHT:
if (cellFocusRange.getEnd() >= getVisibleColumns().size()) {
return;
}
newColumn = cellFocusRange.getEnd();
break;
case KeyCodes.KEY_LEFT:
if (newColumn == 0) {
return;
}
--newColumn;
break;
case KeyCodes.KEY_TAB:
if (event.getShiftKey()) {
newContainer = getPreviousContainer(containerWithFocus);
} else {
newContainer = getNextContainer(containerWithFocus);
}
if (newContainer == containerWithFocus) {
return;
}
break;
case KeyCodes.KEY_HOME:
if (newContainer.getRowCount() > 0) {
newRow = 0;
}
break;
case KeyCodes.KEY_END:
if (newContainer.getRowCount() > 0) {
newRow = newContainer.getRowCount() - 1;
}
break;
case KeyCodes.KEY_PAGEDOWN:
case KeyCodes.KEY_PAGEUP:
if (newContainer.getRowCount() > 0) {
boolean down = event
.getKeyCode() == KeyCodes.KEY_PAGEDOWN;
// If there is a visible focused cell, scroll by one
// page from its position. Otherwise, use the first or
// the last visible row as the scroll start position.
// This avoids jumping when using both keyboard and the
// scroll bar for scrolling.
int firstVisible = getFirstVisibleRowIndex();
int lastVisible = getLastVisibleRowIndex();
if (newRow < firstVisible || newRow > lastVisible) {
newRow = down ? lastVisible : firstVisible;
}
// Scroll by a little less than the visible area to
// account for the possibility that the top and the
// bottom row are only partially visible.
int moveFocusBy = Math.max(1,
lastVisible - firstVisible - 1);
moveFocusBy *= down ? 1 : -1;
newRow += moveFocusBy;
newRow = Math.max(0, Math
.min(newContainer.getRowCount() - 1, newRow));
}
break;
default:
return;
}
if (newContainer != containerWithFocus) {
if (newContainer == escalator.getBody()) {
newRow = lastFocusedBodyRow;
} else if (newContainer == escalator.getHeader()) {
newRow = lastFocusedHeaderRow;
} else {
newRow = lastFocusedFooterRow;
}
} else if (newRow < 0) {
newContainer = getPreviousContainer(newContainer);
if (newContainer == containerWithFocus) {
newRow = 0;
} else if (newContainer == escalator.getBody()) {
newRow = getLastVisibleRowIndex();
} else {
newRow = newContainer.getRowCount() - 1;
}
} else if (newRow >= containerWithFocus.getRowCount()) {
newContainer = getNextContainer(newContainer);
if (newContainer == containerWithFocus) {
newRow = containerWithFocus.getRowCount() - 1;
} else if (newContainer == escalator.getBody()) {
newRow = getFirstVisibleRowIndex();
} else {
newRow = 0;
}
}
if (newContainer.getRowCount() == 0) {
/*
* There are no rows in the container. Can't change the
* focused cell.
*/
return;
}
event.preventDefault();
event.stopPropagation();
setCellFocus(newRow, newColumn, newContainer);
}
}
private RowContainer getPreviousContainer(RowContainer current) {
if (current == escalator.getFooter()) {
current = escalator.getBody();
} else if (current == escalator.getBody()) {
current = escalator.getHeader();
} else {
return current;
}
if (current.getRowCount() == 0) {
return getPreviousContainer(current);
}
return current;
}
private RowContainer getNextContainer(RowContainer current) {
if (current == escalator.getHeader()) {
current = escalator.getBody();
} else if (current == escalator.getBody()) {
current = escalator.getFooter();
} else {
return current;
}
if (current.getRowCount() == 0) {
return getNextContainer(current);
}
return current;
}
private void refreshRow(int row) {
containerWithFocus.refreshRows(row, 1);
}
/**
* Offsets the focused cell's range.
*
* @param offset
* offset for fixing focused cell's range
*/
public void offsetRangeBy(int offset) {
cellFocusRange = cellFocusRange.offsetBy(offset);
}
/**
* Informs {@link CellFocusHandler} that certain range of rows has been
* added to the Grid body. {@link CellFocusHandler} will fix indices
* accordingly.
*
* @param added
* a range of added rows
*/
public void rowsAddedToBody(Range added) {
boolean bodyHasFocus = containerWithFocus == escalator.getBody();
boolean insertionIsAboveFocusedCell = added
.getStart() <= rowWithFocus;
if (bodyHasFocus && insertionIsAboveFocusedCell) {
rowWithFocus += added.length();
rowWithFocus = Math.min(rowWithFocus,
escalator.getBody().getRowCount() - 1);
refreshRow(rowWithFocus);
}
}
/**
* Informs {@link CellFocusHandler} that certain range of rows has been
* removed from the Grid body. {@link CellFocusHandler} will fix indices
* accordingly.
*
* @param removed
* a range of removed rows
*/
public void rowsRemovedFromBody(Range removed) {
if (containerWithFocus != escalator.getBody()) {
return;
} else if (!removed.contains(rowWithFocus)) {
if (removed.getStart() > rowWithFocus) {
return;
}
rowWithFocus = rowWithFocus - removed.length();
} else {
if (containerWithFocus.getRowCount() > removed.getEnd()) {
rowWithFocus = removed.getStart();
} else if (removed.getStart() > 0) {
rowWithFocus = removed.getStart() - 1;
} else {
if (escalator.getHeader().getRowCount() > 0) {
rowWithFocus = Math.min(lastFocusedHeaderRow,
escalator.getHeader().getRowCount() - 1);
containerWithFocus = escalator.getHeader();
} else if (escalator.getFooter().getRowCount() > 0) {
rowWithFocus = Math.min(lastFocusedFooterRow,
escalator.getFooter().getRowCount() - 1);
containerWithFocus = escalator.getFooter();
}
}
}
refreshRow(rowWithFocus);
}
}
private final List> browserEventHandlers = new ArrayList>();
private CellStyleGenerator cellStyleGenerator;
private RowStyleGenerator rowStyleGenerator;
private RowReference rowReference = new RowReference(this);
private CellReference cellReference = new CellReference(rowReference);
private RendererCellReference rendererCellReference = new RendererCellReference(
(RowReference