com.vaadin.client.widgets.Grid Maven / Gradle / Ivy
Show all versions of vaadin-client Show documentation
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.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.Optional;
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.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.DataChangeHandler;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.renderers.ComplexRenderer;
import com.vaadin.client.renderers.Renderer;
import com.vaadin.client.renderers.WidgetRenderer;
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.widget.escalator.Cell;
import com.vaadin.client.widget.escalator.ColumnConfiguration;
import com.vaadin.client.widget.escalator.EscalatorUpdater;
import com.vaadin.client.widget.escalator.FlyweightCell;
import com.vaadin.client.widget.escalator.Row;
import com.vaadin.client.widget.escalator.RowContainer;
import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent;
import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction;
import com.vaadin.client.widget.escalator.Spacer;
import com.vaadin.client.widget.escalator.SpacerUpdater;
import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent;
import com.vaadin.client.widget.escalator.events.RowHeightChangedHandler;
import com.vaadin.client.widget.grid.AutoScroller;
import com.vaadin.client.widget.grid.AutoScroller.AutoScrollerCallback;
import com.vaadin.client.widget.grid.AutoScroller.ScrollAxis;
import com.vaadin.client.widget.grid.CellReference;
import com.vaadin.client.widget.grid.CellStyleGenerator;
import com.vaadin.client.widget.grid.DataAvailableEvent;
import com.vaadin.client.widget.grid.DataAvailableHandler;
import com.vaadin.client.widget.grid.DefaultEditorEventHandler;
import com.vaadin.client.widget.grid.DetailsGenerator;
import com.vaadin.client.widget.grid.EditorHandler;
import com.vaadin.client.widget.grid.EditorHandler.EditorRequest;
import com.vaadin.client.widget.grid.EventCellReference;
import com.vaadin.client.widget.grid.GridEventHandler;
import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
import com.vaadin.client.widget.grid.RendererCellReference;
import com.vaadin.client.widget.grid.RowReference;
import com.vaadin.client.widget.grid.RowStyleGenerator;
import com.vaadin.client.widget.grid.events.AbstractGridKeyEventHandler;
import com.vaadin.client.widget.grid.events.AbstractGridMouseEventHandler;
import com.vaadin.client.widget.grid.events.BodyClickHandler;
import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler;
import com.vaadin.client.widget.grid.events.BodyKeyDownHandler;
import com.vaadin.client.widget.grid.events.BodyKeyPressHandler;
import com.vaadin.client.widget.grid.events.BodyKeyUpHandler;
import com.vaadin.client.widget.grid.events.ColumnReorderEvent;
import com.vaadin.client.widget.grid.events.ColumnReorderHandler;
import com.vaadin.client.widget.grid.events.ColumnResizeEvent;
import com.vaadin.client.widget.grid.events.ColumnResizeHandler;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent;
import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler;
import com.vaadin.client.widget.grid.events.FooterClickHandler;
import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler;
import com.vaadin.client.widget.grid.events.FooterKeyDownHandler;
import com.vaadin.client.widget.grid.events.FooterKeyPressHandler;
import com.vaadin.client.widget.grid.events.FooterKeyUpHandler;
import com.vaadin.client.widget.grid.events.GridClickEvent;
import com.vaadin.client.widget.grid.events.GridDoubleClickEvent;
import com.vaadin.client.widget.grid.events.GridEnabledEvent;
import com.vaadin.client.widget.grid.events.GridEnabledHandler;
import com.vaadin.client.widget.grid.events.GridKeyDownEvent;
import com.vaadin.client.widget.grid.events.GridKeyPressEvent;
import com.vaadin.client.widget.grid.events.GridKeyUpEvent;
import com.vaadin.client.widget.grid.events.GridSelectionAllowedEvent;
import com.vaadin.client.widget.grid.events.GridSelectionAllowedHandler;
import com.vaadin.client.widget.grid.events.HeaderClickHandler;
import com.vaadin.client.widget.grid.events.HeaderDoubleClickHandler;
import com.vaadin.client.widget.grid.events.HeaderKeyDownHandler;
import com.vaadin.client.widget.grid.events.HeaderKeyPressHandler;
import com.vaadin.client.widget.grid.events.HeaderKeyUpHandler;
import com.vaadin.client.widget.grid.events.ScrollEvent;
import com.vaadin.client.widget.grid.events.ScrollHandler;
import com.vaadin.client.widget.grid.events.SelectAllEvent;
import com.vaadin.client.widget.grid.events.SelectAllHandler;
import com.vaadin.client.widget.grid.selection.HasSelectionHandlers;
import com.vaadin.client.widget.grid.selection.MultiSelectionRenderer;
import com.vaadin.client.widget.grid.selection.SelectionEvent;
import com.vaadin.client.widget.grid.selection.SelectionHandler;
import com.vaadin.client.widget.grid.selection.SelectionModel;
import com.vaadin.client.widget.grid.selection.SelectionModelWithSelectionColumn;
import com.vaadin.client.widget.grid.sort.Sort;
import com.vaadin.client.widget.grid.sort.SortEvent;
import com.vaadin.client.widget.grid.sort.SortHandler;
import com.vaadin.client.widget.grid.sort.SortOrder;
import com.vaadin.client.widgets.Escalator.AbstractRowContainer;
import com.vaadin.client.widgets.Escalator.SubPartArguments;
import com.vaadin.client.widgets.Grid.Editor.State;
import com.vaadin.client.widgets.Grid.StaticSection.StaticCell;
import com.vaadin.client.widgets.Grid.StaticSection.StaticRow;
import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.grid.ColumnResizeMode;
import com.vaadin.shared.ui.grid.GridConstants;
import com.vaadin.shared.ui.grid.GridConstants.Section;
import com.vaadin.shared.ui.grid.GridStaticCellType;
import com.vaadin.shared.ui.grid.HeightMode;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.util.SharedUtil;
/**
* 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 com.vaadin.client.renderers.TextRenderer TextRenderer}
* that simply takes in a {@code String} and displays it as the cell's content.
* A more complex renderer might be
* {@link com.vaadin.client.renderers.ProgressBarRenderer 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 com.vaadin.client.widget.grid.datasources.ListDataSource
* ListDataSource}) or even a remote one, retrieving data from e.g. a REST API
* (see {@link com.vaadin.client.data.AbstractRemoteDataSource
* 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) {
this.content = text;
this.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) {
this.content = html;
this.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 (this.content == widget) {
return;
}
if (this.content instanceof Widget) {
// Old widget in the cell, detach it first
section.getGrid().detachWidget((Widget) this.content);
}
this.content = widget;
this.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 (this.content instanceof Widget) {
// Widget in the cell, detach it
section.getGrid().detachWidget((Widget) this.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<>();
private StaticSection> section;
/**
* Map from cell meta data to sets of spanned columns .
*/
private Map>> cellGroups = new HashMap<>();
/**
* 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) {
CELLTYPE cell = getMergedCellForColumn(column);
if (cell != null) {
return cell;
}
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 (getMergedCellForColumn(column) != null) {
throw new IllegalStateException(
"Column is already in a group.");
}
columnGroup.add(column);
}
CELLTYPE joinedCell = createCell();
cellGroups.put(joinedCell, columnGroup);
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 CELLTYPE getMergedCellForColumn(Column, ?> column) {
for (Entry>> entry : cellGroups
.entrySet()) {
if (entry.getValue().contains(column)) {
return entry.getKey();
}
}
return null;
}
void calculateColspans() {
// Reset all cells
for (CELLTYPE cell : this.cells.values()) {
cell.setColspan(1);
}
// Set colspan for grouped cells
for (Entry>> entry : cellGroups
.entrySet()) {
CELLTYPE mergedCell = entry.getKey();
if (!checkMergedCellIsContinuous(entry.getValue())) {
// on error simply break the merged cell
mergedCell.setColspan(1);
} else {
int colSpan = 0;
for (Column, ?> column : entry.getValue()) {
if (!column.isHidden()) {
colSpan++;
}
}
// colspan can't be 0
mergedCell.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 columnIndex;
private RequestCallback callback;
private boolean completed = false;
public EditorRequestImpl(Grid grid, int rowIndex, int columnIndex,
RequestCallback callback) {
this.grid = grid;
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
this.callback = callback;
}
@Override
public int getRowIndex() {
return rowIndex;
}
@Override
public int getColumnIndex() {
return columnIndex;
}
@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() {
complete("", null);
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 column index 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().focusedColumnIndex;
}
}
/**
* 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";
/**
* 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<>();
private List focusHandlers = new ArrayList<>();
private boolean enabled = false;
private State state = State.INACTIVE;
private int rowIndex = -1;
private int focusedColumnIndex = -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(true);
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();
focusedColumnIndex = request.getColumnIndex();
if (focusedColumnIndex >= 0) {
// Update internal focus of Grid
grid.focusCell(rowIndex, focusedColumnIndex);
}
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 iff 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 columnIndex
* the column index 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 columnIndex) {
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 (columnIndex >= grid.getVisibleColumns().size()) {
throw new IllegalArgumentException(
"Edited column index " + columnIndex
+ " was bigger than visible column count.");
}
if (this.rowIndex == rowIndex
&& focusedColumnIndex == columnIndex) {
// NO-OP
return;
}
if (this.rowIndex == rowIndex) {
if (focusedColumnIndex != columnIndex) {
if (columnIndex >= grid.getFrozenColumnCount()) {
// Scroll to new focused column.
grid.getEscalator().scrollToColumn(columnIndex,
ScrollDestination.ANY, 0);
}
focusedColumnIndex = columnIndex;
}
updateHorizontalScrollPosition();
// Update Grid internal focus and focus widget if possible
if (focusedColumnIndex >= 0) {
grid.focusCell(rowIndex, focusedColumnIndex);
focusColumn(focusedColumnIndex);
}
// 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, columnIndex);
} else {
vScrollHandler = grid.addScrollHandler(new ScrollHandler() {
@Override
public void onScroll(ScrollEvent event) {
if (escalator.getVisibleRowRange().contains(rowIndex)) {
show(rowIndex, columnIndex);
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() {
cancel(false);
}
private void cancel(boolean afterSave) {
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,
focusedColumnIndex, null), afterSave);
doCancel();
}
private void doCancel() {
hideOverlay();
state = State.INACTIVE;
rowIndex = -1;
focusedColumnIndex = -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,
focusedColumnIndex, 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 == false && state != State.INACTIVE) {
throw new IllegalStateException(
"Cannot disable: editor is in edit mode");
} else if (enabled == true && 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 == focusedColumnIndex) {
focusColumn(focusedColumnIndex);
}
} 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(event -> {
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 colIndex) {
if (colIndex < 0 || colIndex >= grid.getVisibleColumns().size()) {
// NO-OP
return;
}
Widget editor = getWidget(grid.getVisibleColumn(colIndex));
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();
cellWrapper.getStyle().setLeft(
frozenCellWrapper.getOffsetWidth() - scrollLeft, Unit.PX);
}
/**
* 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 static abstract class AbstractGridKeyEvent
extends KeyEvent {
private Grid> grid;
private final Type associatedType = new Type<>(
getBrowserEventType(), this);
private final CellReference> targetCell;
public AbstractGridKeyEvent(Grid> grid, CellReference> targetCell) {
this.grid = grid;
this.targetCell = targetCell;
}
protected abstract String getBrowserEventType();
/**
* Gets the Grid instance for this event.
*
* @return grid
*/
public Grid> getGrid() {
return grid;
}
/**
* Gets the focused cell for this event.
*
* @return focused cell
*/
public CellReference> getFocusedCell() {
return targetCell;
}
@Override
protected void dispatch(HANDLER handler) {
EventTarget target = getNativeEvent().getEventTarget();
if (Element.is(target)
&& !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 == grid.escalator.getBody()) {
section = Section.BODY;
}
doDispatch(handler, section);
}
}
protected abstract void doDispatch(HANDLER handler, Section section);
@Override
public Type getAssociatedType() {
return associatedType;
}
}
public static abstract class AbstractGridMouseEvent
extends MouseEvent {
private Grid> grid;
private final CellReference> targetCell;
private final Type associatedType = new Type<>(
getBrowserEventType(), this);
public AbstractGridMouseEvent(Grid> grid,
CellReference> targetCell) {
this.grid = grid;
this.targetCell = targetCell;
}
protected abstract String getBrowserEventType();
/**
* Gets the Grid instance for this event.
*
* @return grid
*/
public Grid> getGrid() {
return grid;
}
/**
* Gets the reference of target cell for this event.
*
* @return target cell
*/
public CellReference> getTargetCell() {
return targetCell;
}
@Override
protected void dispatch(HANDLER handler) {
EventTarget target = getNativeEvent().getEventTarget();
if (!Element.is(target)) {
// Target is not an element
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);
@Override
public Type getAssociatedType() {
return associatedType;
}
}
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 Grid.GridSpacerUpdater
*/
private static final double DETAILS_ROW_INITIAL_HEIGHT = 50;
private EventCellReference eventCell = new EventCellReference<>(this);
private GridKeyDownEvent keyDown = new GridKeyDownEvent(this, eventCell);
private GridKeyUpEvent keyUp = new GridKeyUpEvent(this, eventCell);
private GridKeyPressEvent keyPress = new GridKeyPressEvent(this, eventCell);
private GridClickEvent clickEvent = new GridClickEvent(this, eventCell);
private GridDoubleClickEvent doubleClickEvent = new GridDoubleClickEvent(
this, eventCell);
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);
}
int columnIndex = getColumns()
.indexOf(getVisibleColumn(columnIndexDOM));
if (columnIndex >= 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);
}
}
public final class SelectionColumn extends Column
implements GridEnabledHandler, GridSelectionAllowedHandler {
private boolean initDone = false;
private boolean selected = false;
private CheckBox selectAllCheckBox;
private boolean selectAllCheckBoxVisible;
private HeaderCell selectionCell;
SelectionColumn(final Renderer selectColumnRenderer) {
super(selectColumnRenderer);
addEnabledHandler(this);
addSelectionAllowedHandler(this);
}
void initDone() {
setWidth(-1);
setEditable(false);
setResizable(false);
updateEnable();
initDone = true;
}
@Override
protected void setDefaultHeaderContent(HeaderCell selectionCell) {
this.selectionCell = selectionCell;
if (selectAllCheckBox == null) {
// there is no checkbox yet -> create it
selectAllCheckBox = GWT.create(CheckBox.class);
selectAllCheckBox.setStylePrimaryName(
getStylePrimaryName() + SELECT_ALL_CHECKBOX_CLASSNAME);
selectAllCheckBox.addValueChangeHandler(event -> {
selected = event.getValue();
fireEvent(new SelectAllEvent<>(getSelectionModel(),
selected));
});
selectAllCheckBox.setValue(selected);
addHeaderClickHandler(this::onHeaderClickEvent);
// Select all with space when "select all" cell is active
addHeaderKeyUpHandler(this::onHeaderKeyUpEvent);
} else {
// checkbox exists, but default header row has changed -> clear
// rows
for (HeaderRow row : header.getRows()) {
if (row.getCell(this)
.getType() == GridStaticCellType.WIDGET) {
// Detach from old header.
row.getCell(this).setText("");
}
}
}
// attach the checkbox to default row depending on visibility
doSetSelectAllCheckBoxVisible();
}
@Override
public Column setWidth(double pixels) {
if (pixels != getWidth() && initDone) {
throw new UnsupportedOperationException("The selection "
+ "column cannot be modified after init");
} else {
super.setWidth(pixels);
}
return this;
}
@Override
public Boolean getValue(T row) {
return Boolean.valueOf(isSelected(row));
}
@Override
public Column setExpandRatio(int ratio) {
throw new UnsupportedOperationException(
"can't change the expand ratio of the selection column");
}
@Override
public int getExpandRatio() {
return 0;
}
@Override
public Column setMaximumWidth(double pixels) {
throw new UnsupportedOperationException(
"can't change the maximum width of the selection column");
}
@Override
public double getMaximumWidth() {
return -1;
}
@Override
public Column setMinimumWidth(double pixels) {
throw new UnsupportedOperationException(
"can't change the minimum width of the selection column");
}
@Override
public double getMinimumWidth() {
return -1;
}
@Override
public Column setEditable(boolean editable) {
if (initDone) {
throw new UnsupportedOperationException(
"can't set the selection column editable");
}
super.setEditable(editable);
return this;
}
@Override
public void onEnabled(boolean enabled) {
updateEnable();
}
/**
* Sets the select all checkbox visible in the default header row for
* selection column.
*
* @param selectAllCheckBoxVisible
* {@code true} for visible, {@code false} for not
*/
public void setSelectAllCheckBoxVisible(
boolean selectAllCheckBoxVisible) {
if (this.selectAllCheckBoxVisible != selectAllCheckBoxVisible) {
this.selectAllCheckBoxVisible = selectAllCheckBoxVisible;
doSetSelectAllCheckBoxVisible();
}
}
/**
* Returns whether the select all checkbox is visible or not.
*
* @return {@code true} for visible, {@code false} for not
*/
public boolean isSelectAllCheckBoxVisible() {
return selectAllCheckBoxVisible;
}
/**
* Returns the select all checkbox, which is present in the default
* header if the used selection model is of type
* {@link SelectionModelWithSelectionColumn}.
*
* To handle select all, add {@link SelectAllHandler} the grid with
* {@link #addSelectAllHandler(SelectAllHandler)}.
*
* @return the select all checkbox, or an empty optional if not in use
*/
public Optional getSelectAllCheckBox() {
return Optional.ofNullable(selectionColumn == null ? null
: selectionColumn.selectAllCheckBox);
}
/**
* Sets the select all checkbox visible or hidden.
*/
protected void doSetSelectAllCheckBoxVisible() {
assert selectAllCheckBox != null : "Select All Checkbox has not been created for selection column.";
assert selectionCell != null : "Default header cell for selection column not been set.";
if (selectAllCheckBoxVisible) {
selectionCell.setWidget(selectAllCheckBox);
} else {
selectAllCheckBox.removeFromParent();
selectionCell.setText("");
}
}
private void updateEnable() {
if (selectAllCheckBox != null) {
selectAllCheckBox.setEnabled(isEnabled()
&& getSelectionModel().isSelectionAllowed());
}
}
private void onHeaderClickEvent(GridClickEvent event) {
if (selectAllCheckBox.isEnabled()) {
CellReference> targetCell = event.getTargetCell();
int defaultRowIndex = getHeader().getRows()
.indexOf(getDefaultHeaderRow());
if (targetCell.getColumnIndex() == 0
&& targetCell.getRowIndex() == defaultRowIndex) {
selectAllCheckBox.setValue(!selectAllCheckBox.getValue(),
true);
}
}
}
private void onHeaderKeyUpEvent(GridKeyUpEvent event) {
if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE
|| !selectAllCheckBox.isEnabled()) {
return;
}
HeaderRow targetHeaderRow = getHeader()
.getRow(event.getFocusedCell().getRowIndex());
if (!targetHeaderRow.isDefault()) {
return;
}
if (event.getFocusedCell().getColumn() == SelectionColumn.this) {
// Send events to ensure state is updated
selectAllCheckBox.setValue(!selectAllCheckBox.getValue(), true);
}
}
@Override
public void onSelectionAllowed(GridSelectionAllowedEvent event) {
updateEnable();
}
}
/**
* Helper class for performing sorting through the user interface. Controls
* the sort() method, reporting USER as the event originator. This is a
* completely internal class, and is, as such, safe to re-name should a more
* descriptive name come to mind.
*/
private final class UserSorter {
private final Timer timer;
private boolean scheduledMultisort;
private Column, T> column;
private UserSorter() {
timer = new Timer() {
@Override
public void run() {
UserSorter.this.sort(column, scheduledMultisort);
}
};
}
/**
* Toggle sorting for a cell. If the multisort parameter is set to true,
* the cell's sort order is modified as a natural part of a multi-sort
* chain. If false, the sorting order is set to ASCENDING for that
* cell's column. If that column was already the only sorted column in
* the Grid, the sort direction is flipped.
*
* @param cell
* a valid cell reference
* @param multisort
* whether the sort command should act as a multi-sort stack
* or not
*/
public void sort(Column, ?> column, boolean multisort) {
if (!columns.contains(column)) {
throw new IllegalArgumentException(
"Given column is not a column in this grid. "
+ column.toString());
}
if (!column.isSortable()) {
return;
}
final SortOrder so = getSortOrder(column);
if (multisort) {
// If the sort order exists, replace existing value with its
// opposite
if (so != null) {
final int idx = sortOrder.indexOf(so);
sortOrder.set(idx, so.getOpposite());
} else {
// If it doesn't, just add a new sort order to the end of
// the list
sortOrder.add(new SortOrder(column));
}
} else {
// Since we're doing single column sorting, first clear the
// list. Then, if the sort order existed, add its opposite,
// otherwise just add a new sort value
int items = sortOrder.size();
sortOrder.clear();
if (so != null && items == 1) {
sortOrder.add(so.getOpposite());
} else {
sortOrder.add(new SortOrder(column));
}
}
// sortOrder has been changed; tell the Grid to re-sort itself by
// user request.
Grid.this.sort(true);
}
/**
* Perform a sort after a delay.
*
* @param delay
* delay, in milliseconds
*/
public void sortAfterDelay(int delay, boolean multisort) {
column = eventCell.getColumn();
scheduledMultisort = multisort;
timer.schedule(delay);
}
/**
* Check if a delayed sort command has been issued but not yet carried
* out.
*
* @return a boolean value
*/
public boolean isDelayedSortScheduled() {
return timer.isRunning();
}
/**
* Cancel a scheduled sort.
*/
public void cancelDelayedSort() {
timer.cancel();
}
}
/**
* @see Grid#autoColumnWidthsRecalculator
*/
private class AutoColumnWidthsRecalculator {
private double lastCalculatedInnerWidth = -1;
private final ScheduledCommand calculateCommand = new ScheduledCommand() {
@Override
public void execute() {
if (!isScheduled) {
// something cancelled running this.
return;
}
if (header.markAsDirty || footer.markAsDirty) {
if (rescheduleCount < 10) {
/*
* Headers and footers are rendered as finally, this way
* we re-schedule this loop as finally, at the end of
* the queue, so that the headers have a chance to
* render themselves.
*/
Scheduler.get().scheduleFinally(this);
rescheduleCount++;
} else {
/*
* We've tried too many times reschedule finally. Seems
* like something is being deferred. Let the queue
* execute and retry again.
*/
rescheduleCount = 0;
Scheduler.get().scheduleDeferred(this);
}
} else if (currentDataAvailable.isEmpty()
&& dataSource.isWaitingForData()) {
Scheduler.get().scheduleDeferred(this);
} else {
calculate();
}
}
};
private int rescheduleCount = 0;
private boolean isScheduled;
/**
* Calculates and applies column widths, taking into account fixed
* widths and column expand rules
*
* @param immediately
* true
if the widths should be executed
* immediately (ignoring lazy loading completely), or
* false
if the command should be run after a
* while (duplicate non-immediately invocations are ignored).
* @see Column#setWidth(double)
* @see Column#setExpandRatio(int)
* @see Column#setMinimumWidth(double)
* @see Column#setMaximumWidth(double)
*/
public void schedule() {
if (!isScheduled && isAttached()) {
isScheduled = true;
Scheduler.get().scheduleFinally(calculateCommand);
}
}
private void calculate() {
isScheduled = false;
rescheduleCount = 0;
assert !(currentDataAvailable.isEmpty() && dataSource
.isWaitingForData()) : "Trying to calculate column widths without data while data is still being fetched.";
if (columnsAreGuaranteedToBeWiderThanGrid()) {
applyColumnWidths();
} else {
applyColumnWidthsWithExpansion();
}
// Update latest width to prevent recalculate on height change.
lastCalculatedInnerWidth = escalator.getInnerWidth();
}
private boolean columnsAreGuaranteedToBeWiderThanGrid() {
double freeSpace = escalator.getInnerWidth();
for (Column, ?> column : getVisibleColumns()) {
if (column.getWidth() >= 0) {
freeSpace -= column.getWidth();
} else if (column.getMinimumWidth() >= 0) {
freeSpace -= column.getMinimumWidth();
}
}
return freeSpace < 0;
}
@SuppressWarnings("boxing")
private void applyColumnWidths() {
/* Step 1: Apply all column widths as they are. */
Map selfWidths = new LinkedHashMap<>();
List> columns = getVisibleColumns();
for (int index = 0; index < columns.size(); index++) {
selfWidths.put(index, columns.get(index).getWidth());
}
Grid.this.escalator.getColumnConfiguration()
.setColumnWidths(selfWidths);
/*
* Step 2: Make sure that each column ends up obeying their min/max
* width constraints if defined as autowidth. If constraints are
* violated, fix it.
*/
Map constrainedWidths = new LinkedHashMap<>();
for (int index = 0; index < columns.size(); index++) {
Column, T> column = columns.get(index);
boolean hasAutoWidth = column.getWidth() < 0;
if (!hasAutoWidth) {
continue;
}
// TODO: bug: these don't honor the CSS max/min. :(
double actualWidth = column.getWidthActual();
if (actualWidth < getMinWidth(column)) {
constrainedWidths.put(index, column.getMinimumWidth());
} else if (actualWidth > getMaxWidth(column)) {
constrainedWidths.put(index, column.getMaximumWidth());
}
}
Grid.this.escalator.getColumnConfiguration()
.setColumnWidths(constrainedWidths);
}
private void applyColumnWidthsWithExpansion() {
boolean defaultExpandRatios = true;
int totalRatios = 0;
double reservedPixels = 0;
final Set> columnsToExpand = new HashSet<>();
List> nonFixedColumns = new ArrayList<>();
Map columnSizes = new HashMap<>();
final List> visibleColumns = getVisibleColumns();
/*
* Set all fixed widths and also calculate the size-to-fit widths
* for the autocalculated columns.
*
* This way we know with how many pixels we have left to expand the
* rest.
*/
for (Column, T> column : visibleColumns) {
final double widthAsIs = column.getWidth();
final boolean isFixedWidth = widthAsIs >= 0;
// Check for max width just to be sure we don't break the limits
final double widthFixed = Math.max(
Math.min(getMaxWidth(column), widthAsIs),
column.getMinimumWidth());
defaultExpandRatios = defaultExpandRatios
&& (column.getExpandRatio() == -1
|| column == selectionColumn);
if (isFixedWidth) {
columnSizes.put(visibleColumns.indexOf(column), widthFixed);
reservedPixels += widthFixed;
} else {
nonFixedColumns.add(column);
columnSizes.put(visibleColumns.indexOf(column), -1.0d);
}
}
setColumnSizes(columnSizes);
for (Column, T> column : nonFixedColumns) {
final int expandRatio = defaultExpandRatios ? 1
: column.getExpandRatio();
final double maxWidth = getMaxWidth(column);
final double newWidth = Math.min(maxWidth,
column.getWidthActual());
boolean shouldExpand = newWidth < maxWidth && expandRatio > 0
&& column != selectionColumn;
if (shouldExpand) {
totalRatios += expandRatio;
columnsToExpand.add(column);
}
reservedPixels += newWidth;
columnSizes.put(visibleColumns.indexOf(column), newWidth);
}
/*
* Now that we know how many pixels we need at the very least, we
* can distribute the remaining pixels to all columns according to
* their expand ratios.
*/
double pixelsToDistribute = escalator.getInnerWidth()
- reservedPixels;
if (pixelsToDistribute <= 0 || totalRatios <= 0) {
if (pixelsToDistribute <= 0) {
// Set column sizes for expanding columns
setColumnSizes(columnSizes);
}
return;
}
/*
* Check for columns that hit their max width. Adjust
* pixelsToDistribute and totalRatios accordingly. Recheck. Stop
* when no new columns hit their max width
*/
boolean aColumnHasMaxedOut;
do {
aColumnHasMaxedOut = false;
final double widthPerRatio = pixelsToDistribute / totalRatios;
final Iterator> i = columnsToExpand.iterator();
while (i.hasNext()) {
final Column, T> column = i.next();
final int expandRatio = getExpandRatio(column,
defaultExpandRatios);
final int columnIndex = visibleColumns.indexOf(column);
final double autoWidth = columnSizes.get(columnIndex);
final double maxWidth = getMaxWidth(column);
double expandedWidth = autoWidth
+ widthPerRatio * expandRatio;
if (maxWidth <= expandedWidth) {
i.remove();
totalRatios -= expandRatio;
aColumnHasMaxedOut = true;
pixelsToDistribute -= maxWidth - autoWidth;
columnSizes.put(columnIndex, maxWidth);
}
}
} while (aColumnHasMaxedOut);
if (totalRatios <= 0 && columnsToExpand.isEmpty()) {
setColumnSizes(columnSizes);
return;
}
assert pixelsToDistribute > 0 : "We've run out of pixels to distribute ("
+ pixelsToDistribute + "px to " + totalRatios
+ " ratios between " + columnsToExpand.size() + " columns)";
assert totalRatios > 0 && !columnsToExpand
.isEmpty() : "Bookkeeping out of sync. Ratios: "
+ totalRatios + " Columns: "
+ columnsToExpand.size();
/*
* If we still have anything left, distribute the remaining pixels
* to the remaining columns.
*/
final double widthPerRatio;
int leftOver = 0;
if (BrowserInfo.getBrowserString().contains("PhantomJS")) {
// These browsers report subpixels as integers. this usually
// results into issues..
widthPerRatio = (int) (pixelsToDistribute / totalRatios);
leftOver = (int) (pixelsToDistribute
- widthPerRatio * totalRatios);
} else {
widthPerRatio = pixelsToDistribute / totalRatios;
}
for (Column, T> column : columnsToExpand) {
final int expandRatio = getExpandRatio(column,
defaultExpandRatios);
final int columnIndex = visibleColumns.indexOf(column);
final double autoWidth = columnSizes.get(columnIndex);
double totalWidth = autoWidth + widthPerRatio * expandRatio;
if (leftOver > 0) {
totalWidth += 1;
leftOver--;
}
columnSizes.put(columnIndex, totalWidth);
totalRatios -= expandRatio;
}
assert totalRatios == 0 : "Bookkeeping error: there were still some ratios left undistributed: "
+ totalRatios;
/*
* Check the guarantees for minimum width and scoot back the columns
* that don't care.
*/
boolean minWidthsCausedReflows;
do {
minWidthsCausedReflows = false;
/*
* First, let's check which columns were too cramped, and expand
* them. Also keep track on how many pixels we grew - we need to
* remove those pixels from other columns
*/
double pixelsToRemoveFromOtherColumns = 0;
for (Column, T> column : visibleColumns) {
/*
* We can't iterate over columnsToExpand, even though that
* would be convenient. This is because some column without
* an expand ratio might still have a min width - those
* wouldn't show up in that set.
*/
double minWidth = getMinWidth(column);
final int columnIndex = visibleColumns.indexOf(column);
double currentWidth = columnSizes.get(columnIndex);
boolean hasAutoWidth = column.getWidth() < 0;
if (hasAutoWidth && currentWidth < minWidth) {
columnSizes.put(columnIndex, minWidth);
pixelsToRemoveFromOtherColumns += minWidth
- currentWidth;
minWidthsCausedReflows = true;
/*
* Remove this column form the set if it exists. This
* way we make sure that it doesn't get shrunk in the
* next step.
*/
columnsToExpand.remove(column);
}
}
/*
* Now we need to shrink the remaining columns according to
* their ratios. Recalculate the sum of remaining ratios.
*/
totalRatios = 0;
for (Column, ?> column : columnsToExpand) {
totalRatios += getExpandRatio(column, defaultExpandRatios);
}
final double pixelsToRemovePerRatio = pixelsToRemoveFromOtherColumns
/ totalRatios;
for (Column, T> column : columnsToExpand) {
final double pixelsToRemove = pixelsToRemovePerRatio
* getExpandRatio(column, defaultExpandRatios);
int colIndex = visibleColumns.indexOf(column);
columnSizes.put(colIndex,
columnSizes.get(colIndex) - pixelsToRemove);
}
} while (minWidthsCausedReflows);
// Finally set all the column sizes.
setColumnSizes(columnSizes);
}
private void setColumnSizes(Map columnSizes) {
// Set all widths at once
escalator.getColumnConfiguration().setColumnWidths(columnSizes);
}
private int getExpandRatio(Column, ?> column,
boolean defaultExpandRatios) {
int expandRatio = column.getExpandRatio();
if (expandRatio > 0) {
return expandRatio;
} else if (expandRatio < 0) {
assert defaultExpandRatios : "No columns should've expanded";
return 1;
} else {
assert false : "this method should've not been called at all if expandRatio is 0";
return 0;
}
}
/**
* Returns the maximum width of the column, or {@link Double#MAX_VALUE}
* if defined as negative.
*/
private double getMaxWidth(Column, ?> column) {
double maxWidth = column.getMaximumWidth();
if (maxWidth >= 0) {
return maxWidth;
} else {
return Double.MAX_VALUE;
}
}
/**
* Returns the minimum width of the column, or {@link Double#MIN_VALUE}
* if defined as negative.
*/
private double getMinWidth(Column, ?> column) {
double minWidth = column.getMinimumWidth();
if (minWidth >= 0) {
return minWidth;
} else {
return Double.MIN_VALUE;
}
}
/**
* Check whether the auto width calculation is currently scheduled.
*
* @return true
if auto width calculation is currently
* scheduled
*/
public boolean isScheduled() {
return isScheduled;
}
}
private class GridSpacerUpdater implements SpacerUpdater {
private static final String STRIPE_CLASSNAME = "stripe";
private final Map elementToWidgetMap = new HashMap<>();
@Override
public void init(Spacer spacer) {
initTheming(spacer);
int rowIndex = spacer.getRow();
Widget detailsWidget = null;
try {
detailsWidget = detailsGenerator.getDetails(rowIndex);
} catch (Throwable e) {
getLogger().log(Level.SEVERE,
"Exception while generating details for row "
+ rowIndex,
e);
}
final double spacerHeight;
Element spacerElement = spacer.getElement();
if (detailsWidget == null) {
spacerElement.removeAllChildren();
spacerHeight = DETAILS_ROW_INITIAL_HEIGHT;
} else {
Element element = detailsWidget.getElement();
spacerElement.appendChild(element);
setParent(detailsWidget, Grid.this);
Widget previousWidget = elementToWidgetMap.put(element,
detailsWidget);
assert previousWidget == null : "Overwrote a pre-existing widget on row "
+ rowIndex + " without proper removal first.";
/*
* Once we have the content properly inside the DOM, we should
* re-measure it to make sure that it's the correct height.
*
* This is rather tricky, since the row (tr) will get the
* height, but the spacer cell (td) has the borders, which
* should go on top of the previous row and next row.
*/
double contentHeight;
if (detailsGenerator instanceof HeightAwareDetailsGenerator) {
HeightAwareDetailsGenerator sadg = (HeightAwareDetailsGenerator) detailsGenerator;
contentHeight = sadg.getDetailsHeight(rowIndex);
} else {
contentHeight = WidgetUtil
.getRequiredHeightBoundingClientRectDouble(element);
}
double borderTopAndBottomHeight = WidgetUtil
.getBorderTopAndBottomThickness(spacerElement);
double measuredHeight = contentHeight
+ borderTopAndBottomHeight;
assert getElement().isOrHasChild(
spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be.";
spacerHeight = measuredHeight;
}
escalator.getBody().setSpacer(rowIndex, spacerHeight);
if (getHeightMode() == HeightMode.UNDEFINED) {
setHeightByRows(getEscalator().getBody().getRowCount());
}
}
@Override
public void destroy(Spacer spacer) {
Element spacerElement = spacer.getElement();
assert getElement().isOrHasChild(spacerElement) : "Trying "
+ "to destroy a spacer that is not connected to this "
+ "Grid's DOM. (row: " + spacer.getRow() + ", element: "
+ spacerElement + ")";
Widget detailsWidget = elementToWidgetMap
.remove(spacerElement.getFirstChildElement());
if (detailsWidget != null) {
/*
* The widget may be null here if the previous generator
* returned a null widget.
*/
assert spacerElement.getFirstChild() != null : "The "
+ "details row to destroy did not contain a widget - "
+ "probably removed by something else without "
+ "permission? (row: " + spacer.getRow() + ", element: "
+ spacerElement + ")";
setParent(detailsWidget, null);
spacerElement.removeAllChildren();
if (getHeightMode() == HeightMode.UNDEFINED) {
// update spacer height
escalator.getBody().setSpacer(spacer.getRow(), 0);
setHeightByRows(getEscalator().getBody().getRowCount());
}
}
}
private void initTheming(Spacer spacer) {
Element spacerRoot = spacer.getElement();
if (spacer.getRow() % 2 == 1) {
spacerRoot.getParentElement().addClassName(STRIPE_CLASSNAME);
} else {
spacerRoot.getParentElement().removeClassName(STRIPE_CLASSNAME);
}
}
}
/**
* Sidebar displaying toggles for hidable columns and custom widgets
* provided by the application.
*
* The button for opening the sidebar is automatically visible inside the
* grid, if it contains any column hiding options or custom widgets. The
* column hiding toggles and custom widgets become visible once the sidebar
* has been opened.
*
* @since 7.5.0
*/
private static class Sidebar extends Composite implements HasEnabled {
private final ClickHandler openCloseButtonHandler = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (!isOpen()) {
open();
} else {
close();
}
}
};
private final FlowPanel rootContainer;
private final FlowPanel content;
private final MenuBar menuBar;
private final Button openCloseButton;
private final Grid> grid;
private Overlay overlay;
private Sidebar(Grid> grid) {
this.grid = grid;
rootContainer = new FlowPanel();
initWidget(rootContainer);
openCloseButton = new Button();
openCloseButton.addClickHandler(openCloseButtonHandler);
rootContainer.add(openCloseButton);
content = new FlowPanel() {
@Override
public boolean remove(Widget w) {
// Check here to catch child.removeFromParent() calls
boolean removed = super.remove(w);
if (removed) {
updateVisibility();
}
return removed;
}
};
createOverlay();
menuBar = new MenuBar(true) {
@Override
public MenuItem insertItem(MenuItem item, int beforeIndex)
throws IndexOutOfBoundsException {
if (getParent() == null) {
content.insert(this, 0);
updateVisibility();
}
return super.insertItem(item, beforeIndex);
}
@Override
public void removeItem(MenuItem item) {
super.removeItem(item);
if (getItems().isEmpty()) {
menuBar.removeFromParent();
}
}
@Override
public void onBrowserEvent(Event event) {
// selecting a item with enter will lose the focus and
// selected item, which means that further keyboard
// selection won't work unless we do this:
if (event.getTypeInt() == Event.ONKEYDOWN
&& event.getKeyCode() == KeyCodes.KEY_ENTER) {
final MenuItem item = getSelectedItem();
super.onBrowserEvent(event);
Scheduler.get()
.scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
selectItem(item);
focus();
}
});
} else {
super.onBrowserEvent(event);
}
}
};
KeyDownHandler keyDownHandler = new KeyDownHandler() {
@Override
public void onKeyDown(KeyDownEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
close();
}
}
};
openCloseButton.addDomHandler(keyDownHandler,
KeyDownEvent.getType());
menuBar.addDomHandler(keyDownHandler, KeyDownEvent.getType());
}
/**
* Creates and initializes the overlay.
*/
private void createOverlay() {
overlay = GWT.create(Overlay.class);
overlay.setOwner(grid);
overlay.setAutoHideEnabled(true);
overlay.addStyleDependentName("popup");
overlay.add(content);
overlay.addAutoHidePartner(rootContainer.getElement());
overlay.addCloseHandler(new CloseHandler() {
@Override
public void onClose(CloseEvent event) {
removeStyleName("open");
addStyleName("closed");
}
});
overlay.setFitInWindow(true);
}
/**
* Opens the sidebar if not yet opened. Opening the sidebar has no
* effect if it is empty.
*/
public void open() {
if (!isOpen() && isInDOM()) {
addStyleName("open");
removeStyleName("closed");
overlay.showRelativeTo(rootContainer);
}
}
/**
* Closes the sidebar if not yet closed.
*/
public void close() {
overlay.hide();
}
/**
* Returns whether the sidebar is open or not.
*
* @return true
if open, false
if not
*/
public boolean isOpen() {
return overlay != null && overlay.isShowing();
}
@Override
public void setStylePrimaryName(String styleName) {
super.setStylePrimaryName(styleName);
overlay.setStylePrimaryName(styleName);
content.setStylePrimaryName(styleName + "-content");
openCloseButton.setStylePrimaryName(styleName + "-button");
if (isOpen()) {
addStyleName("open");
removeStyleName("closed");
} else {
removeStyleName("open");
addStyleName("closed");
}
}
@Override
public void addStyleName(String style) {
super.addStyleName(style);
overlay.addStyleName(style);
}
@Override
public void removeStyleName(String style) {
super.removeStyleName(style);
overlay.removeStyleName(style);
}
private void setHeightToHeaderCellHeight() {
RowContainer header = grid.escalator.getHeader();
if (header.getRowCount() == 0
|| !header.getRowElement(0).hasChildNodes()) {
getLogger().info(
"No header cell available when calculating sidebar button height");
openCloseButton.setHeight(header.getDefaultRowHeight() + "px");
return;
}
Element firstHeaderCell = header.getRowElement(0)
.getFirstChildElement();
double height = WidgetUtil
.getRequiredHeightBoundingClientRectDouble(firstHeaderCell)
- WidgetUtil.measureVerticalBorder(getElement()) / 2;
openCloseButton.setHeight(height + "px");
}
private void updateVisibility() {
final boolean hasWidgets = content.getWidgetCount() > 0;
final boolean isVisible = isInDOM();
if (isVisible && !hasWidgets) {
Grid.setParent(this, null);
getElement().removeFromParent();
} else if (!isVisible && hasWidgets) {
close();
grid.getElement().appendChild(getElement());
Grid.setParent(this, grid);
// border calculation won't work until attached
setHeightToHeaderCellHeight();
}
}
private boolean isInDOM() {
return getParent() != null;
}
@Override
protected void onAttach() {
super.onAttach();
// make sure the button will get correct height if the button should
// be visible when the grid is rendered the first time.
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
setHeightToHeaderCellHeight();
}
});
}
@Override
public boolean isEnabled() {
return openCloseButton.isEnabled();
}
@Override
public void setEnabled(boolean enabled) {
if (!enabled && isOpen()) {
close();
}
openCloseButton.setEnabled(enabled);
}
}
/**
* UI and functionality related to hiding columns with toggles in the
* sidebar.
*/
private final class ColumnHider {
/** Map from columns to their hiding toggles, component might change */
private HashMap, MenuItem> columnToHidingToggleMap = new HashMap<>();
/**
* When column is being hidden with a toggle, do not refresh toggles for
* no reason. Also helps for keeping the keyboard navigation working.
*/
private boolean hidingColumn;
private void updateColumnHidable(final Column, T> column) {
if (column.isHidable()) {
MenuItem toggle = columnToHidingToggleMap.get(column);
if (toggle == null) {
toggle = createToggle(column);
}
toggle.setStyleName("hidden", column.isHidden());
} else if (columnToHidingToggleMap.containsKey(column)) {
sidebar.menuBar
.removeItem(columnToHidingToggleMap.remove(column));
}
updateTogglesOrder();
}
private MenuItem createToggle(final Column, T> column) {
MenuItem toggle = new MenuItem(createHTML(column), true,
new ScheduledCommand() {
@Override
public void execute() {
hidingColumn = true;
column.setHidden(!column.isHidden(), true);
hidingColumn = false;
}
});
toggle.addStyleName("column-hiding-toggle");
columnToHidingToggleMap.put(column, toggle);
return toggle;
}
private String createHTML(Column, T> column) {
final StringBuffer buf = new StringBuffer();
buf.append("");
String caption = column.getHidingToggleCaption();
if (caption == null) {
caption = column.headerCaption;
}
buf.append(caption);
buf.append("");
return buf.toString();
}
private void updateTogglesOrder() {
if (!hidingColumn) {
int lastIndex = 0;
for (Column, T> column : getColumns()) {
if (column.isHidable()) {
final MenuItem menuItem = columnToHidingToggleMap
.get(column);
sidebar.menuBar.removeItem(menuItem);
sidebar.menuBar.insertItem(menuItem, lastIndex++);
}
}
}
}
private void updateHidingToggle(Column, T> column) {
if (column.isHidable()) {
MenuItem toggle = columnToHidingToggleMap.get(column);
toggle.setHTML(createHTML(column));
toggle.setStyleName("hidden", column.isHidden());
} // else we can just ignore
}
private void removeColumnHidingToggle(Column, T> column) {
sidebar.menuBar.removeItem(columnToHidingToggleMap.get(column));
}
}
/**
* Escalator used internally by grid to render the rows
*/
private Escalator escalator = GWT.create(Escalator.class);
private final Header header = GWT.create(Header.class);
private final Footer footer = GWT.create(Footer.class);
private final Sidebar sidebar = new Sidebar(this);
/**
* List of columns in the grid. Order defines the visible order.
*/
private List> columns = new ArrayList<>();
/**
* The datasource currently in use. Note: it is null
* on initialization, but not after that.
*/
private DataSource dataSource;
private Registration changeHandler;
/**
* Currently available row range in DataSource.
*/
private Range currentDataAvailable = Range.withLength(0, 0);
/**
* The number of frozen columns, 0 freezes the selection column if
* displayed, -1 also prevents selection col from freezing.
*/
private int frozenColumnCount = 0;
/**
* Current sort order. The (private) sort() method reads this list to
* determine the order in which to present rows.
*/
private List sortOrder = new ArrayList<>();
private Renderer selectColumnRenderer = null;
private SelectionColumn selectionColumn;
private String rowStripeStyleName;
private String rowHasDataStyleName;
private String rowSelectedStyleName;
private String cellFocusStyleName;
private String rowFocusStyleName;
/**
* Current selection model.
*/
private SelectionModel selectionModel;
protected final CellFocusHandler cellFocusHandler;
private final UserSorter sorter = new UserSorter();
private final Editor editor = GWT.create(Editor.class);
/**
* The cell a click event originated from
*
* This is a workaround to make Chrome work like Firefox. In Chrome,
* normally if you start a drag on one cell and release on:
*
* - that same cell, the click event is that
<td>
.
* - a cell on that same row, the click event is the parent
*
<tr>
.
* - a cell on another row, the click event is the table section ancestor
* ({@code }, {@code } or {@code }).
*
*
* @see #onBrowserEvent(Event)
*/
private Cell cellOnPrevMouseDown;
/**
* A scheduled command to re-evaluate the widths of all columns
* that have calculated widths. Most probably called because
* minwidth/maxwidth/expandratio has changed.
*/
private final AutoColumnWidthsRecalculator autoColumnWidthsRecalculator = new AutoColumnWidthsRecalculator();
private boolean enabled = true;
private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;
private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater();
/** A set keeping track of the indices of all currently open details */
private Set
visibleDetails = new HashSet<>();
private boolean columnReorderingAllowed;
private ColumnHider columnHider = new ColumnHider();
private DragAndDropHandler dndHandler = new DragAndDropHandler();
private AutoScroller autoScroller = new AutoScroller(this);
private ColumnResizeMode columnResizeMode = ColumnResizeMode.ANIMATED;
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