/* * Copyright 2000-2021 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.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; 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.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.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.WidgetUtil.Reference; 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.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.escalator.events.SpacerIndexChangedEvent; import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler; import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedEvent; import com.vaadin.client.widget.escalator.events.SpacerVisibilityChangedHandler; 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.StaticRow; import com.vaadin.shared.Range; import com.vaadin.shared.Registration; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.ContentMode; 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 */ @SuppressWarnings("deprecation") 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; private String description = null; private ContentMode descriptionContentMode = ContentMode.TEXT; /** * Sets the text displayed in this cell. * * @param text * a plain text caption */ public void setText(String text) { detach(); content = text; type = GridStaticCellType.TEXT; section.requestSectionRefresh(); } /** * Returns the text displayed in this cell. * * @return the plain text caption */ public String getText() { if (type != GridStaticCellType.TEXT) { throw new IllegalStateException( "Cannot fetch Text from a cell with type " + type); } return (String) content; } /** * Returns the section where this cell belongs. * * @return the section */ protected StaticSection getSection() { assert section != null; return section; } /** * Sets the section where this cell belongs. Should not be null. * * @param section * the section to set */ 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) { detach(); content = html; type = GridStaticCellType.HTML; section.requestSectionRefresh(); } /** * Returns the widget in the cell. * * @throws IllegalStateException * if the cell is not {@link GridStaticCellType#WIDGET} * * @return the widget in the cell */ public Widget getWidget() { if (type != GridStaticCellType.WIDGET) { throw new IllegalStateException( "Cannot fetch Widget from a cell with type " + type); } return (Widget) content; } /** * Set widget as the content of the cell. The type of the cell * becomes {@link GridStaticCellType#WIDGET}. All previous content * is discarded. * * @param widget * The widget to add to the cell. Should not be * previously attached anywhere (widget.getParent == * null). */ public void setWidget(Widget widget) { if (content == widget) { return; } detach(); content = widget; type = GridStaticCellType.WIDGET; section.requestSectionRefresh(); } /** * Returns the type of the cell. * * @return the type of content the cell contains. */ public GridStaticCellType getType() { return type; } /** * Returns the custom style name for this cell. * * @return the style name or null if no style name has been set */ public String getStyleName() { return styleName; } /** * Sets a custom style name for this cell. * * @param styleName * the style name to set or null to not use any style * name */ public void setStyleName(String styleName) { this.styleName = styleName; section.requestSectionRefresh(); } /** * Called when the cell is detached from the row * * @since 7.6.3 */ void detach() { if (content instanceof Widget) { // Widget in the cell, detach it section.getGrid().detachWidget((Widget) content); } } /** * Gets the tooltip for the cell. *

* The tooltip is shown in the mode returned by * {@link #getDescriptionContentMode()}. * * @return the tooltip text for this cell * @since 8.4 */ public String getDescription() { return description; } /** * Sets the tooltip for the cell. *

* By default, tooltips are shown as plain text. For HTML tooltips, * see {@link #setDescription(String, ContentMode)} or * {@link #setDescriptionContentMode(ContentMode)}. * * @param description * the tooltip to show when hovering the cell * @since 8.4 */ public void setDescription(String description) { this.description = description; } /** * Sets the tooltip for the cell to be shown with the given content * mode. * * @see ContentMode * @param description * the tooltip to show when hovering the cell * @param descriptionContentMode * the content mode to use for the tooltip (HTML or plain * text) * @since 8.4 */ public void setDescription(String description, ContentMode descriptionContentMode) { setDescription(description); setDescriptionContentMode(descriptionContentMode); } /** * Gets the content mode for the tooltip. *

* The content mode determines how the tooltip is shown. * * @see ContentMode * @return the content mode for the tooltip * @since 8.4 */ public ContentMode getDescriptionContentMode() { return descriptionContentMode; } /** * Sets the content mode for the tooltip. * * @see ContentMode * @param descriptionContentMode * the content mode for the tooltip * @since 8.4 */ public void setDescriptionContentMode( ContentMode descriptionContentMode) { this.descriptionContentMode = descriptionContentMode; } } /** * 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 */ @SuppressWarnings("unchecked") 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; } /** * Returns the size of the cell group for the given column. Zero if * no cell group is set. * * @param column * the column whose cell group size is being investigated * @return size of the cell group, default is {@code 0} */ protected int getSizeOfCellGroup(Column column) { for (Entry>> entry : cellGroups .entrySet()) { if (entry.getValue().contains(column)) { return entry.getValue().size(); } } return 0; } 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; } /** * Creates and adds a cell to this row for the given column. * * @param column * the column that requires a new cell */ protected void addCell(Column column) { CELLTYPE cell = createCell(); cell.setSection(getSection()); cells.put(column, cell); } /** * Removes a cell for the given column from this row. * * @param column * the column that should have its cell reference cleared */ protected void removeCell(Column column) { cells.remove(column); } /** * Creates a cell of a type that matches this row. * * @return a new cell */ protected abstract CELLTYPE createCell(); /** * Returns the section where this row belongs. * * @return the section */ protected StaticSection getSection() { return section; } /** * Sets the section where this row belongs. Should not be null. * * @param section * the section to set */ 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) { if (this.visible != 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(); } /** * Returns the current list of rows within this section. * * @return list of rows */ protected List getRows() { return rows; } /** * Returns how many visible rows there are within this section (all or * nothing, visibility cannot be set on the row level). * * @return amount of visible rows */ protected int getVisibleRowCount() { return isVisible() ? getRowCount() : 0; } /** * Adds cells for the given column to all the rows within this section. * * @param column * the column that requires cells */ protected void addColumn(Column column) { for (ROWTYPE row : rows) { row.addCell(column); } } /** * Removes cells for the given column from all the rows within this * section. * * @param column * the column that should have its cells removed */ protected void removeColumn(Column column) { for (ROWTYPE row : rows) { row.removeCell(column); } } /** * Sets the grid this section belongs to. Should not be null. * * @param grid * the parent grid */ protected void setGrid(Grid grid) { this.grid = grid; } /** * Returns the grid this section belongs to. * * @return the parent grid */ protected Grid getGrid() { assert grid != null; return grid; } /** * Triggers column span calculation for all the rows within this section * that contain spanned cells. */ 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(() -> { 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; /** * Sets whether this is the default header row or not. Setting this * value to {@code true} resets the cell contents for this row to column * header captions. Setting this value to {@code false} doesn't update * the cell contents. * * @param isDefault * {@code true} if this row should be default header row, * {@code false} otherwise */ protected void setDefault(boolean isDefault) { this.isDefault = isDefault; if (isDefault) { for (Column column : getSection().grid.getColumns()) { column.setDefaultHeaderContent(getCell(column)); } } } /** * Returns whether this is the default header row or not. * * @return {@code true} if this row is the default header row, * {@code false} otherwise */ 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(() -> { if (markAsDirty) { markAsDirty = false; getGrid().refreshFooter(); } }); } } /** * A single cell in a grid Footer row. Has a textual caption. * */ public static class FooterCell extends StaticSection.StaticCell { } /** * A single row in a grid Footer section. * */ public static class FooterRow extends StaticSection.StaticRow { @Override protected FooterCell createCell() { return new FooterCell(); } } private static class EditorRequestImpl implements EditorRequest { /** * A callback interface used to notify the invoker of the editor handler * of completed editor requests. * * @param * the row data type */ public static interface RequestCallback { /** * The method that must be called when the request has been * processed correctly. * * @param request * the original request object */ public void onSuccess(EditorRequest request); /** * The method that must be called when processing the request has * produced an aborting error. * * @param request * the original request object */ public void onError(EditorRequest request); } private Grid grid; private final int rowIndex; private final int columnIndexDOM; private RequestCallback callback; private boolean completed = false; public EditorRequestImpl(Grid grid, int rowIndex, int columnIndexDOM, RequestCallback callback) { this.grid = grid; this.rowIndex = rowIndex; this.columnIndexDOM = columnIndexDOM; this.callback = callback; } @Override public int getRowIndex() { return rowIndex; } @Override public int getColumnIndex() { return columnIndexDOM; } @Override public T getRow() { return grid.getDataSource().getRow(rowIndex); } @Override public Grid getGrid() { return grid; } @Override public Widget getWidget(Grid.Column 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; /** * Constructs a new {@link GridEvent}. * * @param event * a native event * @param cell * the cell the event targets */ 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 * {@code true} if the event has already been handled, * {@code false} otherwise */ 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; /** * Constructs a new {@link EditorDomEvent}. * * @param event * a native event * @param cell * the cell the event targets * @param editorWidget * the editor widget of that cell */ protected EditorDomEvent(Event event, EventCellReference cell, Widget editorWidget) { super(event, cell); this.editorWidget = editorWidget; } /** * Returns the editor of the Grid this event originated from. * * @return the related editor instance */ public Editor getEditor() { return getGrid().getEditor(); } /** * Returns the currently focused editor widget. * * @return the focused editor widget or {@code null} if not editable */ public Widget getEditorWidget() { return editorWidget; } /** * Returns the row index the editor is open at. If the editor is not * open, returns -1. * * @return the index of the edited row or -1 if editor is not open */ public int getRowIndex() { return getEditor().rowIndex; } /** * Returns the DOM column index (excluding hidden columns) the editor * was opened at. If the editor is not open, returns -1. * * @return the column index or -1 if editor is not open */ public int getFocusedColumnIndex() { return getEditor().focusedColumnIndexDOM; } } /** * An editor UI for Grid rows. A single Grid row at a time can be opened for * editing. * * @since 7.6 * @param * the row type of the grid */ public static class Editor implements DeferredWorker { /** * @deprecated use {@link DefaultEditorEventHandler#KEYCODE_OPEN} * instead */ @Deprecated public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER; /** * @deprecated use {@link DefaultEditorEventHandler#KEYCODE_CLOSE} * instead */ @Deprecated 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); /** * Confirms the valid status of the binder so as to determine * whether to allow pending navigation action. * * @param isValid * {@code true} if the binder value is valid */ void confirmValidity(boolean isValid); } /** * The internal state options for the editor. */ protected enum State { /** Editor is closed or disabled and as such not in edit mode. */ INACTIVE, /** * Editor is getting activated. If activation is successful, next * state will be {@link State#BINDING} . */ ACTIVATING, /** * Editor binding is being set up. If binding is successful, next * state will be {@link State#ACTIVE}. */ BINDING, /** Editor is open and enabled and in edit mode. */ ACTIVE, /** * Editor is saving changes. If saving is successful, state will be * updated to {@link State#INACTIVE}. */ 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 focusedColumnIndexDOM = -1; private String styleName = null; private HandlerRegistration hScrollHandler; 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(); focusedColumnIndexDOM = request.getColumnIndex(); if (focusedColumnIndexDOM >= 0) { // Update internal focus of Grid grid.focusCell(rowIndex, focusedColumnIndexDOM); } showOverlay(); } } @Override public void onError(EditorRequest request) { if (state == State.BINDING) { if (rowIndex == -1) { doCancel(); } else { state = State.ACTIVE; // TODO: Maybe restore focus? } bindTimeout.cancel(); } } }; /** A set of all the columns that display an error flag. */ private final Set> columnErrors = new HashSet<>(); private boolean buffered = true; /** Original position of editor */ private double originalTop; /** Original scroll position of grid when editor was opened */ private double originalScrollTop; private RowHandle pinnedRowHandle; /** * Constructs a new {@link Editor}. */ public Editor() { saveButton = new Button(); saveButton.setText(GridConstants.DEFAULT_SAVE_CAPTION); saveButton.addClickHandler(event -> { save(); FocusUtil.setFocus(grid, true); }); cancelButton = new Button(); cancelButton.setText(GridConstants.DEFAULT_CANCEL_CAPTION); cancelButton.addClickHandler(event -> { cancel(); FocusUtil.setFocus(grid, true); }); } /** * Sets an error message for the editor, or removes it if the given * message is {@code null}. If the editor is in edit mode or currently * processing a save operation, updates the editor cells to show error * styles for the given columns and clears error styles from any other * columns. * * @param errorMessage * the message to set, or {@code null} if no message should * be shown * @param errorColumns * the columns that currently contain errors */ 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 c : grid.getColumns()) { grid.getEditor().setEditorColumnError(c, errorColumns.contains(c)); } } } /** * Returns the index of the row that is currently assigned to the * editor. If the editor is open and in the edit mode, that row's * contents can be edited via the editor cells. -1 if the editor is * closed. * * @return currently assigned row index, or -1 if not set */ 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)}. * * @param rowIndex * the index of the row to be edited * * @see #editRow(int, int) */ public void editRow(int rowIndex) { // Focus the last focused column in the editor if grid or its child // was focused before the edit request Cell focusedCell = grid.cellFocusHandler.getFocusedCell(); Element focusedElement = WidgetUtil.getFocusedElement(); if (focusedCell != null && focusedElement != null && grid.getElement().isOrHasChild(focusedElement)) { editRow(rowIndex, focusedCell.getColumn()); } else { editRow(rowIndex, -1); } } /** * Opens the editor over the row with the given index and attempts to * focus the editor widget in the given column index. Does not move * focus if the widget is not focusable or if the column index is -1. * * @param rowIndex * the index of the row to be edited * @param columnIndexDOM * the column index (excluding hidden columns) of the editor * widget that should be initially focused or -1 to not set * focus * * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException * if this editor is already in edit mode and in buffered * mode * * @since 7.5 */ public void editRow(final int rowIndex, final int columnIndexDOM) { if (!enabled) { throw new IllegalStateException( "Cannot edit row: editor is not enabled"); } if (isWorkPending()) { // Request pending a response, don't move try to start another // request. return; } if (state != State.INACTIVE && this.rowIndex != rowIndex) { if (isBuffered()) { throw new IllegalStateException( "Cannot edit row: editor already in edit mode"); } else if (!columnErrors.isEmpty()) { // Don't move row if errors are present // FIXME: Should attempt bind if error field values have // changed. return; } } if (columnIndexDOM >= grid.getVisibleColumns().size()) { throw new IllegalArgumentException( "Edited column index " + columnIndexDOM + " was bigger than visible column count."); } if (this.rowIndex == rowIndex && focusedColumnIndexDOM == columnIndexDOM) { // NO-OP return; } if (this.rowIndex == rowIndex) { if (focusedColumnIndexDOM != columnIndexDOM) { if (columnIndexDOM >= grid.getFrozenColumnCount()) { // Scroll to new focused column. grid.getEscalator().scrollToColumn(columnIndexDOM, ScrollDestination.ANY, 0); } focusedColumnIndexDOM = columnIndexDOM; } updateHorizontalScrollPosition(); // Update Grid internal focus and focus widget if possible if (focusedColumnIndexDOM >= 0) { grid.focusCell(rowIndex, focusedColumnIndexDOM); focusColumn(focusedColumnIndexDOM); } // No need to request anything from the editor handler. return; } state = State.ACTIVATING; grid.scrollToRow(rowIndex, ScrollDestination.ANY, () -> show(rowIndex, columnIndexDOM)); } /** * 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, focusedColumnIndexDOM, null), afterSave); doCancel(); } private void doCancel() { hideOverlay(); state = State.INACTIVE; rowIndex = -1; focusedColumnIndexDOM = -1; grid.getEscalator().setScrollLocked(Direction.VERTICAL, false); updateSelectionCheckboxesAsNeeded(true); } private void updateSelectionCheckboxesAsNeeded(boolean isEnabled) { // FIXME: This is too much guessing. Define a better way to do this. if (grid.selectionColumn != null && grid.selectionColumn .getRenderer() instanceof MultiSelectionRenderer) { grid.refreshBody(); HeaderCell cell = grid.getDefaultHeaderRow() .getCell(grid.selectionColumn); // if lazy provider, then no checkbox if (cell.getType() == GridStaticCellType.WIDGET) { CheckBox checkBox = (CheckBox) grid.getDefaultHeaderRow() .getCell(grid.selectionColumn).getWidget(); checkBox.setEnabled(isEnabled); } } } /** * Saves any unsaved changes to the data source and hides the editor. * * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException * if this editor is not in edit mode */ public void save() { if (!enabled) { throw new IllegalStateException( "Cannot save: editor is not enabled"); } if (state != State.ACTIVE) { throw new IllegalStateException( "Cannot save: editor is not in edit mode"); } state = State.SAVING; setButtonsEnabled(false); saveTimeout.schedule(SAVE_TIMEOUT_MS); EditorRequest request = new EditorRequestImpl<>(grid, rowIndex, focusedColumnIndexDOM, saveRequestCallback); handler.save(request); updateSelectionCheckboxesAsNeeded(true); } /** * Returns the handler responsible for binding data and editor widgets * to this editor. * * @return the editor handler or null if not set */ public EditorHandler getHandler() { return handler; } /** * Sets the handler responsible for binding data and editor widgets to * this editor. * * @param rowHandler * the new editor handler * * @throws IllegalStateException * if this editor is currently in edit mode */ public void setHandler(EditorHandler rowHandler) { if (state != State.INACTIVE) { throw new IllegalStateException( "Cannot set EditorHandler: editor is currently in edit mode"); } handler = rowHandler; } /** * Returns the enabled state of this editor. * * @return {@code true} if enabled, {@code false} otherwise */ public boolean isEnabled() { return enabled; } /** * Sets the enabled state of this editor. * * @param enabled * {@code true} if enabled, {@code false} otherwise * * @throws IllegalStateException * if in edit mode and trying to disable * @throws IllegalStateException * if the editor handler is not set */ public void setEnabled(boolean enabled) { if (!enabled && state != State.INACTIVE) { throw new IllegalStateException( "Cannot disable: editor is in edit mode"); } else if (enabled && getHandler() == null) { throw new IllegalStateException( "Cannot enable: EditorHandler not set"); } this.enabled = enabled; } /** * If the Editor is still activating, proceeds to the binding phase. * Otherwise does nothing. * * @param rowIndex * the index of the row to be edited * @param columnIndex * the column index (excluding hidden columns) of the editor * widget that should be initially focused or -1 to not set * focus */ 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); } } /** * Sets the grid this editor belongs to. Should not be null. Should only * be called once. * * @param grid * the parent grid */ 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; } /** * Returns the internal state of the editor. * * @return the state */ protected State getState() { return state; } /** * Sets the internal state of the editor. * * @param state * the state to set */ 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 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 grid 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(event -> { updateHorizontalScrollPosition(); updateVerticalScrollPosition(); }); addEditorAndContents(gridElement, tr); updateSizeAndPosition(gridElement, tr); } private void addEditorAndContents(DivElement gridElement, TableRowElement tr) { gridElement.appendChild(editorOverlay); editorOverlay.appendChild(frozenCellWrapper); editorOverlay.appendChild(cellWrapper); editorOverlay.appendChild(messageAndButtonsWrapper); updateBufferedStyleName(); updateSelectedStyleName(); 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 column = grid.getVisibleColumn(i); if (i < frozenColumns) { frozenCellWrapper.appendChild(cell); frozenColumnsWidth += WidgetUtil .getRequiredWidthBoundingClientRectDouble( tr.getCells().getItem(i)); } else { cellWrapper.appendChild(cell); } if (column.isEditable()) { Widget editor = getHandler().getWidget(column); if (editor != null) { columnToWidget.put(column, editor); grid.attachWidget(editor, cell); } if (i == focusedColumnIndexDOM) { focusColumn(focusedColumnIndexDOM); } } else { cell.addClassName(NOT_EDITABLE_CLASS_NAME); cell.addClassName(tr.getCells().getItem(i).getClassName()); // If the focused or frozen stylename is present it should // not be inherited by the editor cell as it is not useful // in the editor and would look broken without additional // style rules. This is a bit of a hack. cell.removeClassName(grid.cellFocusStyleName); cell.removeClassName("frozen"); if (column == grid.selectionColumn) { // Duplicate selection column CheckBox pinnedRowHandle = grid.getDataSource().getHandle( grid.getDataSource().getRow(rowIndex)); pinnedRowHandle.pin(); // We need to duplicate the selection CheckBox for the // editor overlay since the original one is hidden by // the overlay final CheckBox checkBox = GWT.create(CheckBox.class); checkBox.setStylePrimaryName(grid.getStylePrimaryName() + "-selection-checkbox"); // label of checkbox should only be visible for // assistive devices checkBox.addStyleName("v-assistive-device-only-label"); 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()); } private void updateSizeAndPosition(DivElement gridElement, TableRowElement tr) { 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(); double borderHeight = WidgetUtil .getBorderBottomThickness(editorOverlay); editorOverlay.getStyle().setBottom(gridHeight - overlayTop - tr.getOffsetHeight() + borderHeight, Unit.PX); editorOverlay.getStyle().clearTop(); } // Do not render over the vertical scrollbar editorOverlay.getStyle().setWidth(grid.escalator.getInnerWidth(), Unit.PX); } private void focusColumn(int columnIndexDOM) { if (columnIndexDOM < 0 || columnIndexDOM >= grid.getVisibleColumns().size()) { // NO-OP return; } Widget editor = getWidget(grid.getVisibleColumn(columnIndexDOM)); if (editor instanceof Focusable) { ((Focusable) editor).focus(); } else if (editor instanceof com.google.gwt.user.client.ui.Focusable) { ((com.google.gwt.user.client.ui.Focusable) editor) .setFocus(true); } else { grid.focus(); } } private boolean buttonsShouldBeRenderedBelow(TableRowElement tr) { TableSectionElement tfoot = grid.escalator.getFooter().getElement(); double tfootPageTop = WidgetUtil.getBoundingClientRect(tfoot) .getTop(); double trPageBottom = WidgetUtil.getBoundingClientRect(tr) .getBottom(); int messageAndButtonsHeight = messageAndButtonsWrapper .getOffsetHeight(); double bottomOfButtons = trPageBottom + messageAndButtonsHeight; return bottomOfButtons < tfootPageTop; } /** * Hides the editor overlay and clears its contents. */ 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"); } } private void updateSelectedStyleName() { // Add class name with selected modifier if the editor is being // opened on selected row, see #11634 String selectedStylename = styleName + "-selected"; if (grid.isSelected(grid.getDataSource().getRow(getRow()))) { cellWrapper.addClassName(selectedStylename); } else { cellWrapper.removeClassName(selectedStylename); } } /** * Sets the editor's primary style name and updates all dependent style * names. * * @param primaryName * the new primary style name */ 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); } /** * Sets the editor's buttons enabled. Does not update the enabled status * of the grid this editor belongs to, nor ask the enabled status from * it. * * @param enabled * {@code true} to enable the buttons, {@code false} to * disable them */ 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); } /** * Sets the caption of the save button. Should not be {@code null}. * * @param saveCaption * the new caption text * @throws IllegalArgumentException * if the given text is {@code null} */ public void setSaveCaption(String saveCaption) throws IllegalArgumentException { if (saveCaption == null) { throw new IllegalArgumentException( "Save caption cannot be null"); } saveButton.setText(saveCaption); } /** * Returns the caption of the save button. * * @return the caption text */ public String getSaveCaption() { return saveButton.getText(); } /** * Sets the caption of the cancel button. Should not be {@code null}. * * @param cancelCaption * the new caption text * @throws IllegalArgumentException * if the given text is {@code null} */ public void setCancelCaption(String cancelCaption) throws IllegalArgumentException { if (cancelCaption == null) { throw new IllegalArgumentException( "Cancel caption cannot be null"); } cancelButton.setText(cancelCaption); } /** * Returns the caption of the cancel button. * * @return the caption text */ public String getCancelCaption() { return cancelButton.getText(); } /** * If the given column is indicated to have an error in the editor, sets * the error styles to the corresponding editor cell and lists the * column as having an error. Otherwise removes the error styles from * the cell and removes the column from the error collection. * * @param column * the column which should have its editor error status * updated * @param hasError * {@code true} if the cell should get error styles, * {@code false} if the error styles should be removed from * the cell */ public void setEditorColumnError(Column 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); } } /** * Clears all editor error styles from the editor cells and clears the * error column collection. */ 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(); } /** * Returns true if the column has been listed as containing an error. * * @param column * the column whose error status is checked * @return {@code true} if the column is listed as containing an error, * {@code false} otherwise */ public boolean isEditorColumnError(Column column) { return columnErrors.contains(column); } /** * Sets the buffered mode. When the editor is in buffered mode, edits * are only committed when the user clicks the save button. In * unbuffered mode valid changes are automatically committed. * * @param buffered * {@code true} if editor should be buffered, {@code false} * otherwise */ public void setBuffered(boolean buffered) { this.buffered = buffered; setMessageAndButtonsWrapperVisible(buffered); } /** * Returns whether this editor is buffered or not. * * @return {@code true} if editor is buffered, {@code false} otherwise * * @see #setBuffered(boolean) */ 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(); } /** * Returns the column index of the editor cell that contains the given * element, or -1 if the element can't be found within the editor cells. * * @param e * the element to be located * @return the column index of the associated editor cell, or -1 if not * found */ 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; } } /** * Event class for key events that happen within the grid. * * @param * the event handler type */ public abstract static class AbstractGridKeyEvent extends KeyEvent { /** * @since 7.7.9 */ public AbstractGridKeyEvent() { } /** * @param grid * the grid where the event occurred * @param targetCell * the cell that the event targeted * * @deprecated This constructor's arguments are no longer used. Use the * no-args constructor instead. */ @Deprecated public AbstractGridKeyEvent(Grid grid, CellReference targetCell) { } /** * Returns the {@link BrowserEvents} type that corresponds with this * event. * * @return the browser event type */ protected abstract String getBrowserEventType(); /** * Gets the Grid instance for this event, if it originated from a Grid. * * @return the grid this event originated from, or {@code null} if this * event did not originate from a grid */ public Grid getGrid() { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { return null; } return WidgetUtil.findWidget(Element.as(target), Grid.class, false); } /** * Gets the reference of target cell for this event, if this event * originated from a Grid. * * @return target cell, or {@code null} if this event did not originate * from a grid */ public CellReference getFocusedCell() { return getGrid().getEventCell(); } @Override protected void dispatch(HANDLER handler) { EventTarget target = getNativeEvent().getEventTarget(); Grid grid = getGrid(); if (Element.is(target) && grid != null) { final RowContainer container = Stream .of(grid.escalator.getHeader(), grid.escalator.getBody(), grid.escalator.getFooter()) .filter(c -> c.getCell(target.cast()) != null) .findFirst() .orElse(grid.cellFocusHandler.containerWithFocus); Section section = Section.FOOTER; if (container == grid.escalator.getHeader()) { section = Section.HEADER; } else if (container == getGrid().escalator.getBody()) { section = Section.BODY; } // Don't handle event of child widget unless the column has been // explicitly permitted to do so if (grid.isElementInChildWidget(Element.as(target))) { Cell cell = container.getCell(target.cast()); if (cell != null) { Column column = grid .getVisibleColumn(cell.getColumn()); if (column == null || !column.isHandleWidgetEvents()) { return; } } } doDispatch(handler, section); } } /** * The given handler processes the event if the handler type matches the * given section. * * @param handler * handler * @param section * the section where the target element is located */ protected abstract void doDispatch(HANDLER handler, Section section); } /** * Event class for mouse events that happen within the grid. * * @param * the event handler type */ public abstract static class AbstractGridMouseEvent extends MouseEvent { /** * @since 7.7.9 */ public AbstractGridMouseEvent() { } /** * @param grid * the grid where the event occurred * @param targetCell * the cell that the event targeted * * @deprecated This constructor's arguments are no longer used. Use the * no-args constructor instead. */ @Deprecated public AbstractGridMouseEvent(Grid grid, CellReference targetCell) { } /** * Returns the {@link BrowserEvents} type that corresponds with this * event. * * @return the browser event type */ protected abstract String getBrowserEventType(); /** * Gets the Grid instance for this event, if it originated from a Grid. * * @return the grid this event originated from, or {@code null} if this * event did not originate from a grid */ public Grid getGrid() { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { return null; } return WidgetUtil.findWidget(Element.as(target), Grid.class, false); } /** * Gets the reference of target cell for this event, if this event * originated from a Grid. * * @return target cell, or {@code null} if this event did not originate * from a grid */ public CellReference getTargetCell() { Grid grid = getGrid(); if (grid == null) { return null; } return grid.getEventCell(); } @Override protected void dispatch(HANDLER handler) { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { // Target is not an element return; } Grid grid = getGrid(); if (grid == null) { // Target is not an element of a grid return; } Element targetElement = Element.as(target); if (ignoreEventFromTarget(grid, targetElement)) { // Event on this target should be ignored 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); } /** * Returns whether the mouse event on the target element should be * ignored. * * @param grid * the {@code Grid} instance from which the event originated * @param targetElement * the element from which the event originated * @return {@code true} if the event should be ignored, {@code false} if * it should be handled * @since 8.2 */ protected boolean ignoreEventFromTarget(Grid grid, Element targetElement) { boolean childWidget = grid.isElementInChildWidget(targetElement); boolean handleWidgetEvent = false; RowContainer container = grid.getEscalator() .findRowContainer(targetElement); if (container != null) { Cell cell = container.getCell(targetElement); if (cell != null) { Column column = grid .getVisibleColumn(cell.getColumn()); handleWidgetEvent = column != null && column.isHandleWidgetEvents(); } } return childWidget && !handleWidgetEvent; } /** * The given handler processes the event if the handler type matches the * given section. * * @param handler * handler * @param section * the section where the target element is located */ protected abstract void doDispatch(HANDLER handler, Section section); } private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle"; /** * An initial height that is given to new details rows before rendering the * appropriate widget that we then can be measure * * @see Grid.GridSpacerUpdater */ private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; private EventCellReference eventCell = new EventCellReference(this); /** * Focus handler for Grid's cells. Updates focus style names and handles * focus scrolling. */ private class CellFocusHandler { private RowContainer containerWithFocus = escalator.getBody(); private int rowWithFocus = 0; private Range cellFocusRange = Range.withLength(0, 1); private int lastFocusedBodyRow = 0; private int lastFocusedHeaderRow = 0; private int lastFocusedFooterRow = 0; private TableCellElement cellWithFocusStyle = null; private TableRowElement rowWithFocusStyle = null; public CellFocusHandler() { sinkEvents(getNavigationEvents()); } private Cell getFocusedCell() { return new Cell(rowWithFocus, cellFocusRange.getStart(), cellWithFocusStyle); } /** * Sets style names for given cell when needed. */ public void updateFocusedCellStyle(FlyweightCell cell, RowContainer cellContainer) { int cellRow = cell.getRow(); int cellColumn = cell.getColumn(); int colSpan = cell.getColSpan(); boolean columnHasFocus = Range.withLength(cellColumn, colSpan) .intersects(cellFocusRange); if (cellContainer == containerWithFocus) { // Cell is in the current container if (cellRow == rowWithFocus && columnHasFocus) { if (cellWithFocusStyle != cell.getElement()) { // Cell is correct but it does not have focused style if (cellWithFocusStyle != null) { // Remove old focus style setStyleName(cellWithFocusStyle, cellFocusStyleName, false); } cellWithFocusStyle = cell.getElement(); // Add focus style to correct cell. setStyleName(cellWithFocusStyle, cellFocusStyleName, true); } } else if (cellWithFocusStyle == cell.getElement()) { // Due to escalator reusing cells, a new cell has the same // element but is not the focused cell. setStyleName(cellWithFocusStyle, cellFocusStyleName, false); cellWithFocusStyle = null; } } } /** * Sets focus style for the given row if needed. * * @param row * a row object */ public void updateFocusedRowStyle(Row row) { if (rowWithFocus == row.getRow() && containerWithFocus == escalator.getBody()) { if (row.getElement() != rowWithFocusStyle) { // Row should have focus style but does not have it. if (rowWithFocusStyle != null) { setStyleName(rowWithFocusStyle, rowFocusStyleName, false); } rowWithFocusStyle = row.getElement(); setStyleName(rowWithFocusStyle, rowFocusStyleName, true); } } else if (rowWithFocusStyle == row.getElement() || containerWithFocus != escalator.getBody() && rowWithFocusStyle != null) { // Remove focus style. setStyleName(rowWithFocusStyle, rowFocusStyleName, false); rowWithFocusStyle = null; } } /** * Sets the currently focused. *

* NOTE: the column index is the index in DOM, not the logical * column index which includes hidden columns. * * @param rowIndex * the index of the row having focus * @param columnIndexDOM * the index of the cell having focus * @param container * the row container having focus */ private void setCellFocus(int rowIndex, int columnIndexDOM, RowContainer container) { if (container == null || rowIndex == rowWithFocus && cellFocusRange.contains(columnIndexDOM) && container == this.containerWithFocus) { return; } int oldRow = rowWithFocus; rowWithFocus = rowIndex; Range oldRange = cellFocusRange; if (container == escalator.getBody()) { scrollToRow(rowWithFocus); cellFocusRange = Range.withLength(columnIndexDOM, 1); } else { int i = 0; Element cell = container.getRowElement(rowWithFocus) .getFirstChildElement(); do { int colSpan = cell .getPropertyInt(FlyweightCell.COLSPAN_ATTR); Range cellRange = Range.withLength(i, colSpan); if (cellRange.contains(columnIndexDOM)) { cellFocusRange = cellRange; break; } cell = cell.getNextSiblingElement(); ++i; } while (cell != null); } if (columnIndexDOM >= escalator.getColumnConfiguration() .getFrozenColumnCount()) { escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY, 10); } if (this.containerWithFocus == container) { if (oldRange.equals(cellFocusRange) && oldRow != rowWithFocus) { refreshRow(oldRow); } else { refreshHeader(); refreshFooter(); } } else { RowContainer oldContainer = this.containerWithFocus; this.containerWithFocus = container; if (oldContainer == escalator.getBody()) { lastFocusedBodyRow = oldRow; } else if (oldContainer == escalator.getHeader()) { lastFocusedHeaderRow = oldRow; } else { lastFocusedFooterRow = oldRow; } if (!oldRange.equals(cellFocusRange)) { refreshHeader(); refreshFooter(); if (oldContainer == escalator.getBody()) { oldContainer.refreshRows(oldRow, 1); } } else { oldContainer.refreshRows(oldRow, 1); } } refreshRow(rowWithFocus); } /** * Sets focus on a cell. * *

* Note: cell focus is not the same as JavaScript's * {@code document.activeElement}. * * @param cell * a cell object */ public void setCellFocus(CellReference cell) { setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(), escalator.findRowContainer(cell.getElement())); } /** * Gets list of events that can be used for cell focusing. * * @return list of navigation related event types */ public Collection getNavigationEvents() { return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK); } /** * Handle events that can move the cell focus. */ public void handleNavigationEvent(Event event, CellReference cell) { if (event.getType().equals(BrowserEvents.CLICK)) { setCellFocus(cell); // Grid should have focus when clicked. getElement().focus(); } else if (event.getType().equals(BrowserEvents.KEYDOWN)) { int newRow = rowWithFocus; RowContainer newContainer = containerWithFocus; int newColumn = cellFocusRange.getStart(); switch (event.getKeyCode()) { case KeyCodes.KEY_DOWN: ++newRow; break; case KeyCodes.KEY_UP: --newRow; break; case KeyCodes.KEY_RIGHT: if (cellFocusRange.getEnd() >= getVisibleColumns().size()) { return; } newColumn = cellFocusRange.getEnd(); break; case KeyCodes.KEY_LEFT: if (newColumn == 0) { return; } --newColumn; break; case KeyCodes.KEY_TAB: if (event.getShiftKey()) { newContainer = getPreviousContainer(containerWithFocus); } else { newContainer = getNextContainer(containerWithFocus); } if (newContainer == containerWithFocus) { return; } break; case KeyCodes.KEY_HOME: if (newContainer.getRowCount() > 0) { newRow = 0; } break; case KeyCodes.KEY_END: if (newContainer.getRowCount() > 0) { newRow = newContainer.getRowCount() - 1; } break; case KeyCodes.KEY_PAGEDOWN: case KeyCodes.KEY_PAGEUP: if (newContainer.getRowCount() > 0) { boolean down = event .getKeyCode() == KeyCodes.KEY_PAGEDOWN; // If there is a visible focused cell, scroll by one // page from its position. Otherwise, use the first or // the last visible row as the scroll start position. // This avoids jumping when using both keyboard and the // scroll bar for scrolling. int firstVisible = getFirstVisibleRowIndex(); int lastVisible = getLastVisibleRowIndex(); if (newRow < firstVisible || newRow > lastVisible) { newRow = down ? lastVisible : firstVisible; } // Scroll by a little less than the visible area to // account for the possibility that the top and the // bottom row are only partially visible. int moveFocusBy = Math.max(1, lastVisible - firstVisible - 1); moveFocusBy *= down ? 1 : -1; newRow += moveFocusBy; newRow = Math.max(0, Math .min(newContainer.getRowCount() - 1, newRow)); } break; default: return; } if (newContainer != containerWithFocus) { if (newContainer == escalator.getBody()) { newRow = lastFocusedBodyRow; } else if (newContainer == escalator.getHeader()) { newRow = lastFocusedHeaderRow; } else { newRow = lastFocusedFooterRow; } } else if (newRow < 0) { newContainer = getPreviousContainer(newContainer); if (newContainer == containerWithFocus) { newRow = 0; } else if (newContainer == escalator.getBody()) { newRow = getLastVisibleRowIndex(); } else { newRow = newContainer.getRowCount() - 1; } } else if (newRow >= containerWithFocus.getRowCount()) { newContainer = getNextContainer(newContainer); if (newContainer == containerWithFocus) { newRow = containerWithFocus.getRowCount() - 1; } else if (newContainer == escalator.getBody()) { newRow = getFirstVisibleRowIndex(); } else { newRow = 0; } } if (newContainer.getRowCount() == 0) { /* * There are no rows in the container. Can't change the * focused cell. */ return; } event.preventDefault(); event.stopPropagation(); setCellFocus(newRow, newColumn, newContainer); } } private RowContainer getPreviousContainer(RowContainer current) { if (current == escalator.getFooter()) { current = escalator.getBody(); } else if (current == escalator.getBody()) { current = escalator.getHeader(); } else { return current; } if (current.getRowCount() == 0) { return getPreviousContainer(current); } return current; } private RowContainer getNextContainer(RowContainer current) { if (current == escalator.getHeader()) { current = escalator.getBody(); } else if (current == escalator.getBody()) { current = escalator.getFooter(); } else { return current; } if (current.getRowCount() == 0) { return getNextContainer(current); } return current; } private void refreshRow(int row) { containerWithFocus.refreshRows(row, 1); } /** * Offsets the focused cell's range. * * @param offset * offset for fixing focused cell's range */ public void offsetRangeBy(int offset) { cellFocusRange = cellFocusRange.offsetBy(offset); } /** * Informs {@link CellFocusHandler} that certain range of rows has been * added to the Grid body. {@link CellFocusHandler} will fix indices * accordingly. * * @param added * a range of added rows */ public void rowsAddedToBody(Range added) { boolean bodyHasFocus = containerWithFocus == escalator.getBody(); boolean insertionIsAboveFocusedCell = added .getStart() < rowWithFocus; if (bodyHasFocus && insertionIsAboveFocusedCell) { rowWithFocus += added.length(); rowWithFocus = Math.min(rowWithFocus, escalator.getBody().getRowCount() - 1); refreshRow(rowWithFocus); } } /** * Informs {@link CellFocusHandler} that certain range of rows has been * removed from the Grid body. {@link CellFocusHandler} will fix indices * accordingly. * * @param removed * a range of removed rows */ public void rowsRemovedFromBody(Range removed) { if (containerWithFocus != escalator.getBody()) { return; } else if (!removed.contains(rowWithFocus)) { if (removed.getStart() > rowWithFocus) { return; } rowWithFocus = rowWithFocus - removed.length(); } else { if (containerWithFocus.getRowCount() > removed.getEnd()) { rowWithFocus = removed.getStart(); } else if (removed.getStart() > 0) { rowWithFocus = removed.getStart() - 1; } else { if (escalator.getHeader().getRowCount() > 0) { rowWithFocus = Math.min(lastFocusedHeaderRow, escalator.getHeader().getRowCount() - 1); containerWithFocus = escalator.getHeader(); } else if (escalator.getFooter().getRowCount() > 0) { rowWithFocus = Math.min(lastFocusedFooterRow, escalator.getFooter().getRowCount() - 1); containerWithFocus = escalator.getFooter(); } } } refreshRow(rowWithFocus); } } /** * A column that contains CheckBoxes for representing Grid's row selection. * By default only used with MultiSelectionModel. * */ 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); // label of checkbox should only be visible for assistive // devices selectAllCheckBox.addStyleName("v-assistive-device-only-label"); selectAllCheckBox.addValueChangeHandler(event -> { selected = event.getValue(); fireEvent(new SelectAllEvent<>(getSelectionModel(), selected)); }); selectAllCheckBox.setText("Selects all rows of the table."); 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() { if (selectAllCheckBox == null || selectionCell == null) { // There is no default header row to display select all checkbox return; } if (selectAllCheckBoxVisible) { selectionCell.setWidget(selectAllCheckBox); } else { 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 column; private UserSorter() { timer = new Timer() { @Override public void run() { scheduledMultisort = true; } }; } /** * 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); } 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); } /** * Invoked on touchstart, marks itself that we will perform sorting on * touchend. By default single sort is performed, however on long touch * we will perform multitouch. * * Actual sorting is only performed after {@link #onTouchEnd()} is * invoked. * * @param delay * delay, in milliseconds */ public void awaitForTouchEnd(int delay) { cancelAwaitForTouchEnd(); column = eventCell.getColumn(); scheduledMultisort = false; timer.schedule(delay); } /** * Notifies that the finger has been lifted from the tablet/mobile. * Depending on how much time has passed, we need to perform singlesort * or multisort. * * Does nothing if the await has been canceled by a call to * {@link #cancelAwaitForTouchEnd()}. */ public void onTouchEnd() { if (column != null) { sort(column, scheduledMultisort); cancelAwaitForTouchEnd(); } } /** * Cancel a scheduled sort. */ public void cancelAwaitForTouchEnd() { timer.cancel(); column = null; } } /** * @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() || escalator.getBody().getRowCount() > 0)) { 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. * * @see Column#setWidth(double) * @see Column#setExpandRatio(int) * @see Column#setMinimumWidth(double) * @see Column#setMaximumWidth(double) */ public void schedule() { if (!isScheduled && isAttached() && !(currentDataAvailable.isEmpty() && escalator.getBody().getRowCount() > 0)) { 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."; // Make SelectAllCheckbox visible getSelectionColumn().ifPresent(col -> { if (getDefaultHeaderRow() == null) { return; } HeaderCell headerCell = getDefaultHeaderRow().getCell(col); if (headerCell.getType().equals(GridStaticCellType.WIDGET)) { // SelectAllCheckbox is present already return; } headerCell.setWidget(col.selectAllCheckBox); refreshHeader(); // Paint. }); if (columnsAreGuaranteedToBeWiderThanGrid()) { applyColumnWidths(); } else { applyColumnWidthsWithExpansion(); } // Hide the SelectAllCheckbox if needed getSelectionColumn() .ifPresent(SelectionColumn::doSetSelectAllCheckBoxVisible); // Update latest width to prevent recalculate on height change. lastCalculatedInnerWidth = escalator.getInnerWidth(); } private boolean columnsAreGuaranteedToBeWiderThanGrid() { double freeSpace = escalator.getInnerWidth(); for (Column column : getVisibleColumns()) { /* * Check the width and min width and ensure that no column can * be expected to be narrower than what the resize handler * requires, if one is present. */ if (column.getWidth() >= 0) { if (column.isResizable() && resizeHandleWidth > 0) { freeSpace -= Math.max(resizeHandleWidth, column.getWidth()); } else { freeSpace -= column.getWidth(); } } else if (column.getMinimumWidth() >= 0) { if (column.isResizable() && resizeHandleWidth > 0) { freeSpace -= Math.max(resizeHandleWidth, column.getMinimumWidth()); } else { freeSpace -= column.getMinimumWidth(); } } else if (column.isResizable() && resizeHandleWidth > 0) { freeSpace -= resizeHandleWidth; } } return freeSpace < 0; } 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, true); /* * 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 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, true); } @SuppressWarnings("unchecked") 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 column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; defaultExpandRatios = defaultExpandRatios && (column.getExpandRatio() == -1 || column == selectionColumn); if (isFixedWidth) { // Check for min & max width just to be sure we don't break // the limits double widthFixed = Math.max( Math.min(getMaxWidth(column), widthAsIs), column.getMinimumWidth()); if (column.isResizable() && resizeHandleWidth > 0) { // Ensure the resize handle fits widthFixed = Math.max(widthFixed, resizeHandleWidth); } columnSizes.put(visibleColumns.indexOf(column), widthFixed); reservedPixels += widthFixed; } else { nonFixedColumns.add(column); columnSizes.put(visibleColumns.indexOf(column), -1.0d); } } /* * Set column sizes so that it's possible to measure non-fixed * actual sizes without previously applied expand ratio tweaks, but * don't trigger the element size recalculation before the rest of * this method has also been processed. */ setColumnSizes(columnSizes, false); for (Column column : nonFixedColumns) { final int expandRatio = defaultExpandRatios ? 1 : column.getExpandRatio(); final double maxWidth = getMaxWidth(column); double newWidth; if (column.isMinimumWidthFromContent()) { if (column.isResizable() && resizeHandleWidth > 0) { // Ensure the resize handle fits newWidth = Math.max( Math.min(maxWidth, column.getWidthActual()), resizeHandleWidth); } else { newWidth = Math.min(maxWidth, column.getWidthActual()); } } else { if (column.isResizable() && resizeHandleWidth > 0) { // Ensure the resize handle fits newWidth = resizeHandleWidth; } else { newWidth = 0; } } 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, true); } /* * If pixelsToDistribute > 0 the element size recalculation * isn't done at all, even if some column sizes were set * earlier, but this doesn't appear to be detrimental while * attempting to trigger the recalculation here breaks a * GridEditRow test. */ return; } Object[] result = handleMaximumWidths(totalRatios, pixelsToDistribute, defaultExpandRatios, columnsToExpand, visibleColumns, columnSizes); if (result.length != 3) { // everything handled already return; } // update values based on maximum width handling totalRatios = (int) result[0]; pixelsToDistribute = (double) result[1]; columnSizes = (Map) result[2]; 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. */ columnSizes = distributeRemainingPixels(totalRatios, pixelsToDistribute, defaultExpandRatios, columnsToExpand, visibleColumns, columnSizes); columnSizes = handleMinimumWidths(defaultExpandRatios, columnsToExpand, visibleColumns, columnSizes); // Finally set all the column sizes. setColumnSizes(columnSizes, true); } private Object[] handleMaximumWidths(int totalRatios, double pixelsToDistribute, final boolean defaultExpandRatios, final Set> columnsToExpand, final List> visibleColumns, final Map columnSizes) { /* * 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 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, true); // nothing left to handle return new Object[] {}; } // this must return exactly 3 objects in this precise order return new Object[] { totalRatios, pixelsToDistribute, columnSizes }; } private Map distributeRemainingPixels(int totalRatios, final double pixelsToDistribute, final boolean defaultExpandRatios, final Set> columnsToExpand, final List> visibleColumns, final Map columnSizes) { 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 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; return columnSizes; } private Map handleMinimumWidths( final boolean defaultExpandRatios, final Set> columnsToExpand, final List> visibleColumns, final Map columnSizes) { /* * 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 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. */ int totalRatios = 0; for (Column column : columnsToExpand) { totalRatios += getExpandRatio(column, defaultExpandRatios); } final double pixelsToRemovePerRatio = pixelsToRemoveFromOtherColumns / totalRatios; for (Column column : columnsToExpand) { final double pixelsToRemove = pixelsToRemovePerRatio * getExpandRatio(column, defaultExpandRatios); int colIndex = visibleColumns.indexOf(column); columnSizes.put(colIndex, columnSizes.get(colIndex) - pixelsToRemove); } } while (minWidthsCausedReflows); return columnSizes; } private void setColumnSizes(Map columnSizes, boolean recalculateElementSizes) { // Set all widths at once escalator.getColumnConfiguration().setColumnWidths(columnSizes, recalculateElementSizes); } 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. */ final 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 = 0d; if (contentHeight > 0) { measuredHeight = contentHeight + borderTopAndBottomHeight; } else { Scheduler.get().scheduleFinally(() -> { // make sure the spacer hasn't got removed if (spacer.getElement().getParentElement() != null) { // re-check the height double confirmedContentHeight = WidgetUtil .getRequiredHeightBoundingClientRectDouble( element); if (confirmedContentHeight > 0) { double confirmedMeasuredHeight = confirmedContentHeight + WidgetUtil .getBorderTopAndBottomThickness( spacer.getElement()); escalator.getBody().setSpacer(spacer.getRow(), confirmedMeasuredHeight); if (getHeightMode() == HeightMode.UNDEFINED) { setHeightByRows(getEscalator().getBody() .getRowCount()); } } } }); } 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) { 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 = 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(() -> { selectItem(item); focus(); }); } else { super.onBrowserEvent(event); } } }; KeyDownHandler keyDownHandler = 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 (!WidgetUtil.isDisplayed(header.getElement()) || header.getRowCount() == 0 || !header.getRowElement(0).hasChildNodes()) { getLogger().info( "No header cell available when calculating sidebar button height"); // If the Grid is hidden with styles when this is called the // border height will be off, but it's usually only a matter of // a pixel or so. Removing a style name cannot trigger a full // refresh of the layout, it's developer's responsibility to do // that where needed. double height = header.getDefaultRowHeight() - WidgetUtil.measureVerticalBorder(getElement()) / 2; openCloseButton.setHeight(height + "px"); return; } // Use actual height instead of expected height in case the height // is modified by styles. 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); } } private boolean isInDOM() { return getParent() != null; } @Override protected void onAttach() { super.onAttach(); // Make sure the button will get correct height whenever the Sidebar // is added to the Grid. 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 Map, 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; /** * When several columns are set hidable, don't reset the Sidebar for * every column separately. */ private boolean toggleUpdateTriggered; private void updateColumnHidable(final Column 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 column) { MenuItem toggle = new MenuItem(createHTML(column), true, () -> { hidingColumn = true; column.setHidden(!column.isHidden(), true); hidingColumn = false; }); toggle.addStyleName("column-hiding-toggle"); columnToHidingToggleMap.put(column, toggle); return toggle; } private String createHTML(Column column) { final StringBuilder buf = new StringBuilder(); 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 && !toggleUpdateTriggered) { // This method is called whenever a column is set hidable. If // there are multiple hidable columns, it will get called // separately for all of them. There is no need to update the // order more than once and no other layouting is dependent on // the Sidebar layouting getting finished first, so wait until // all calls have arrived before proceeding further. toggleUpdateTriggered = true; Scheduler.get().scheduleFinally(() -> { int lastIndex = 0; for (Column column : getColumns()) { if (column.isHidable()) { final MenuItem menuItem = columnToHidingToggleMap .get(column); sidebar.menuBar.removeItem(menuItem); sidebar.menuBar.insertItem(menuItem, lastIndex++); } } toggleUpdateTriggered = false; }); } } private void updateHidingToggle(Column 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 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; private boolean recalculateColumnWidthsNeeded = false; /** * 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; /** * Focus handler for Grid's cells. Updates focus style names and handles * focus scrolling. */ 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<>(); /** A set of indices of details to reopen after detach and on attach */ private final Set reattachVisibleDetails = 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); @SuppressWarnings("unchecked") private RendererCellReference rendererCellReference = new RendererCellReference( (RowReference) rowReference); private boolean refreshBodyRequested = false; private double resizeHandleWidth = 0; private DragAndDropHandler.DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { @Override public void onAutoScroll(int scrollDiff) { autoScrollX = scrollDiff; onDragUpdate(null); } @Override public void onAutoScrollReachedMin() { // make sure the drop marker is visible on the left autoScrollX = 0; updateDragDropMarker(clientX); } @Override public void onAutoScrollReachedMax() { // make sure the drop marker is visible on the right autoScrollX = 0; updateDragDropMarker(clientX); } }; /** * Elements for displaying the dragged column(s) and drop marker * properly */ private Element table; private Element tableHeader; /** Marks the column drop location */ private Element dropMarker; /** A copy of the dragged column(s), moves with cursor. */ private Element dragElement; /** Tracks index of the column whose left side the drop would occur */ private int latestColumnDropIndex; /** * Map of possible drop positions for the column and the corresponding * column index. */ private final TreeMap possibleDropPositions = new TreeMap<>(); /** * Makes sure that drag cancel doesn't cause anything unwanted like sort */ private HandlerRegistration columnSortPreventRegistration; private int clientX; /** How much the grid is being auto scrolled while dragging. */ private int autoScrollX; /** Captures the value of the focused column before reordering */ private int focusedColumnIndex; /** Offset caused by the drag and drop marker width */ private double dropMarkerWidthOffset; private void initHeaderDragElementDOM() { if (table == null) { tableHeader = DOM.createTHead(); dropMarker = DOM.createDiv(); tableHeader.appendChild(dropMarker); table = DOM.createTable(); table.appendChild(tableHeader); table.setClassName("header-drag-table"); } // update the style names on each run in case primary name has been // modified tableHeader.setClassName( escalator.getHeader().getElement().getClassName()); dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); int topOffset = 0; for (int i = 0; i < eventCell.getRowIndex(); i++) { topOffset += escalator.getHeader().getRowElement(i) .getFirstChildElement().getOffsetHeight(); } tableHeader.getStyle().setTop(topOffset, Unit.PX); getElement().appendChild(table); dropMarkerWidthOffset = WidgetUtil .getRequiredWidthBoundingClientRectDouble(dropMarker) / 2; } @Override public void onDragUpdate(Event e) { if (e != null) { clientX = WidgetUtil.getTouchOrMouseClientX(e); autoScrollX = 0; } resolveDragElementHorizontalPosition(clientX); updateDragDropMarker(clientX); } private void updateDragDropMarker(final int clientX) { final double scrollLeft = getScrollLeft(); final double cursorXCoordinate = clientX - escalator.getHeader().getElement().getAbsoluteLeft(); final Entry cellEdgeOnRight = possibleDropPositions .ceilingEntry(cursorXCoordinate); final Entry cellEdgeOnLeft = possibleDropPositions .floorEntry(cursorXCoordinate); final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE : cellEdgeOnRight.getKey() - cursorXCoordinate; final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE : cursorXCoordinate - cellEdgeOnLeft.getKey(); double dropMarkerLeft = 0 - scrollLeft; if (diffToRightEdge > diffToLeftEdge) { latestColumnDropIndex = cellEdgeOnLeft.getValue(); dropMarkerLeft += cellEdgeOnLeft.getKey(); } else { latestColumnDropIndex = cellEdgeOnRight.getValue(); dropMarkerLeft += cellEdgeOnRight.getKey(); } dropMarkerLeft += autoScrollX; final double frozenColumnsWidth = autoScroller .getFrozenColumnsWidth(); final double rightBoundaryForDrag = getSidebarBoundaryComparedTo( dropMarkerLeft); final int visibleColumns = getVisibleColumns().size(); // First check if the drop marker should move left because of the // sidebar opening button. this only the case if the grid is // scrolled to the right if (latestColumnDropIndex == visibleColumns && rightBoundaryForDrag < dropMarkerLeft && dropMarkerLeft <= escalator.getInnerWidth()) { dropMarkerLeft = rightBoundaryForDrag - dropMarkerWidthOffset; } else if ( // Check if the drop marker shouldn't be shown at all dropMarkerLeft < frozenColumnsWidth || dropMarkerLeft > Math .min(rightBoundaryForDrag, escalator.getInnerWidth()) || dropMarkerLeft < 0) { dropMarkerLeft = -10000000; } dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); } private void resolveDragElementHorizontalPosition(final int clientX) { double left = clientX - table.getAbsoluteLeft(); // Do not show the drag element beyond a spanned header cell // limitation final Double leftBound = possibleDropPositions.firstKey(); final Double rightBound = possibleDropPositions.lastKey(); final double scrollLeft = getScrollLeft(); if (left + scrollLeft < leftBound) { left = leftBound - scrollLeft + autoScrollX; } else if (left + scrollLeft > rightBound) { left = rightBound - scrollLeft + autoScrollX; } // Do not show the drag element beyond the grid final double sidebarBoundary = getSidebarBoundaryComparedTo(left); final double gridBoundary = escalator.getInnerWidth(); final double rightBoundary = Math.min(sidebarBoundary, gridBoundary); // Do not show on left of the frozen columns (even if scrolled) final int frozenColumnsWidth = (int) autoScroller .getFrozenColumnsWidth(); left = Math.max(frozenColumnsWidth, Math.min(left, rightBoundary)); left -= dragElement.getClientWidth() / 2; dragElement.getStyle().setLeft(left, Unit.PX); } private boolean isSidebarOnDraggedRow() { return eventCell.getRowIndex() == 0 && sidebar.isInDOM() && !sidebar.isOpen(); } /** * Returns the sidebar left coordinate, in relation to the grid. Or * Double.MAX_VALUE if it doesn't cause a boundary. */ private double getSidebarBoundaryComparedTo(double left) { if (isSidebarOnDraggedRow()) { double absoluteLeft = left + getElement().getAbsoluteLeft(); double sidebarLeft = sidebar.getElement().getAbsoluteLeft(); double diff = absoluteLeft - sidebarLeft; if (diff > 0) { return left - diff; } } return Double.MAX_VALUE; } @Override public boolean onDragStart(Event e) { calculatePossibleDropPositions(); if (possibleDropPositions.isEmpty()) { return false; } initHeaderDragElementDOM(); // needs to clone focus and sorting indicators too (UX) dragElement = DOM.clone(eventCell.getElement(), true); dragElement.getStyle().clearWidth(); dropMarker.getStyle().setProperty("height", dragElement.getStyle().getHeight()); tableHeader.appendChild(dragElement); // mark the column being dragged for styling eventCell.getElement().addClassName("dragged"); // mark the floating cell, for styling & testing dragElement.addClassName("dragged-column-header"); // start the auto scroll handler autoScroller.setScrollArea(60); autoScroller.start(e, ScrollAxis.HORIZONTAL, autoScrollerCallback); return true; } @Override public void onDragEnd() { table.removeFromParent(); dragElement.removeFromParent(); eventCell.getElement().removeClassName("dragged"); } @Override public void onDrop() { final int draggedColumnIndex = eventCell.getColumnIndex(); final StaticRow draggedCellRow = header .getRow(eventCell.getRowIndex()); int colspan = draggedCellRow .getSizeOfCellGroup(getColumn(draggedColumnIndex)); colspan = colspan == 0 ? 1 : colspan; if (latestColumnDropIndex != draggedColumnIndex && latestColumnDropIndex != draggedColumnIndex + colspan) { List> columns = getColumns(); List> reordered = new ArrayList<>(); if (draggedColumnIndex < latestColumnDropIndex) { reordered.addAll(columns.subList(0, draggedColumnIndex)); reordered.addAll( columns.subList(draggedColumnIndex + colspan, latestColumnDropIndex)); reordered.addAll(columns.subList(draggedColumnIndex, draggedColumnIndex + colspan)); reordered.addAll(columns.subList(latestColumnDropIndex, columns.size())); } else { reordered.addAll(columns.subList(0, latestColumnDropIndex)); reordered.addAll(columns.subList(draggedColumnIndex, draggedColumnIndex + colspan)); reordered.addAll(columns.subList(latestColumnDropIndex, draggedColumnIndex)); reordered.addAll(columns.subList( draggedColumnIndex + colspan, columns.size())); } // since setColumnOrder will add it anyway! reordered.remove(selectionColumn); // capture focused cell column before reorder Cell focusedCell = cellFocusHandler.getFocusedCell(); if (focusedCell != null) { // take hidden columns into account focusedColumnIndex = getColumns() .indexOf(getVisibleColumn(focusedCell.getColumn())); } @SuppressWarnings("unchecked") Column[] array = reordered .toArray(new Column[reordered.size()]); setColumnOrder(true, array); transferCellFocusOnDrop(); } // else no reordering } private void transferCellFocusOnDrop() { final Cell focusedCell = cellFocusHandler.getFocusedCell(); if (focusedCell != null && focusedCell.getElement() != null) { final int focusedColumnIndexDOM = focusedCell.getColumn(); final int focusedRowIndex = focusedCell.getRow(); final int draggedColumnIndex = eventCell.getColumnIndex(); // transfer focus if it was effected by the new column order final RowContainer rowContainer = escalator .findRowContainer(focusedCell.getElement()); if (focusedColumnIndex == draggedColumnIndex) { // move with the dragged column int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 : latestColumnDropIndex; // remove hidden columns from indexing adjustedDropIndex = getVisibleColumns() .indexOf(getColumn(adjustedDropIndex)); cellFocusHandler.setCellFocus(focusedRowIndex, adjustedDropIndex, rowContainer); } else if (latestColumnDropIndex <= focusedColumnIndex && draggedColumnIndex > focusedColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedColumnIndexDOM + 1, rowContainer); } else if (latestColumnDropIndex > focusedColumnIndex && draggedColumnIndex < focusedColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedColumnIndexDOM - 1, rowContainer); } } } @Override public void onDragCancel() { // cancel next click so that we may prevent column sorting if // mouse was released on top of the dragged cell if (columnSortPreventRegistration == null) { columnSortPreventRegistration = Event .addNativePreviewHandler(event -> { if (event.getTypeInt() == Event.ONCLICK) { event.cancel(); event.getNativeEvent().preventDefault(); columnSortPreventRegistration.removeHandler(); columnSortPreventRegistration = null; } }); } autoScroller.stop(); } /** * Returns the amount of frozen columns. The selection column is always * considered frozen, since it can't be moved. */ private int getSelectionAndFrozenColumnCount() { // no matter if selection column is frozen or not, it is considered // frozen for column dnd reorder if (getSelectionModel() instanceof SelectionModelWithSelectionColumn) { return Math.max(0, getFrozenColumnCount()) + 1; } else { return Math.max(0, getFrozenColumnCount()); } } private void calculatePossibleDropPositions() { possibleDropPositions.clear(); final int draggedColumnIndex = eventCell.getColumnIndex(); final StaticRow draggedCellRow = header .getRow(eventCell.getRowIndex()); final int draggedColumnRightIndex = draggedColumnIndex + draggedCellRow.getCell(eventCell.getColumn()) .getColspan(); final int frozenColumns = getSelectionAndFrozenColumnCount(); final Range draggedCellRange = Range.between(draggedColumnIndex, draggedColumnRightIndex); /* * If the dragged cell intersects with a spanned cell in any other * header or footer row, then the drag is limited inside that * spanned cell. The same rules apply: the cell can't be dropped * inside another spanned cell. The left and right bounds keep track * of the edges of the most limiting spanned cell. */ int leftBound = -1; int rightBound = getColumnCount() + 1; final HashSet unavailableColumnDropIndices = new HashSet<>(); final List> rows = new ArrayList<>(); rows.addAll(header.getRows()); rows.addAll(footer.getRows()); for (StaticRow row : rows) { if (!row.hasSpannedCells()) { continue; } final boolean isDraggedCellRow = row.equals(draggedCellRow); for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) { int colspan = row .getSizeOfCellGroup(getColumn(cellColumnIndex)); if (colspan <= 1) { continue; } final int cellColumnRightIndex = cellColumnIndex + colspan; final Range cellRange = Range.between(cellColumnIndex, cellColumnRightIndex); final boolean intersects = draggedCellRange .intersects(cellRange); if (intersects && !isDraggedCellRow) { // if the currently iterated cell is inside or same as // the dragged cell, then it doesn't restrict the drag if (cellRange.isSubsetOf(draggedCellRange)) { cellColumnIndex = cellColumnRightIndex - 1; continue; } /* * if the dragged cell is a spanned cell and it crosses * with the currently iterated cell without sharing * either start or end then not possible to drag the * cell. */ if (!draggedCellRange.isSubsetOf(cellRange)) { return; } // the spanned cell overlaps the dragged cell (but is // not the dragged cell) if (cellColumnIndex <= draggedColumnIndex && cellColumnIndex > leftBound) { leftBound = cellColumnIndex; } if (cellColumnRightIndex < rightBound) { rightBound = cellColumnRightIndex; } cellColumnIndex = cellColumnRightIndex - 1; } else { // can't drop inside a spanned cell, or this is the // dragged cell while (colspan > 1) { cellColumnIndex++; colspan--; unavailableColumnDropIndices.add(cellColumnIndex); } } } } if (leftBound == rightBound - 1) { return; } double position = autoScroller.getFrozenColumnsWidth(); // iterate column indices and add possible drop positions for (int i = frozenColumns; i < getColumnCount(); i++) { Column column = getColumn(i); if (!unavailableColumnDropIndices.contains(i) && !column.isHidden()) { if (leftBound != -1) { if (i >= leftBound && i <= rightBound) { possibleDropPositions.put(position, i); } } else { possibleDropPositions.put(position, i); } } position += column.getWidthActual(); } if (leftBound == -1) { // add the right side of the last column as columns.size() possibleDropPositions.put(position, getColumnCount()); } } }; /** * Base class for grid columns internally used by the Grid. The user should * use {@link Column} when creating new columns. * * @param * the column type * * @param * the row type */ public abstract static class Column { /** * Default renderer for GridColumns. Renders everything into text * through {@link Object#toString()}. */ private final class DefaultTextRenderer implements Renderer { boolean warned = false; private static final String DEFAULT_RENDERER_WARNING = "This column uses a dummy default TextRenderer. " + "A more suitable renderer should be set using the setRenderer() method."; @Override public void render(RendererCellReference cell, Object data) { if (!warned && !(data instanceof String)) { getLogger().warning( Column.this + ": " + DEFAULT_RENDERER_WARNING); warned = true; } final String text; if (data == null) { text = ""; } else { text = data.toString(); } cell.getElement().setInnerText(text); } } /** * the column is associated with */ private Grid grid; /** * Width of column in pixels as {@link #setWidth(double)} has been * called */ private double widthUser = GridConstants.DEFAULT_COLUMN_WIDTH_PX; /** * Renderer for rendering a value into the cell */ private Renderer bodyRenderer; private boolean sortable = false; private boolean editable = true; private boolean resizable = true; private boolean hidden = false; private boolean hidable = false; private String headerCaption = ""; private String assistiveCaption = null; private String hidingToggleCaption = null; private boolean handleWidgetEvents = false; private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; private double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH; private int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO; private boolean minimumWidthFromContent = true; /** * Constructs a new column with a simple TextRenderer. */ public Column() { setRenderer(new DefaultTextRenderer()); } /** * Constructs a new column with a simple TextRenderer. * * @param caption * The header caption for this column * * @throws IllegalArgumentException * if given header caption is null */ public Column(String caption) throws IllegalArgumentException { this(); setHeaderCaption(caption); } /** * Constructs a new column with a custom renderer. * * @param renderer * The renderer to use for rendering the cells * * @throws IllegalArgumentException * if given Renderer is null */ public Column(Renderer renderer) throws IllegalArgumentException { setRenderer(renderer); } /** * Constructs a new column with a custom renderer. * * @param renderer * The renderer to use for rendering the cells * @param caption * The header caption for this column * * @throws IllegalArgumentException * if given Renderer or header caption is null */ public Column(String caption, Renderer renderer) throws IllegalArgumentException { this(renderer); setHeaderCaption(caption); } /** * Internally used by the grid to set itself * * @param grid */ private void setGrid(Grid grid) { if (this.grid != null && grid != null) { // Trying to replace grid throw new IllegalStateException("Column already is attached " + "to a grid. Remove the column first from the grid " + "and then add it. (in: " + toString() + ")"); } if (this.grid != null) { this.grid.recalculateColumnWidths(); } this.grid = grid; if (this.grid != null) { this.grid.recalculateColumnWidths(); } } /** * Sets a header caption for this column. * * @param caption * The header caption for this column * @return the column itself * */ public Column setHeaderCaption(String caption) { if (caption == null) { caption = ""; } if (!this.headerCaption.equals(caption)) { this.headerCaption = caption; if (grid != null) { updateHeader(); } } return this; } /** * Returns the current header caption for this column. * * @since 7.6 * @return the header caption string */ public String getHeaderCaption() { return headerCaption; } /** * Sets the header aria-label for this column. * * @param caption * The header aria-label for this column * @return the column itself * * @since 8.2 */ public Column setAssistiveCaption(String caption) { if (!Objects.equals(this.assistiveCaption, caption)) { this.assistiveCaption = caption; if (grid != null) { grid.getHeader().requestSectionRefresh(); } } return this; } /** * Returns the current header aria-label for this column. * * @return the header aria-label string * * @since 8.2 */ public String getAssistiveCaption() { return assistiveCaption; } private void updateHeader() { HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { setDefaultHeaderContent(row.getCell(this)); if (isHidable()) { grid.columnHider.updateHidingToggle(this); } } } /** * Returns the data that should be rendered into the cell. By default * returning Strings and Widgets are supported. If the return type is a * String then it will be treated as preformatted text. *

* To support other types you will need to pass a custom renderer to the * column via the column constructor. * * @param row * The row object that provides the cell content. * * @return The cell content */ public abstract C getValue(T row); /** * The renderer to render the cell with. By default renders the data as * a String or adds the widget into the cell if the column type is of * widget type. * * @return The renderer to render the cell content with */ public Renderer getRenderer() { return bodyRenderer; } /** * Sets a custom {@link Renderer} for this column. * * @param renderer * The renderer to use for rendering the cells * @return the column itself * * @throws IllegalArgumentException * if given Renderer is null */ public Column setRenderer(Renderer renderer) throws IllegalArgumentException { if (renderer == null) { throw new IllegalArgumentException("Renderer cannot be null."); } if (renderer != bodyRenderer) { // Variables used to restore removed column. boolean columnRemoved = false; double widthInConfiguration = 0.0d; ColumnConfiguration conf = null; int index = 0; if (!isHidden() && grid != null && (bodyRenderer instanceof WidgetRenderer || renderer instanceof WidgetRenderer)) { // Column needs to be recreated. index = grid.getVisibleColumns().indexOf(this); conf = grid.escalator.getColumnConfiguration(); widthInConfiguration = conf.getColumnWidth(index); conf.removeColumns(index, 1); columnRemoved = true; } // Complex renderers need to be destroyed. if (bodyRenderer instanceof ComplexRenderer) { ((ComplexRenderer) bodyRenderer).destroy(); } bodyRenderer = renderer; if (columnRemoved) { // Restore the column. conf.insertColumns(index, 1); conf.setColumnWidth(index, widthInConfiguration); } if (!isHidden() && grid != null) { grid.requestRefreshBody(); } } return this; } /** * Sets the pixel width of the column. Use a negative value for the grid * to autosize column based on content and available space. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. *

* If the column is currently {@link #isHidden() hidden}, then this set * width has effect only once the column has been made visible again. * * @param pixels * the width in pixels or negative for auto sizing * @return this column */ public Column setWidth(double pixels) { if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) { widthUser = pixels; if (!isHidden()) { scheduleColumnWidthRecalculator(); } } return this; } void doSetWidth(double pixels) { assert !isHidden() : "applying width for a hidden column"; if (grid != null) { int index = grid.getVisibleColumns().indexOf(this); ColumnConfiguration conf = grid.escalator .getColumnConfiguration(); conf.setColumnWidth(index, pixels); } } /** * Returns the pixel width of the column as given by the user. *

* Note: If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. *

* Note: Returns the value, even if the column is currently * {@link #isHidden() hidden}. * * @return pixel width of the column, or a negative number if the column * width has been automatically calculated. * @see #setWidth(double) * @see #getWidthActual() */ public double getWidth() { return widthUser; } /** * Returns the effective pixel width of the column. *

* This differs from {@link #getWidth()} only when the column has been * automatically resized, or when the column is currently * {@link #isHidden() hidden}, when the value is 0. * * @return pixel width of the column. */ public double getWidthActual() { if (isHidden()) { return 0; } return grid.escalator.getColumnConfiguration().getColumnWidthActual( grid.getVisibleColumns().indexOf(this)); } void reapplyWidth() { scheduleColumnWidthRecalculator(); } /** * Sets whether the column should be sortable by the user. The grid can * be sorted by a sortable column by clicking or tapping the column's * default header. Programmatic sorting using the Grid#sort methods is * not affected by this setting. * * @param sortable * {@code true} if the user should be able to sort the * column, {@code false} otherwise * @return the column itself */ public Column setSortable(boolean sortable) { if (this.sortable != sortable) { this.sortable = sortable; if (grid != null) { grid.getHeader().requestSectionRefresh(); } } return this; } /** * Returns whether the user can sort the grid by this column. *

* Note: it is possible to sort by this column programmatically * using the Grid#sort methods regardless of the returned value. * * @return {@code true} if the column is sortable by the user, * {@code false} otherwise */ public boolean isSortable() { return sortable; } /** * Sets whether this column can be resized by the user. * * @since 7.6 * * @param resizable * {@code true} if this column should be resizable, * {@code false} otherwise * @return this column */ public Column setResizable(boolean resizable) { if (this.resizable != resizable) { this.resizable = resizable; if (grid != null) { grid.getHeader().requestSectionRefresh(); } } return this; } /** * Returns whether this column can be resized by the user. Default is * {@code true}. *

* Note: the column can be programmatically resized using * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless * of the returned value. * * @since 7.6 * * @return {@code true} if this column is resizable, {@code false} * otherwise */ public boolean isResizable() { return grid.isEnabled() && resizable; } /** * Hides or shows the column. By default columns are visible before * explicitly hiding them. * * @since 7.5.0 * @param hidden * true to hide the column, false * to show * @return this column */ public Column setHidden(boolean hidden) { setHidden(hidden, false); return this; } private void setHidden(boolean hidden, boolean userOriginated) { if (this.hidden != hidden) { if (grid == null) { // Not yet attached so just update the flag so that a column // can initially be hidden this.hidden = hidden; return; } if (hidden) { grid.escalator.getColumnConfiguration().removeColumns( grid.getVisibleColumns().indexOf(this), 1); this.hidden = hidden; } else { this.hidden = hidden; int columnIndex = grid.getVisibleColumns().indexOf(this); grid.escalator.getColumnConfiguration() .insertColumns(columnIndex, 1); // make sure column is set to frozen if it needs to be, // escalator doesn't handle situation where the added column // would be the last frozen column int gridFrozenColumns = grid.getFrozenColumnCount(); // Correct column index for multiselect mode if (grid.getSelectionColumn().isPresent()) { gridFrozenColumns++; } int escalatorFrozenColumns = grid.escalator .getColumnConfiguration().getFrozenColumnCount(); if (gridFrozenColumns > escalatorFrozenColumns && escalatorFrozenColumns == columnIndex && grid.getColumns() .indexOf(this) < gridFrozenColumns) { grid.escalator.getColumnConfiguration() .setFrozenColumnCount(++escalatorFrozenColumns); } } grid.columnHider.updateHidingToggle(this); grid.header.updateColSpans(); grid.footer.updateColSpans(); scheduleColumnWidthRecalculator(); this.grid.fireEvent(new ColumnVisibilityChangeEvent<>(this, hidden, userOriginated)); } } /** * Returns whether this column is hidden. Default is {@code false}. * * @since 7.5.0 * @return {@code true} if the column is currently hidden, {@code false} * otherwise */ public boolean isHidden() { return hidden; } /** * Set whether it is possible for the user to hide this column or not. * Default is {@code false}. *

* Note: it is still possible to hide the column * programmatically using {@link #setHidden(boolean)}. * * @since 7.5.0 * @param hidable * {@code true} the user can hide this column, {@code false} * otherwise * @return this column */ public Column setHidable(boolean hidable) { if (this.hidable != hidable) { this.hidable = hidable; grid.columnHider.updateColumnHidable(this); } return this; } /** * Is it possible for the the user to hide this column. Default is * {@code false}. *

* Note: the column can be programmatically hidden using * {@link #setHidden(boolean)} regardless of the returned value. * * @since 7.5.0 * @return true if the user can hide the column, * false if not */ public boolean isHidable() { return hidable; } /** * Sets the hiding toggle's caption for this column. Shown in the toggle * for this column in the grid's sidebar when the column is * {@link #isHidable() hidable}. *

* The default value is null. In this case the header * caption is used, see {@link #setHeaderCaption(String)}. * * @since 7.5.0 * @param hidingToggleCaption * the caption for the hiding toggle for this column * @return this column */ public Column setHidingToggleCaption(String hidingToggleCaption) { this.hidingToggleCaption = hidingToggleCaption; if (isHidable()) { grid.columnHider.updateHidingToggle(this); } return this; } /** * Gets the hiding toggle caption for this column. * * @since 7.5.0 * @see #setHidingToggleCaption(String) * @return the hiding toggle's caption for this column */ public String getHidingToggleCaption() { return hidingToggleCaption; } @Override public String toString() { String details = ""; if (headerCaption != null && !headerCaption.isEmpty()) { details += "header:\"" + headerCaption + "\" "; } else { details += "header:empty "; } if (grid != null) { int index = grid.getColumns().indexOf(this); if (index != -1) { details += "attached:#" + index + " "; } else { details += "attached:unindexed "; } } else { details += "detached "; } details += "sortable:" + sortable + " "; return getClass().getSimpleName() + "[" + details.trim() + "]"; } /** * Sets the minimum width for this column. *

* This defines the minimum guaranteed pixel width of the column * when it is set to expand. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param pixels * the minimum width * @return this column */ public Column setMinimumWidth(double pixels) { final double maxwidth = getMaximumWidth(); if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) { throw new IllegalArgumentException("New minimum width (" + pixels + ") was greater than maximum width (" + maxwidth + ")"); } if (minimumWidthPx != pixels) { minimumWidthPx = pixels; scheduleColumnWidthRecalculator(); } return this; } /** * Sets whether the width of the contents in the column should be * considered minimum width for this column. *

* If this is set to true (default for backwards * compatibility), then a column will not shrink to smaller than the * width required to show the contents available when calculating the * widths (first N rows). *

* If this is set to false, then a column will shrink if * necessary to the minimum width defined by * {@link #setMinimumWidth(double)} when it is set to expand. * * @param minimumWidthFromContent * true to reserve space for all contents, * false to allow the column to shrink smaller * than the contents * @since 8.1 */ public void setMinimumWidthFromContent( boolean minimumWidthFromContent) { this.minimumWidthFromContent = minimumWidthFromContent; } /** * Gets whether the width of the contents in the column should be * considered minimum width for this column. * * @return true to reserve space for all contents, * false to allow the column to shrink smaller than * the contents * @since 8.1 */ public boolean isMinimumWidthFromContent() { return minimumWidthFromContent; } /** * Sets the maximum width for this column. *

* This defines the maximum allowed pixel width of the column when * it is set to expand. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param pixels * the maximum width * @return this column */ public Column setMaximumWidth(double pixels) { final double minwidth = getMinimumWidth(); if (pixels >= 0 && pixels < minwidth && minwidth >= 0) { throw new IllegalArgumentException("New maximum width (" + pixels + ") was less than minimum width (" + minwidth + ")"); } if (maximumWidthPx != pixels) { maximumWidthPx = pixels; scheduleColumnWidthRecalculator(); } return this; } /** * Sets the ratio with which the column expands. *

* By default, all columns expand equally (treated as if all of them had * an expand ratio of 1). Once at least one column gets a defined expand * ratio, the implicit expand ratio is removed, and only the defined * expand ratios are taken into account. *

* If a column has a defined width ({@link #setWidth(double)}), it * overrides this method's effects. *

* Example: A grid with three columns, with expand ratios 0, 1 * and 2, respectively. The column with a ratio of 0 is exactly * as wide as its contents requires. The column with a ratio of * 1 is as wide as it needs, plus a third of any excess * space, bceause we have 3 parts total, and this column * reservs only one of those. The column with a ratio of 2, is as wide * as it needs to be, plus two thirds of the excess * width. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param ratio * the expand ratio of this column. {@code 0} to not have it * expand at all. A negative number to clear the expand * value. * @return this column */ public Column setExpandRatio(int ratio) { if (expandRatio != ratio) { expandRatio = ratio; scheduleColumnWidthRecalculator(); } return this; } /** * Clears the column's expand ratio. *

* Same as calling {@link #setExpandRatio(int) setExpandRatio(-1)} * * @return this column */ public Column clearExpandRatio() { return setExpandRatio(-1); } /** * Gets the minimum width for this column. * * @return the minimum width for this column * @see #setMinimumWidth(double) */ public double getMinimumWidth() { return minimumWidthPx; } /** * Gets the maximum width for this column. * * @return the maximum width for this column * @see #setMaximumWidth(double) */ public double getMaximumWidth() { return maximumWidthPx; } /** * Gets the expand ratio for this column. * * @return the expand ratio for this column * @see #setExpandRatio(int) */ public int getExpandRatio() { return expandRatio; } /** * Sets whether the values in this column should be editable by the user * when the row editor is active. By default columns are editable. * * @param editable * {@code true} to set this column editable, {@code false} * otherwise * @return this column * * @throws IllegalStateException * if the editor is currently active * * @see Grid#editRow(int) * @see Grid#isEditorActive() */ public Column setEditable(boolean editable) { if (editable != this.editable && grid != null && grid.isEditorActive()) { throw new IllegalStateException( "Cannot change column editable status while the editor is active"); } this.editable = editable; return this; } /** * Returns whether the values in this column are editable by the user * when the row editor is active. * * @return {@code true} if this column is editable, {@code false} * otherwise * * @see #setEditable(boolean) */ public boolean isEditable() { return editable; } private void scheduleColumnWidthRecalculator() { if (grid != null) { grid.recalculateColumnWidths(); } else { /* * NOOP * * Since setGrid() will call reapplyWidths as the colum is * attached to a grid, it will call setWidth, which, in turn, * will call this method again. Therefore, it's guaranteed that * the recalculation is scheduled eventually, once the column is * attached to a grid. */ } } /** * Resets the default header cell contents to column header captions. * * @since 7.5.1 * @param cell * default header cell for this column */ protected void setDefaultHeaderContent(HeaderCell cell) { cell.setText(headerCaption); } /** * Returns whether Grid should handle events from Widgets in this * Column. * * @return {@code true} to handle events from widgets; {@code false} to * not * @since 8.3 */ public boolean isHandleWidgetEvents() { return handleWidgetEvents; } /** * Sets whether Grid should handle events from Widgets in this Column. * * @param handleWidgetEvents * {@code true} to let grid handle events from widgets; * {@code false} to not * * @since 8.3 */ public void setHandleWidgetEvents(boolean handleWidgetEvents) { this.handleWidgetEvents = handleWidgetEvents; } } /** * EscalatorUpdater implementation for Grid's body section. * * @see EscalatorUpdater * */ protected class BodyUpdater implements EscalatorUpdater { @Override public void preAttach(Row row, Iterable cellsToAttach) { int rowIndex = row.getRow(); rowReference.set(rowIndex, getDataSource().getRow(rowIndex), row.getElement()); for (FlyweightCell cell : cellsToAttach) { Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { Column column = getVisibleColumn( cell.getColumn()); rendererCellReference.set(cell, getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .init(rendererCellReference); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error initing cell in column " + cell.getColumn(), e); } } } } @Override public void postAttach(Row row, Iterable attachedCells) { for (FlyweightCell cell : attachedCells) { Renderer renderer = findRenderer(cell); if (renderer instanceof WidgetRenderer) { try { WidgetRenderer widgetRenderer = (WidgetRenderer) renderer; Widget widget = widgetRenderer.createWidget(); assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget."; assert widget .getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached."; assert cell.getElement() .getChildCount() == 0 : "Cell content should be empty when adding Widget"; // Physical attach cell.getElement().appendChild(widget.getElement()); // Logical attach setParent(widget, Grid.this); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error attaching child widget in column " + cell.getColumn(), e); } } } } @Override @SuppressWarnings("unchecked") public void update(Row row, Iterable cellsToUpdate) { int rowIndex = row.getRow(); TableRowElement rowElement = row.getElement(); T rowData = dataSource.getRow(rowIndex); boolean hasData = rowData != null; /* * TODO could be more efficient to build a list of all styles that * should be used and update the element only once instead of * attempting to update only the ones that have changed. */ // Assign stylename for rows with data boolean usedToHaveData = rowElement .hasClassName(rowHasDataStyleName); if (usedToHaveData != hasData) { setStyleName(rowElement, rowHasDataStyleName, hasData); } boolean isEvenIndex = row.getRow() % 2 == 0; setStyleName(rowElement, rowStripeStyleName, !isEvenIndex); rowReference.set(rowIndex, rowData, rowElement); boolean isSelected = hasData && isSelected(rowData); if (Grid.this.selectionModel.isSelectionAllowed()) { rowElement.setAttribute("aria-selected", String.valueOf(isSelected)); } else { rowElement.removeAttribute("aria-selected"); } if (hasData) { setStyleName(rowElement, rowSelectedStyleName, isSelected); if (rowStyleGenerator != null) { try { String rowStylename = rowStyleGenerator .getStyle(rowReference); setCustomStyleName(rowElement, rowStylename); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error generating styles for row " + row.getRow(), e); } } else { // Remove in case there was a generator previously setCustomStyleName(rowElement, null); } } else if (usedToHaveData) { setStyleName(rowElement, rowSelectedStyleName, false); setCustomStyleName(rowElement, null); } cellFocusHandler.updateFocusedRowStyle(row); for (FlyweightCell cell : cellsToUpdate) { Column column = getVisibleColumn(cell.getColumn()); final int columnIndex = getColumns().indexOf(column); assert column != null : "Column was not found from cell (" + cell.getColumn() + "," + cell.getRow() + ")"; cellFocusHandler.updateFocusedCellStyle(cell, escalator.getBody()); if (hasData && cellStyleGenerator != null) { try { cellReference.set(cell.getColumn(), columnIndex, column); String generatedStyle = cellStyleGenerator .getStyle(cellReference); setCustomStyleName(cell.getElement(), generatedStyle); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error generating style for cell in column " + cell.getColumn(), e); } } else if (hasData || usedToHaveData) { setCustomStyleName(cell.getElement(), null); } @SuppressWarnings("rawtypes") Renderer renderer = column.getRenderer(); try { rendererCellReference.set(cell, columnIndex, column); if (renderer instanceof ComplexRenderer) { // Hide cell content if needed @SuppressWarnings("rawtypes") ComplexRenderer clxRenderer = (ComplexRenderer) renderer; if (hasData) { if (!usedToHaveData) { // Prepare cell for rendering clxRenderer.setContentVisible( rendererCellReference, true); } Object value = column.getValue(rowData); clxRenderer.render(rendererCellReference, value); } else { // Prepare cell for no data clxRenderer.setContentVisible(rendererCellReference, false); } } else if (hasData) { // Simple renderers just render Object value = column.getValue(rowData); renderer.render(rendererCellReference, value); } else { // Clear cell if there is no data cell.getElement().removeAllChildren(); } } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error rendering cell in column " + cell.getColumn(), e); } } } @Override public void preDetach(Row row, Iterable cellsToDetach) { for (FlyweightCell cell : cellsToDetach) { Renderer renderer = findRenderer(cell); if (renderer instanceof WidgetRenderer) { try { Widget w = WidgetUtil.findWidget( cell.getElement().getFirstChildElement()); if (w != null) { // Logical detach setParent(w, null); // Physical detach cell.getElement().removeChild(w.getElement()); } } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error detaching widget in column " + cell.getColumn(), e); } } } } @Override public void postDetach(Row row, Iterable detachedCells) { int rowIndex = row.getRow(); // Passing null row data since it might not exist in the data source // any more rowReference.set(rowIndex, null, row.getElement()); for (FlyweightCell cell : detachedCells) { Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { Column column = getVisibleColumn( cell.getColumn()); rendererCellReference.set(cell, getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .destroy(rendererCellReference); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error destroying cell in column " + cell.getColumn(), e); } } } } } /** * EscalatorUpdater implementation for Grid's static sections (header and * footer). * * @see EscalatorUpdater * */ protected class StaticSectionUpdater implements EscalatorUpdater { private StaticSection section; private RowContainer container; /** * Constructs an updater instance for the given section. * * @param section * the section that needs an updater * @param container * the row container of the given section */ public StaticSectionUpdater(StaticSection section, RowContainer container) { super(); this.section = section; this.container = container; } @Override public void update(Row row, Iterable cellsToUpdate) { StaticSection.StaticRow staticRow = section.getRow(row.getRow()); final List> columns = getVisibleColumns(); setCustomStyleName(row.getElement(), staticRow.getStyleName()); for (FlyweightCell cell : cellsToUpdate) { final StaticSection.StaticCell metadata = staticRow .getCell(columns.get(cell.getColumn())); // Decorate default row with sorting indicators if (staticRow instanceof HeaderRow) { addAriaLabelToHeaderRow(cell); addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell); } // Assign colspan to cell before rendering cell.setColSpan(metadata.getColspan()); Element td = cell.getElement(); td.removeAllChildren(); setCustomStyleName(td, metadata.getStyleName()); Element content; // Wrap text or html content in default header to isolate // the content from the possible column resize drag handle // next to it GridStaticCellType metaDataType = metadata.getType(); if (metaDataType != GridStaticCellType.WIDGET) { content = DOM.createDiv(); if (staticRow instanceof HeaderRow) { content.setClassName(getStylePrimaryName() + "-column-header-content"); if (((HeaderRow) staticRow).isDefault()) { content.setClassName(content.getClassName() + " " + getStylePrimaryName() + "-column-default-header-content"); } } else if (staticRow instanceof FooterRow) { content.setClassName(getStylePrimaryName() + "-column-footer-content"); } else { getLogger().severe("Unhandled static row type " + staticRow.getClass().getCanonicalName()); } td.appendChild(content); } else { content = td; } switch (metaDataType) { case TEXT: content.setInnerText(metadata.getText()); break; case HTML: content.setInnerHTML(metadata.getHtml()); break; case WIDGET: preDetach(row, Arrays.asList(cell)); content.setInnerHTML(""); postAttach(row, Arrays.asList(cell)); break; default: getLogger().severe("Unhandled metadata type: " + (metaDataType == null ? "null" : metaDataType.name())); break; } // XXX: Should add only once in preAttach/postAttach or when // resizable status changes // Only add resize handles to default header row for now if (columns.get(cell.getColumn()).isResizable() && staticRow instanceof HeaderRow && ((HeaderRow) staticRow).isDefault()) { final DivElement resizeElement = Document.get() .createDivElement(); resizeElement.addClassName(getStylePrimaryName() + "-column-resize-simple-indicator"); final int column = cell.getColumn(); final DragHandle dragger = new DragHandle( getStylePrimaryName() + "-column-resize-handle"); dragger.addTo(td); // Save the newest resize handle's width with the assumption // that all the resize handles are the same size. This is // used in column's minimum width calculations, so the // border of the cell is also included. resizeHandleWidth = dragger.getElement().getOffsetWidth() + WidgetUtil.getBorderLeftAndRightThickness(td); final DragHandleCallback simpleResizeMode = createSimpleResizeModeCallback( column, dragger, resizeElement); final DragHandleCallback animatedResizeMode = createAnimatedResizeModeCallback( column); // DragHandle gets assigned a 'master callback' that // delegates functionality to the correct case-specific // implementation dragger.setCallback(createResizeModeAwareDragHandleCallback( simpleResizeMode, animatedResizeMode)); } cellFocusHandler.updateFocusedCellStyle(cell, container); } } // Common functionality for drag handle callback // implementations private abstract class AbstractDHCallback implements DragHandleCallback { protected Column col; protected double initialWidth = 0; protected double minCellWidth; protected double width; AbstractDHCallback(int column) { col = getVisibleColumn(column); } protected void dragStarted() { initialWidth = col.getWidthActual(); width = initialWidth; minCellWidth = escalator .getMinCellWidth(getVisibleColumns().indexOf(col)); for (Column c : getVisibleColumns()) { if (selectionColumn == c) { // Don't modify selection column. continue; } if (c.getWidth() < 0) { c.setWidth(c.getWidthActual()); fireEvent(new ColumnResizeEvent<>(c)); } } WidgetUtil.setTextSelectionEnabled(getElement(), false); } protected void dragEnded() { WidgetUtil.setTextSelectionEnabled(getElement(), true); } } private DragHandleCallback createSimpleResizeModeCallback( final int column, final DragHandle dragger, final DivElement resizeElement) { return new AbstractDHCallback(column) { @Override protected void dragEnded() { super.dragEnded(); dragger.getElement().removeChild(resizeElement); } @Override public void onStart() { dragStarted(); dragger.getElement().appendChild(resizeElement); resizeElement.getStyle() .setLeft((dragger.getElement().getOffsetWidth() - resizeElement.getOffsetWidth()) * .5, Unit.PX); resizeElement.getStyle() .setHeight(col.grid.getOffsetHeight(), Unit.PX); } @Override public void onUpdate(double deltaX, double deltaY) { width = Math.max(minCellWidth, initialWidth + deltaX); resizeElement.getStyle() .setLeft( (dragger.getElement().getOffsetWidth() - resizeElement.getOffsetWidth()) * .5 + (width - initialWidth), Unit.PX); } @Override public void onCancel() { dragEnded(); } @Override public void onComplete() { dragEnded(); col.setWidth(width); // Need to wait for column width recalculation // scheduled by setWidth() before firing the event Scheduler.get().scheduleDeferred( () -> fireEvent(new ColumnResizeEvent<>(col))); } }; } private DragHandleCallback createAnimatedResizeModeCallback( final int column) { return new AbstractDHCallback(column) { @Override public void onStart() { dragStarted(); } @Override public void onUpdate(double deltaX, double deltaY) { width = Math.max(minCellWidth, initialWidth + deltaX); col.setWidth(width); } @Override public void onCancel() { dragEnded(); col.setWidth(initialWidth); } @Override public void onComplete() { dragEnded(); col.setWidth(width); fireEvent(new ColumnResizeEvent<>(col)); } }; } private DragHandleCallback createResizeModeAwareDragHandleCallback( final DragHandleCallback simpleResizeMode, final DragHandleCallback animatedResizeMode) { return new DragHandleCallback() { private DragHandleCallback currentCallback; @Override public void onStart() { switch (getColumnResizeMode()) { case SIMPLE: currentCallback = simpleResizeMode; break; case ANIMATED: currentCallback = animatedResizeMode; break; default: throw new UnsupportedOperationException( "Support for current column resize mode is not yet implemented"); } currentCallback.onStart(); } @Override public void onUpdate(double deltaX, double deltaY) { currentCallback.onUpdate(deltaX, deltaY); } @Override public void onCancel() { currentCallback.onCancel(); } @Override public void onComplete() { currentCallback.onComplete(); } }; } private void addAriaLabelToHeaderRow(FlyweightCell cell) { Element cellElement = cell.getElement(); final Column column = getVisibleColumn(cell.getColumn()); if (column.getAssistiveCaption() != null) { cellElement.setAttribute("aria-label", column.getAssistiveCaption()); } else { cellElement.removeAttribute("aria-label"); } } private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow, FlyweightCell cell) { Element cellElement = cell.getElement(); boolean sortedBefore = cellElement.hasClassName("sort-asc") || cellElement.hasClassName("sort-desc"); cleanup(cell); if (!headerRow.isDefault()) { // Nothing more to do if not in the default row return; } final Column column = getVisibleColumn(cell.getColumn()); SortOrder sortingOrder = getSortOrder(column); boolean sortable = column.isSortable(); if (sortable) { cellElement.addClassName("sortable"); cellElement.setAttribute("aria-sort", "none"); } if (!sortable || sortingOrder == null) { // Only apply sorting indicators to sortable header columns return; } if (SortDirection.ASCENDING == sortingOrder.getDirection()) { cellElement.addClassName("sort-asc"); cellElement.setAttribute("aria-sort", "ascending"); } else { cellElement.addClassName("sort-desc"); cellElement.setAttribute("aria-sort", "descending"); } int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder); if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) { // Show sort order indicator if column is // sorted and other sorted columns also exists. cellElement.setAttribute("sort-order", String.valueOf(sortIndex + 1)); cellElement.setAttribute("aria-sort", "other"); } if (!sortedBefore) { verifyColumnWidth(column); } } /** * Sort indicator requires a bit more space from the cell than normally. * This method check that the now sorted column has enough width. * * @param column * sorted column */ private void verifyColumnWidth(Column column) { int colIndex = getColumns().indexOf(column); double minWidth = escalator.getMinCellWidth(colIndex); if (column.getWidthActual() < minWidth) { // Fix column size escalator.getColumnConfiguration().setColumnWidth(colIndex, minWidth); fireEvent(new ColumnResizeEvent<>(column)); } } /** * Finds the sort order for this column */ private SortOrder getSortOrder(Column column) { for (SortOrder order : Grid.this.getSortOrder()) { if (order.getColumn() == column) { return order; } } return null; } private void cleanup(FlyweightCell cell) { Element cellElement = cell.getElement(); cellElement.removeAttribute("sort-order"); cellElement.removeAttribute("aria-sort"); cellElement.removeClassName("sort-desc"); cellElement.removeClassName("sort-asc"); cellElement.removeClassName("sortable"); } @Override public void preAttach(Row row, Iterable cellsToAttach) { } @Override public void postAttach(Row row, Iterable attachedCells) { StaticSection.StaticRow gridRow = section.getRow(row.getRow()); List> columns = getVisibleColumns(); for (FlyweightCell cell : attachedCells) { StaticSection.StaticCell metadata = gridRow .getCell(columns.get(cell.getColumn())); /* * If the cell contains widgets that are not currently attached * then attach them now. */ if (GridStaticCellType.WIDGET.equals(metadata.getType())) { final Widget widget = metadata.getWidget(); if (widget != null && !widget.isAttached()) { getGrid().attachWidget(metadata.getWidget(), cell.getElement()); } } } } @Override public void preDetach(Row row, Iterable cellsToDetach) { if (section.getRowCount() > row.getRow()) { StaticSection.StaticRow gridRow = section .getRow(row.getRow()); List> columns = getVisibleColumns(); for (FlyweightCell cell : cellsToDetach) { StaticSection.StaticCell metadata = gridRow .getCell(columns.get(cell.getColumn())); if (GridStaticCellType.WIDGET.equals(metadata.getType()) && metadata.getWidget() != null && metadata.getWidget().isAttached()) { getGrid().detachWidget(metadata.getWidget()); } } } } /** * Returns the Grid instance for this updater. * * @return the grid that is being updated */ @SuppressWarnings("rawtypes") protected Grid getGrid() { return section.grid; } @Override public void postDetach(Row row, Iterable detachedCells) { } }; /** * Creates a new instance. */ public Grid() { initWidget(escalator); getElement().setTabIndex(0); cellFocusHandler = new CellFocusHandler(); setStylePrimaryName(STYLE_NAME); setAriaRole("grid"); escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); escalator.getBody().setEscalatorUpdater(createBodyUpdater()); escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); header.setGrid(this); HeaderRow defaultRow = header.appendRow(); header.setDefaultRow(defaultRow); footer.setGrid(this); editor.setGrid(this); setSelectionModel(new SelectionModel.NoSelectionModel<>()); escalator.getBody().setSpacerUpdater(gridSpacerUpdater); escalator.addScrollHandler(event -> fireEvent(new ScrollEvent())); escalator.addRowVisibilityChangeHandler(event -> { if (dataSource != null && dataSource.size() != 0) { dataSource.ensureAvailability(event.getFirstVisibleRow(), event.getVisibleRowCount()); } }); escalator.addVerticalScrollbarVisibilityChangeHandler(event -> { if (!(currentDataAvailable.isEmpty() && escalator.getBody().getRowCount() > 0)) { recalculateColumnWidths(); } else { recalculateColumnWidthsNeeded = true; } }); // Default action on SelectionEvents. Refresh the body so changed // become visible. addSelectionHandler(new SelectionHandler() { @Override public void onSelect(SelectionEvent event) { refreshBody(); } }); addSpacerIndexChangedHandler(new SpacerIndexChangedHandler() { @Override public void onSpacerIndexChanged(SpacerIndexChangedEvent event) { // remove old index and add new index visibleDetails.remove(event.getOldIndex()); visibleDetails.add(event.getNewIndex()); } }); // Sink header events and key events sinkEvents(getHeader().getConsumedEvents()); sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP, BrowserEvents.KEYPRESS, BrowserEvents.DBLCLICK, BrowserEvents.MOUSEDOWN, BrowserEvents.CLICK)); // Make ENTER and SHIFT+ENTER in the header perform sorting addHeaderKeyUpHandler(event -> { if (event.getNativeKeyCode() != KeyCodes.KEY_ENTER) { return; } if (getHeader().getRow(event.getFocusedCell().getRowIndex()) .isDefault()) { // Only sort for enter on the default header sorter.sort(event.getFocusedCell().getColumn(), event.isShiftKeyDown()); } }); browserEventHandlers.addAll(Arrays.asList( // Opening, closing and navigating in the editor new EditorEventHandler(), // Keyboard and click handlers, Escalator events new SuperEventHandler(), // Column reordering via header drag&drop new HeaderCellDragStartHandler(), // Column sorting via header click new HeaderDefaultRowEventHandler(), // Invoking event-aware renderers new RendererEventHandler(), // Moving cell focus by keyboard or mouse new CellFocusEventHandler())); } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { if (enabled == this.enabled) { return; } this.enabled = enabled; getElement().setTabIndex(enabled ? 0 : -1); // Editor save and cancel buttons need to be disabled. boolean editorOpen = editor.getState() != State.INACTIVE; if (editorOpen) { editor.setGridEnabled(enabled); } sidebar.setEnabled(enabled); getEscalator().setScrollLocked(Direction.VERTICAL, !enabled || editorOpen); getEscalator().setScrollLocked(Direction.HORIZONTAL, !enabled); getHeader().requestSectionRefresh(); fireEvent(new GridEnabledEvent(enabled)); } /** * Sets the column resize mode to use. The default mode is * {@link ColumnResizeMode.ANIMATED}. * * @param mode * a ColumnResizeMode value * * @since 7.7.5 */ public void setColumnResizeMode(ColumnResizeMode mode) { columnResizeMode = mode; } /** * Returns the current column resize mode. The default mode is * {@link ColumnResizeMode.ANIMATED}. * * @return a ColumnResizeMode value * @since 7.7.5 */ public ColumnResizeMode getColumnResizeMode() { return columnResizeMode; } @Override public void setStylePrimaryName(String style) { super.setStylePrimaryName(style); escalator.setStylePrimaryName(style); editor.setStylePrimaryName(style); sidebar.setStylePrimaryName(style + "-sidebar"); sidebar.addStyleName("v-contextmenu"); String rowStyle = getStylePrimaryName() + "-row"; rowHasDataStyleName = rowStyle + "-has-data"; rowSelectedStyleName = rowStyle + "-selected"; rowStripeStyleName = rowStyle + "-stripe"; cellFocusStyleName = getStylePrimaryName() + "-cell-focused"; rowFocusStyleName = getStylePrimaryName() + "-row-focused"; if (isAttached()) { refreshHeader(); requestRefreshBody(); refreshFooter(); } } /** * Adds the given role as 'role="$param"' to the <table> * element of the grid. * * @param role * the role param * @since 8.2 */ protected void setAriaRole(String role) { escalator.getTable().setAttribute("role", role); } /** * Creates the escalator updater used to update the header rows in this * grid. The updater is invoked when header rows or columns are added or * removed, or the content of existing header cells is changed. * * @return the new header updater instance * * @see GridHeader * @see Grid#getHeader() */ protected EscalatorUpdater createHeaderUpdater() { return new StaticSectionUpdater(header, escalator.getHeader()); } /** * Creates the escalator updater used to update the body rows in this grid. * The updater is invoked when body rows or columns are added or removed, * the content of body cells is changed, or the body is scrolled to expose * previously hidden content. * * @return the new body updater instance */ protected EscalatorUpdater createBodyUpdater() { return new BodyUpdater(); } /** * Creates the escalator updater used to update the footer rows in this * grid. The updater is invoked when header rows or columns are added or * removed, or the content of existing header cells is changed. * * @return the new footer updater instance * * @see GridFooter * @see #getFooter() */ protected EscalatorUpdater createFooterUpdater() { return new StaticSectionUpdater(footer, escalator.getFooter()); } /** * Refreshes header or footer rows on demand * * @param rows * The row container * @param firstRowIsVisible * is the first row visible * @param isHeader * true if we refreshing the header, else assumed * the footer */ private void refreshRowContainer(RowContainer rows, StaticSection section) { // Add or Remove rows on demand int rowDiff = section.getVisibleRowCount() - rows.getRowCount(); if (rowDiff > 0) { rows.insertRows(0, rowDiff); } else if (rowDiff < 0) { rows.removeRows(0, -rowDiff); } // Refresh all the rows if (rows.getRowCount() > 0) { rows.refreshRows(0, rows.getRowCount()); } } /** * Focus a body cell by row and column index. * * @param rowIndex * index of row to focus * @param columnIndexDOM * index (excluding hidden columns) of cell to focus */ void focusCell(int rowIndex, int columnIndexDOM) { final Range rowRange = Range.between(0, dataSource.size()); final Range columnRange = Range.between(0, getVisibleColumns().size()); assert rowRange.contains( rowIndex) : "Illegal row index. Should be in range " + rowRange; assert columnRange.contains( columnIndexDOM) : "Illegal column index. Should be in range " + columnRange; if (rowRange.contains(rowIndex) && columnRange.contains(columnIndexDOM)) { cellFocusHandler.setCellFocus(rowIndex, columnIndexDOM, escalator.getBody()); WidgetUtil.focus(getElement()); } } /** * Refreshes all header rows */ void refreshHeader() { refreshRowContainer(escalator.getHeader(), header); } /** * Refreshes all body rows */ private void refreshBody() { escalator.getBody().refreshRows(0, escalator.getBody().getRowCount()); } /** * Request delayed refresh of all body rows. * * @since 8.1 */ public void requestRefreshBody() { if (!refreshBodyRequested) { refreshBodyRequested = true; Scheduler.get().scheduleFinally(() -> { refreshBodyRequested = false; refreshBody(); }); } } /** * Refreshes all footer rows */ void refreshFooter() { refreshRowContainer(escalator.getFooter(), footer); } /** * Adds columns as the last columns in the grid. * * @param columns * the columns to add */ @SuppressWarnings("unchecked") public void addColumns(Column... columns) { if (columns.length == 0) { // Nothing to add. return; } addColumnsSkipSelectionColumnCheck(Arrays.asList(columns), getVisibleColumns().size()); } /** * Adds a column as the last column in the grid. * * @param * class that extends Column * @param column * the column to add * @return given column */ public > C addColumn(C column) { addColumn(column, getColumnCount()); return column; } /** * Inserts a column into a specific position in the grid. * * @param * class that extends Column * @param column * the column to add * @param index * the index where the column should be inserted into * @return given column * * @throws IllegalStateException * if Grid's current selection model renders a selection column, * and {@code index} is 0. */ public > C addColumn(C column, int index) { if (column == selectionColumn) { throw new IllegalArgumentException( "The selection column may not be added manually"); } else if (selectionColumn != null && index == 0) { throw new IllegalStateException("A column cannot be inserted " + "before the selection column"); } addColumnsSkipSelectionColumnCheck(Collections.singleton(column), index); return column; } private void addColumnsSkipSelectionColumnCheck( Collection> columnsToAdd, int startIndex) { AtomicInteger index = new AtomicInteger(startIndex); columnsToAdd.forEach(col -> { // Register column with grid columns.add(index.getAndIncrement(), col); header.addColumn(col); footer.addColumn(col); // Register this grid instance with the column col.setGrid(this); }); escalator.getColumnConfiguration().insertColumns(startIndex, (int) columnsToAdd.stream().filter(col -> !col.isHidden()) .count()); columnsToAdd.forEach(col -> { // Reapply column width col.reapplyWidth(); // Sink all renderer events Set events = new HashSet<>(); events.addAll(getConsumedEventsForRenderer(col.getRenderer())); if (col.isHidable()) { columnHider.updateColumnHidable(col); } sinkEvents(events); }); } private void sinkEvents(Collection events) { assert events != null; int eventsToSink = 0; for (String typeName : events) { int typeInt = Event.getTypeInt(typeName); if (typeInt < 0) { // Type not recognized by typeInt sinkBitlessEvent(typeName); } else { eventsToSink |= typeInt; } } if (eventsToSink > 0) { sinkEvents(eventsToSink); } } private Renderer findRenderer(FlyweightCell cell) { Column column = getVisibleColumn(cell.getColumn()); assert column != null : "Could not find column at index:" + cell.getColumn(); return column.getRenderer(); } /** * Removes a column from the grid. * * @param column * the column to remove */ public void removeColumn(Column column) { if (column != null && column.equals(selectionColumn)) { throw new IllegalArgumentException( "The selection column may not be removed manually."); } removeColumnSkipSelectionColumnCheck(column); } private void removeColumnSkipSelectionColumnCheck(Column column) { int columnIndex = columns.indexOf(column); // Remove from column configuration int visibleColumnIndex = getVisibleColumns().indexOf(column); if (visibleColumnIndex < 0) { assert column.isHidden(); // Hidden columns are not included in Escalator } else { getEscalator().getColumnConfiguration() .removeColumns(visibleColumnIndex, 1); } header.removeColumn(column); footer.removeColumn(column); // de-register column with grid ((Column) column).setGrid(null); columns.remove(columnIndex); if (column.isHidable()) { columnHider.removeColumnHidingToggle(column); } updateFrozenColumns(); } /** * Returns the amount of columns in the grid. *

* NOTE: this includes the hidden columns in the count. * * @return The number of columns in the grid */ public int getColumnCount() { return columns.size(); } /** * Returns a list columns in the grid, including hidden columns. *

* For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ public List> getColumns() { return Collections.unmodifiableList(new ArrayList<>(columns)); } /** * Returns a list of the currently visible columns in the grid. *

* No {@link Column#isHidden() hidden} columns included. * * @since 7.5.0 * @return A unmodifiable list of the currently visible columns in the grid */ public List> getVisibleColumns() { List> visible = new ArrayList<>(); for (Column c : columns) { if (!c.isHidden()) { visible.add(c); } } return Collections.unmodifiableList(visible); } /** * Returns a column by its index in the grid. *

* NOTE: The indexing includes hidden columns. * * @param index * the index of the column * @return The column in the given index * @throws IllegalArgumentException * if the column index does not exist in the grid */ public Column getColumn(int index) throws IllegalArgumentException { if (index < 0 || index >= columns.size()) { throw new IllegalStateException("Column not found."); } return columns.get(index); } private Column getVisibleColumn(int index) throws IllegalArgumentException { List> visibleColumns = getVisibleColumns(); if (index < 0 || index >= visibleColumns.size()) { throw new IllegalStateException("Column not found."); } return visibleColumns.get(index); } /** * Returns the header section of this grid. The default header contains a * single row displaying the column captions. * * @return the header */ protected Header getHeader() { return header; } /** * Gets the header row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return header row at given index * @throws IllegalArgumentException * if no row exists at given index */ public HeaderRow getHeaderRow(int rowIndex) { return header.getRow(rowIndex); } /** * Inserts a new row at the given position to the header section. 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 IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendHeaderRow() * @see #prependHeaderRow() * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow addHeaderRowAt(int index) { return header.addRowAt(index); } /** * Adds a new row at the bottom of the header section. * * @return the new row * @see #prependHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow appendHeaderRow() { return header.appendRow(); } /** * Returns the current default row of the header section. The default row is * a special header row providing a user interface for sorting columns. * Setting a header caption for column updates cells in the default header. * * @return the default row or null if no default row set */ public HeaderRow getDefaultHeaderRow() { return header.getDefaultRow(); } /** * Gets the row count for the header section. * * @return row count */ public int getHeaderRowCount() { return header.getRowCount(); } /** * Adds a new row at the top of the header section. * * @return the new row * @see #appendHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow prependHeaderRow() { return header.prependRow(); } /** * Removes the given row from the header section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeHeaderRow(int) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(HeaderRow row) { header.removeRow(row); } /** * Removes the row at the given position from the header section. * * @param rowIndex * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeHeaderRow(HeaderRow) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(int rowIndex) { header.removeRow(rowIndex); } /** * Sets the default row of the header. The default row is a special header * row providing a user interface for sorting columns. *

* Note: Setting the default header row will reset all cell contents to * Column defaults. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * header does not contain the row */ public void setDefaultHeaderRow(HeaderRow row) { header.setDefaultRow(row); } /** * Sets the visibility of the header section. * * @param visible * true to show header section, false to hide */ public void setHeaderVisible(boolean visible) { header.setVisible(visible); } /** * Returns the visibility of the header section. * * @return true if visible, false otherwise. */ public boolean isHeaderVisible() { return header.isVisible(); } /* Grid Footers */ /** * Returns the footer section of this grid. The default footer is empty. * * @return the footer */ protected Footer getFooter() { return footer; } /** * Gets the footer row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return footer row at given index * @throws IllegalArgumentException * if no row exists at given index */ public FooterRow getFooterRow(int rowIndex) { return footer.getRow(rowIndex); } /** * Inserts a new row at the given position to the footer section. 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 IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendFooterRow() * @see #prependFooterRow() * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow addFooterRowAt(int index) { return footer.addRowAt(index); } /** * Adds a new row at the bottom of the footer section. * * @return the new row * @see #prependFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow appendFooterRow() { return footer.appendRow(); } /** * Gets the row count for the footer. * * @return row count */ public int getFooterRowCount() { return footer.getRowCount(); } /** * Adds a new row at the top of the footer section. * * @return the new row * @see #appendFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow prependFooterRow() { return footer.prependRow(); } /** * Removes the given row from the footer section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeFooterRow(int) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(FooterRow row) { footer.removeRow(row); } /** * Removes the row at the given position from the footer section. * * @param rowIndex * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeFooterRow(FooterRow) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(int rowIndex) { footer.removeRow(rowIndex); } /** * Sets the visibility of the footer section. * * @param visible * true to show footer section, false to hide */ public void setFooterVisible(boolean visible) { footer.setVisible(visible); } /** * Returns the visibility of the footer section. * * @return true if visible, false otherwise. */ public boolean isFooterVisible() { return footer.isVisible(); } /** * Returns the {@link Editor} for this Grid. * * @return editor */ public Editor getEditor() { return editor; } /** * Gets the {@link Escalator} used by this Grid instance. * * @return the escalator instance, never null */ public Escalator getEscalator() { return escalator; } /** * {@inheritDoc} *

* Note: This method will change the widget's size in the browser * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. * * @see #setHeightMode(HeightMode) */ @Override public void setHeight(String height) { escalator.setHeight(height); } @Override public void setWidth(String width) { escalator.setWidth(width); } /** * Sets the data source used by this grid. * * @param dataSource * the data source to use, not null * @throws IllegalArgumentException * if dataSource is null */ public void setDataSource(final DataSource dataSource) throws IllegalArgumentException { if (dataSource == null) { throw new IllegalArgumentException("dataSource can't be null."); } if (changeHandler != null) { changeHandler.remove(); changeHandler = null; } this.dataSource = dataSource; changeHandler = dataSource .addDataChangeHandler(new DataChangeHandler() { @Override public void dataUpdated(int firstIndex, int numberOfItems) { escalator.getBody().refreshRows(firstIndex, numberOfItems); } @Override public void dataRemoved(int firstIndex, int numberOfItems) { for (int i = 0; i < numberOfItems; ++i) { visibleDetails.remove(firstIndex + i); } escalator.getBody().removeRows(firstIndex, numberOfItems); Range removed = Range.withLength(firstIndex, numberOfItems); cellFocusHandler.rowsRemovedFromBody(removed); } @Override public void dataAdded(int firstIndex, int numberOfItems) { escalator.getBody().insertRows(firstIndex, numberOfItems); Range added = Range.withLength(firstIndex, numberOfItems); cellFocusHandler.rowsAddedToBody(added); } @Override public void dataAvailable(int firstIndex, int numberOfItems) { currentDataAvailable = Range.withLength(firstIndex, numberOfItems); if (recalculateColumnWidthsNeeded) { // Ensure that cache has actually been populated or // all rows removed, otherwise wait for next call. if (numberOfItems > 0 || getDataSource().size() == 0) { recalculateColumnWidths(); recalculateColumnWidthsNeeded = false; } } fireEvent(new DataAvailableEvent(currentDataAvailable)); } @Override public void resetDataAndSize(int newSize) { // It might take a while for new data to arrive, // clear the record of cached rows. currentDataAvailable = Range.emptyRange(); RowContainer body = escalator.getBody(); int oldSize = body.getRowCount(); if (newSize > oldSize) { if (oldSize == 0 && !isHeaderVisible()) { // Fixes framework/issues/11607 // Need to recalculate column widths when the // first row is added to a non-header grid, // otherwise the checkbox will be aligned in a // wrong place. Wait until the cache has been // populated before making the call. recalculateColumnWidthsNeeded = true; } body.insertRows(oldSize, newSize - oldSize); cellFocusHandler.rowsAddedToBody(Range .withLength(oldSize, newSize - oldSize)); } else if (newSize < oldSize) { body.removeRows(newSize, oldSize - newSize); cellFocusHandler.rowsRemovedFromBody(Range .withLength(newSize, oldSize - newSize)); } if (newSize > 0) { Range visibleRowRange = escalator .getVisibleRowRange(); dataSource.ensureAvailability( visibleRowRange.getStart(), visibleRowRange.length()); } else { // We won't expect any data more data updates, so // just make the bookkeeping happy. dataAvailable(0, 0); } assert body.getRowCount() == newSize; } }); int previousRowCount = escalator.getBody().getRowCount(); if (previousRowCount != 0) { escalator.getBody().removeRows(0, previousRowCount); } setEscalatorSizeFromDataSource(); } private void setEscalatorSizeFromDataSource() { assert escalator.getBody().getRowCount() == 0; int size = dataSource.size(); if (size > 0) { escalator.getBody().insertRows(0, size); } } /** * Gets the {@Link DataSource} for this Grid. * * @return the data source used by this grid */ public DataSource getDataSource() { return dataSource; } /** * Sets the number of frozen columns in this grid. Setting the count to 0 * means that no data columns will be frozen, but the built-in selection * checkbox column will still be frozen if it's in use. Setting the count to * -1 will also disable the selection column. *

* The default value is 0. * * @param numberOfColumns * the number of columns that should be frozen * * @throws IllegalArgumentException * if the column count is < -1 or > the number of visible * columns */ public void setFrozenColumnCount(int numberOfColumns) { if (numberOfColumns < -1 || numberOfColumns > getColumnCount()) { throw new IllegalArgumentException( "count must be between -1 and the current number of columns (" + getColumnCount() + ")"); } frozenColumnCount = numberOfColumns; updateFrozenColumns(); } private void updateFrozenColumns() { int visibleFrozenColumnCount = getVisibleFrozenColumnCount(); ColumnConfiguration columnConfiguration = escalator .getColumnConfiguration(); if (columnConfiguration.getColumnCount() < visibleFrozenColumnCount) { // new columns may not have got added yet, delay and check the // correct count again Scheduler.get().scheduleFinally(() -> columnConfiguration .setFrozenColumnCount(getVisibleFrozenColumnCount())); } else { columnConfiguration.setFrozenColumnCount(visibleFrozenColumnCount); } } private int getVisibleFrozenColumnCount() { int numberOfColumns = getFrozenColumnCount(); // for the escalator the hidden columns are not in the frozen column // count, but for grid they are. thus need to convert the index int limit = getFrozenColumnCount(); if (getSelectionColumn().isPresent()) { // If the grid is in MultiSelect mode, getColumn(0) in the following // for loop returns the selection column. Accordingly, verifying // which frozen columns are visible if the selection column is // present should take this fact into account. limit++; } for (int i = 0; i < limit; i++) { if (i >= getColumnCount() || getColumn(i).isHidden()) { numberOfColumns--; } } if (numberOfColumns == -1) { numberOfColumns = 0; } else if (selectionColumn != null) { numberOfColumns++; } return numberOfColumns; } /** * Gets the number of frozen columns in this grid. 0 means that no data * columns will be frozen, but the built-in selection checkbox column will * still be frozen if it's in use. -1 means that not even the selection * column is frozen. *

* NOTE: This includes {@link Column#isHidden() hidden columns} in * the count. * * @return the number of frozen columns */ public int getFrozenColumnCount() { return frozenColumnCount; } /** * Adds an event handler that gets notified when the range of visible rows * changes e.g. because of scrolling, row resizing or spacers * appearing/disappearing. * * @param handler * the event handler * @return a handler registration for the added handler */ public HandlerRegistration addRowVisibilityChangeHandler( RowVisibilityChangeHandler handler) { /* * Reusing Escalator's RowVisibilityChangeHandler, since a scroll * concept is too abstract. e.g. the event needs to be re-sent when the * widget is resized. */ return escalator.addRowVisibilityChangeHandler(handler); } /** * Scrolls to a certain row, using {@link ScrollDestination#ANY}. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @throws IllegalArgumentException * if rowIndex is below zero, or above the maximum value * supported by the data source. */ public void scrollToRow(int rowIndex) throws IllegalArgumentException { scrollToRow(rowIndex, ScrollDestination.ANY, GridConstants.DEFAULT_PADDING); } /** * Scrolls to a certain row, using user-specified scroll destination. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @param destination * desired destination placement of scrolled-to-row. See * {@link ScrollDestination} for more information. * @throws IllegalArgumentException * if rowIndex is below zero, or above the maximum value * supported by the data source. */ public void scrollToRow(int rowIndex, ScrollDestination destination) throws IllegalArgumentException { scrollToRow(rowIndex, destination, destination == ScrollDestination.MIDDLE ? 0 : GridConstants.DEFAULT_PADDING); } /** * Helper method for making sure desired row is visible and it is properly * rendered. * * @param rowIndex * the row to look for * @param destination * the desired scroll destination * @param callback * the callback command to execute when row is available * @since 8.4 */ public void scrollToRow(int rowIndex, ScrollDestination destination, Runnable callback) { waitUntilVisible(rowIndex, destination, callback); } /** * Helper method for making sure desired row is visible and it is properly * rendered. * * @param rowIndex * the row to look for * @param whenRendered * the callback command to execute when row is available * @since 8.4 */ public void scrollToRow(int rowIndex, Runnable whenRendered) { scrollToRow(rowIndex, ScrollDestination.ANY, whenRendered); } /** * Scrolls to a certain row using only user-specified parameters. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @param destination * desired destination placement of scrolled-to-row. See * {@link ScrollDestination} for more information. * @param paddingPx * number of pixels to overscroll. Behavior depends on * destination. * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} * and padding is nonzero, because having a padding on a * centered row is undefined behavior, or if rowIndex is below * zero or above the row count of the data source. */ private void scrollToRow(int rowIndex, ScrollDestination destination, int paddingPx) throws IllegalArgumentException { int maxsize = escalator.getBody().getRowCount() - 1; if (rowIndex < 0) { throw new IllegalArgumentException( "Row index (" + rowIndex + ") is below zero!"); } if (rowIndex > maxsize) { throw new IllegalArgumentException("Row index (" + rowIndex + ") is above maximum (" + maxsize + ")!"); } escalator.scrollToRowAndSpacer(rowIndex, destination, paddingPx); } /** * Helper method for scrolling and making sure row is visible. * * @param rowIndex * the row index to make visible * @param destination * the desired scroll destination * @param whenVisible * the callback method to call when row is visible */ private void waitUntilVisible(int rowIndex, ScrollDestination destination, Runnable whenVisible) { boolean waitForCache = false; if (getDataSource().getRow(rowIndex) == null) { // not yet in cache, wait for this to change waitForCache = true; Reference registration = new Reference<>(); registration.set(getDataSource() .addDataChangeHandler(new DataChangeHandler() { @Override public void resetDataAndSize(int estimatedNewDataSize) { // data set changed, cancel the operation registration.get().remove(); } @Override public void dataUpdated(int firstRowIndex, int numberOfRows) { // NOP } @Override public void dataRemoved(int firstRowIndex, int numberOfRows) { // data set changed, cancel the operation registration.get().remove(); } @Override public void dataAvailable(int firstRowIndex, int numberOfRows) { // if new available range contains the row, // try again if (Range.withLength(firstRowIndex, numberOfRows) .contains(rowIndex)) { registration.get().remove(); waitUntilVisible(rowIndex, destination, whenVisible); } } @Override public void dataAdded(int firstRowIndex, int numberOfRows) { // data set changed, cancel the operation registration.get().remove(); } })); } scrollToRow(rowIndex, destination); if (!waitForCache) { // all necessary adjustments done, time to perform whenVisible.run(); } } /** * Scrolls to the beginning of the very first row. */ public void scrollToStart() { if (getEscalator().getBody().getRowCount() > 0) { scrollToRow(0, ScrollDestination.START); } } /** * Scrolls to the end of the very last row. */ public void scrollToEnd() { if (getEscalator().getBody().getRowCount() > 0) { scrollToRow(escalator.getBody().getRowCount() - 1, ScrollDestination.END); } } /** * Sets the vertical scroll offset. * * @param px * the number of pixels this grid should be scrolled down */ public void setScrollTop(double px) { escalator.setScrollTop(px); } /** * Gets the vertical scroll offset. * * @return the number of pixels this grid is scrolled down */ public double getScrollTop() { return escalator.getScrollTop(); } /** * Sets the horizontal scroll offset. * * @since 7.5.0 * @param px * the number of pixels this grid should be scrolled right */ public void setScrollLeft(double px) { escalator.setScrollLeft(px); } /** * Gets the horizontal scroll offset. * * @return the number of pixels this grid is scrolled to the right */ public double getScrollLeft() { return escalator.getScrollLeft(); } /** * Returns the height of the scrollable area in pixels. * * @since 7.5.0 * @return the height of the scrollable area in pixels */ public double getScrollHeight() { return escalator.getScrollHeight(); } /** * Returns the width of the scrollable area in pixels. * * @since 7.5.0 * @return the width of the scrollable area in pixels. */ public double getScrollWidth() { return escalator.getScrollWidth(); } private static final Logger getLogger() { return Logger.getLogger(Grid.class.getName()); } /** * Sets the number of rows that should be visible in Grid's body, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* If Grid is currently not in {@link HeightMode#ROW}, the given value is * remembered, and applied once the mode is applied. * * @param rows * The height in terms of number of rows displayed in Grid's * body. If Grid doesn't contain enough rows, white space is * displayed instead. * @throws IllegalArgumentException * if {@code rows} is zero or less * @throws IllegalArgumentException * if {@code rows} is {@link Double#isInifinite(double) * infinite} * @throws IllegalArgumentException * if {@code rows} is {@link Double#isNaN(double) NaN} * * @see #setHeightMode(HeightMode) */ public void setHeightByRows(double rows) throws IllegalArgumentException { escalator.setHeightByRows(rows); } /** * Gets the amount of rows in Grid's body that are shown, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}. * * @return the amount of rows that should be shown in Grid's body, while in * {@link HeightMode#ROW}. * @see #setHeightByRows(double) */ public double getHeightByRows() { return escalator.getHeightByRows(); } /** * Defines the mode in which the Grid widget's height is calculated. *

* If {@link HeightMode#CSS} is given, Grid will respect the values given * via {@link #setHeight(String)}, and behave as a traditional Widget. *

* If {@link HeightMode#ROW} is given, Grid will make sure that the body * will display as many rows as {@link #getHeightByRows()} defines. * Note: If headers/footers are inserted or removed, the widget * will resize itself to still display the required amount of rows in its * body. It also takes the horizontal scrollbar into account. * * @param heightMode * the mode in to which Grid should be set */ public void setHeightMode(HeightMode heightMode) { /* * This method is a workaround for the fact that Vaadin re-applies * widget dimensions (height/width) on each state change event. The * original design was to have setHeight an setHeightByRow be equals, * and whichever was called the latest was considered in effect. * * But, because of Vaadin always calling setHeight on the widget, this * approach doesn't work. */ escalator.setHeightMode(heightMode); } /** * Returns the current {@link HeightMode} the Grid is in. *

* Defaults to {@link HeightMode#CSS}. * * @return the current HeightMode */ public HeightMode getHeightMode() { return escalator.getHeightMode(); } private Set getConsumedEventsForRenderer(Renderer renderer) { Set events = new HashSet<>(); if (renderer instanceof ComplexRenderer) { Collection consumedEvents = ((ComplexRenderer) renderer) .getConsumedEvents(); if (consumedEvents != null) { events.addAll(consumedEvents); } } return events; } @Override public void onBrowserEvent(Event event) { if (!isEnabled()) { return; } String eventType = event.getType(); if (eventType.equals(BrowserEvents.FOCUS) || eventType.equals(BrowserEvents.BLUR)) { super.onBrowserEvent(event); return; } EventTarget target = event.getEventTarget(); if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { return; } Element element = Element.as(target); RowContainer container = escalator.findRowContainer(element); Cell cell; if (container == null) { if (eventType.equals(BrowserEvents.KEYDOWN) || eventType.equals(BrowserEvents.KEYUP) || eventType.equals(BrowserEvents.KEYPRESS)) { cell = cellFocusHandler.getFocusedCell(); container = cellFocusHandler.containerWithFocus; } else { // Click might be in an editor cell, should still map. if (editor.editorOverlay != null && editor.editorOverlay.isOrHasChild(element)) { container = escalator.getBody(); int rowIndex = editor.getRow(); int colIndex = editor.getElementColumn(element); if (colIndex < 0) { // Click in editor, but not for any column. return; } try { TableCellElement cellElement = container .getRowElement(rowIndex).getCells() .getItem(colIndex); cell = new Cell(rowIndex, colIndex, cellElement); } catch (IllegalStateException exception) { // IllegalStateException may occur if user has scrolled // Grid so that Escalator has updated, and row under // Editor is no longer there return; } } else { if (escalator.getElement().isOrHasChild(element)) { eventCell.set(new Cell(-1, -1, null), Section.BODY); // Fire native events. super.onBrowserEvent(event); } return; } } } else { cell = container.getCell(element); if (eventType.equals(BrowserEvents.MOUSEDOWN)) { cellOnPrevMouseDown = cell; } else if (cell == null && eventType.equals(BrowserEvents.CLICK)) { /* * Chrome has an interesting idea on click targets (see * cellOnPrevMouseDown javadoc). Firefox, on the other hand, has * the mousedown target as the click target. */ cell = cellOnPrevMouseDown; } } if (cell == null) { getLogger().log(Level.WARNING, "received " + eventType + "-event with a null cell target"); return; } eventCell.set(cell, getSectionFromContainer(container)); GridEvent gridEvent = new GridEvent<>(event, eventCell); for (GridEventHandler handler : browserEventHandlers) { handler.onEvent(gridEvent); } } private Section getSectionFromContainer(RowContainer container) { assert container != null : "RowContainer should not be null"; if (container == escalator.getBody()) { return Section.BODY; } else if (container == escalator.getFooter()) { return Section.FOOTER; } else if (container == escalator.getHeader()) { return Section.HEADER; } assert false : "RowContainer was not header, footer or body."; return null; } private boolean isOrContainsInSpacer(Node node) { Node n = node; while (n != null && n != getElement()) { boolean isElement = Element.is(n); if (isElement) { String className = Element.as(n).getClassName(); // Also check whether className is indeed a string. For // SVGElement it may be of type SVGAnimatedString. // https://developer.mozilla.org/en-US/docs/Web/API/Element/className#Notes if (WidgetUtil.isString(className) && className .contains(getStylePrimaryName() + "-spacer")) { return true; } } n = n.getParentNode(); } return false; } private boolean isElementInChildWidget(Element e) { Widget w = WidgetUtil.findWidget(e); if (w == this) { return false; } /* * If e is directly inside this grid, but the grid is wrapped in a * Composite, findWidget is not going to find this, only the wrapper. * Thus we need to check its parents to see if we encounter this; if we * don't, the found widget is actually a parent of this, so we should * return false. */ while (w != null && w != this) { w = w.getParent(); } return w != null; } private class EditorEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (!isEditorEnabled()) { return; } Widget widget; if (editor.focusedColumnIndexDOM < 0) { widget = null; } else { widget = editor.getWidget( getVisibleColumn(editor.focusedColumnIndexDOM)); } EditorDomEvent editorEvent = new EditorDomEvent<>( event.getDomEvent(), event.getCell(), widget); event.setHandled( getEditor().getEventHandler().handleEvent(editorEvent)); } }; private class SuperEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (event.isHandled()) { return; } Grid.super.onBrowserEvent(event.getDomEvent()); } }; private abstract class AbstractGridEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (event.isHandled()) { return; } event.setHandled(isElementInChildWidget( Element.as(event.getDomEvent().getEventTarget()))); } }; private class RendererEventHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!event.getCell().isBody()) { return; } Column gridColumn = event.getCell().getColumn(); boolean enterKey = event.getDomEvent().getType() .equals(BrowserEvents.KEYDOWN) && event.getDomEvent().getKeyCode() == KeyCodes.KEY_ENTER; boolean doubleClick = event.getDomEvent().getType() .equals(BrowserEvents.DBLCLICK); if (gridColumn.getRenderer() instanceof ComplexRenderer) { ComplexRenderer cplxRenderer = (ComplexRenderer) gridColumn .getRenderer(); if (cplxRenderer.getConsumedEvents() .contains(event.getDomEvent().getType())) { if (cplxRenderer.onBrowserEvent(event.getCell(), event.getDomEvent())) { event.setHandled(true); } } // Calls onActivate if KeyDown and Enter or double click if ((enterKey || doubleClick) && cplxRenderer.onActivate(event.getCell())) { event.setHandled(true); } } } }; private class CellFocusEventHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } Collection navigation = cellFocusHandler .getNavigationEvents(); if (navigation.contains(event.getDomEvent().getType())) { cellFocusHandler.handleNavigationEvent(event.getDomEvent(), event.getCell()); } } }; private class HeaderCellDragStartHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!isColumnReorderingAllowed()) { return; } if (!event.getCell().isHeader()) { return; } int offset = 0; // apply offset depending on selection column, see // #10546 if (getSelectionColumn().isPresent()) { offset = -1; } if (event.getCell().getColumnIndex() + offset < getFrozenColumnCount()) { return; } if (event.getDomEvent().getTypeInt() == Event.ONMOUSEDOWN && event.getDomEvent() .getButton() == NativeEvent.BUTTON_LEFT || event.getDomEvent().getTypeInt() == Event.ONTOUCHSTART) { dndHandler.onDragStartOnDraggableElement(event.getDomEvent(), headerCellDndCallback); event.getDomEvent().preventDefault(); event.getDomEvent().stopPropagation(); // fixes https://github.com/vaadin/framework/issues/8632 // don't mark the event as handled, in order for the next // handler in the handler chain (HeaderDefaultRowEventHandler) // to be able to receive it. This should be safe since the next // handlers in the chain (RendererEventHandler and // CellFocusEventHandler) do not react to header touches/clicks. // event.setHandled(true); } } }; private class HeaderDefaultRowEventHandler extends AbstractGridEventHandler { private Point rowEventTouchStartingPoint; @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!event.getCell().isHeader()) { return; } if (!getHeader().getRow(event.getCell().getRowIndex()) .isDefault()) { return; } if (!event.getCell().getColumn().isSortable()) { // Only handle sorting events if the column is sortable return; } if (BrowserEvents.MOUSEDOWN.equals(event.getDomEvent().getType()) && event.getDomEvent().getShiftKey()) { // Don't select text when shift clicking on a header. event.getDomEvent().preventDefault(); } if (BrowserEvents.TOUCHSTART .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } event.getDomEvent().preventDefault(); Touch touch = event.getDomEvent().getChangedTouches().get(0); rowEventTouchStartingPoint = new Point(touch.getClientX(), touch.getClientY()); sorter.awaitForTouchEnd(GridConstants.LONG_TAP_DELAY); event.setHandled(true); } else if (BrowserEvents.TOUCHMOVE .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } if (rowEventTouchStartingPoint == null) { return; } event.getDomEvent().preventDefault(); Touch touch = event.getDomEvent().getChangedTouches().get(0); double diffX = Math.abs( touch.getClientX() - rowEventTouchStartingPoint.getX()); double diffY = Math.abs( touch.getClientY() - rowEventTouchStartingPoint.getY()); // Cancel long tap if finger strays too far from // starting point if (diffX > GridConstants.LONG_TAP_THRESHOLD || diffY > GridConstants.LONG_TAP_THRESHOLD) { sorter.cancelAwaitForTouchEnd(); } event.setHandled(true); } else if (BrowserEvents.TOUCHEND .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } if (rowEventTouchStartingPoint == null) { return; } sorter.onTouchEnd(); event.setHandled(true); } else if (BrowserEvents.TOUCHCANCEL .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } sorter.cancelAwaitForTouchEnd(); event.setHandled(true); } else if (BrowserEvents.CLICK .equals(event.getDomEvent().getType())) { sorter.sort(event.getCell().getColumn(), event.getDomEvent().getShiftKey()); } } }; @Override public com.google.gwt.user.client.Element getSubPartElement( String subPart) { /* * handles details[] (translated to spacer[] for Escalator), cell[], * header[] and footer[] */ // "#header[0][0]/DRAGhANDLE" Element escalatorElement = escalator.getSubPartElement( subPart.replaceFirst("^details\\[", "spacer[")); if (escalatorElement != null) { int detailIdx = subPart.indexOf("/"); if (detailIdx > 0) { String detail = subPart.substring(detailIdx + 1); getLogger().severe("Looking up detail from index " + detailIdx + " onward: \"" + detail + "\""); if (detail.equalsIgnoreCase("content")) { // XXX: Fix this to look up by class name! return DOM.asOld(Element.as(escalatorElement.getChild(0))); } if (detail.equalsIgnoreCase("draghandle")) { // XXX: Fix this to look up by class name! return DOM.asOld(Element.as(escalatorElement.getChild(1))); } } return DOM.asOld(escalatorElement); } SubPartArguments args = SubPartArguments.create(subPart); Element editor = getSubPartElementEditor(args); if (editor != null) { return DOM.asOld(editor); } return null; } private Element getSubPartElementEditor(SubPartArguments args) { if (!args.getType().equalsIgnoreCase("editor") || editor.getState() != State.ACTIVE) { return null; } if (args.getIndicesLength() == 0) { return editor.editorOverlay; } else if (args.getIndicesLength() == 1) { int index = args.getIndex(0); if (index >= columns.size()) { return null; } escalator.scrollToColumn(index, ScrollDestination.ANY, 0); Widget widget = editor.getWidget(columns.get(index)); if (widget != null) { return widget.getElement(); } // No widget for the column. return null; } return null; } @Override public String getSubPartName( com.google.gwt.user.client.Element subElement) { String escalatorStructureName = escalator.getSubPartName(subElement); if (escalatorStructureName != null) { return escalatorStructureName.replaceFirst("^spacer", "details"); } String editorName = getSubPartNameEditor(subElement); if (editorName != null) { return editorName; } return null; } private String getSubPartNameEditor(Element subElement) { if (editor.getState() != State.ACTIVE || !editor.editorOverlay.isOrHasChild(subElement)) { return null; } int i = 0; for (Column column : columns) { if (editor.getWidget(column).getElement() .isOrHasChild(subElement)) { return "editor[" + i + "]"; } ++i; } return "editor"; } private void setSelectColumnRenderer( final Renderer selectColumnRenderer) { if (this.selectColumnRenderer == selectColumnRenderer) { return; } if (this.selectColumnRenderer != null) { if (this.selectColumnRenderer instanceof ComplexRenderer) { // End of Life for the old selection column renderer. ((ComplexRenderer) this.selectColumnRenderer).destroy(); } // Clear field so frozen column logic in the remove method knows // what to do Column colToRemove = selectionColumn; selectionColumn = null; removeColumnSkipSelectionColumnCheck(colToRemove); cellFocusHandler.offsetRangeBy(-1); } this.selectColumnRenderer = selectColumnRenderer; if (selectColumnRenderer != null) { cellFocusHandler.offsetRangeBy(1); selectionColumn = new SelectionColumn(selectColumnRenderer); addColumnsSkipSelectionColumnCheck( Collections.singleton(selectionColumn), 0); selectionColumn.initDone(); } else { selectionColumn = null; requestRefreshBody(); } updateFrozenColumns(); } /** * Sets the current selection model. * * @param selectionModel * a selection model implementation. * @throws IllegalArgumentException * if selection model argument is null */ public void setSelectionModel(SelectionModel selectionModel) { if (selectionModel == null) { throw new IllegalArgumentException("Selection model can't be null"); } this.selectionModel = selectionModel; if (selectionModel instanceof SelectionModelWithSelectionColumn) { setSelectColumnRenderer( ((SelectionModelWithSelectionColumn) selectionModel) .getRenderer()); } else { setSelectColumnRenderer(null); } if (this.selectionModel.isMultiSelectionAllowed()) { escalator.getTable().setAttribute("aria-multiselectable", "true"); } else if (this.selectionModel.isSelectionAllowed()) { escalator.getTable().setAttribute("aria-multiselectable", "false"); } else { escalator.getTable().removeAttribute("aria-multiselectable"); } // Refresh rendered rows to update selection, if it has changed requestRefreshBody(); } /** * Gets a reference to the current selection model. * * @return the currently used SelectionModel instance. */ public SelectionModel getSelectionModel() { return selectionModel; } /** * Returns if a row is selected. * * @param row * a row object * @return {@code true}, if the current selection model considers the * provided row object selected. */ public boolean isSelected(T row) { return selectionModel.isSelected(row); } /** * Selects a row using the current selection model. *

* Only selection models implementing {@link SelectionModel.Single} and * {@link SelectionModel.Multi} are supported; for anything else, an * exception will be thrown. * * @param row * a row object * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public void select(T row) { if (getSelectionModel().isSelectionAllowed()) { getSelectionModel().select(row); } } /** * Deselects a row using the current selection model. *

* Only selection models implementing {@link SelectionModel.Single} and * {@link SelectionModel.Multi} are supported; for anything else, an * exception will be thrown. * * @param row * a row object * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public void deselect(T row) { if (getSelectionModel().isSelectionAllowed()) { getSelectionModel().deselect(row); } } /** * Deselects all rows using the current selection model. * * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public void deselectAll() { getSelectionModel().deselectAll(); } @Override public HandlerRegistration addSelectionHandler( final SelectionHandler handler) { return addHandler(handler, SelectionEvent.getType()); } /** * Sets the current sort order using the fluid Sort API. Read the * documentation for {@link Sort} for more information. * * @param s * a sort instance */ public void sort(Sort s) { setSortOrder(s.build()); } /** * Sorts the Grid data in ascending order along one column. * * @param * the column type * @param column * a grid column reference */ public void sort(Column column) { sort(column, SortDirection.ASCENDING); } /** * Sorts the Grid data along one column. * * @param * the column type * @param column * a grid column reference * @param direction * a sort direction value */ public void sort(Column column, SortDirection direction) { sort(Sort.by(column, direction)); } /** * Sets the sort order to use. Setting this causes the Grid to re-sort * itself. * * @param order * a sort order list. If set to null, the sort order is cleared. */ public void setSortOrder(List order) { setSortOrder(order, false); } /** * Clears the sort order and indicators without re-sorting. */ private void clearSortOrder() { sortOrder.clear(); refreshHeader(); } private void setSortOrder(List order, boolean userOriginated) { if (order != sortOrder) { sortOrder.clear(); if (order != null) { sortOrder.addAll(order); } } sort(userOriginated); } /** * Get a copy of the current sort order array. * * @return a copy of the current sort order array */ public List getSortOrder() { return Collections.unmodifiableList(sortOrder); } /** * Finds the sorting order for this column */ private SortOrder getSortOrder(Column column) { for (SortOrder order : getSortOrder()) { if (order.getColumn() == column) { return order; } } return null; } /** * Register a GWT event handler for a sorting event. This handler gets * called whenever this Grid needs its data source to provide data sorted in * a specific order. * * @param handler * a sort event handler * @return the registration for the handler */ public HandlerRegistration addSortHandler(SortHandler handler) { return addHandler(handler, SortEvent.getType()); } /** * Register a GWT event handler for a select all event. This handler gets * called whenever Grid needs all rows selected. *

* In case the select all checkbox is not visible in the * {@link SelectionColumn}, it will be come visible after adding the * handler. * * @param handler * a select all event handler * @return the registration for the handler */ public HandlerRegistration addSelectAllHandler( SelectAllHandler handler) { HandlerRegistration registration = addHandler(handler, SelectAllEvent.getType()); return registration; } /** * Register a GWT event handler for a data available event. This handler * gets called whenever the {@link DataSource} for this Grid has new data * available. *

* This handle will be fired with the current available data after * registration is done. * * @param handler * a data available event handler * @return the registration for the handler */ public HandlerRegistration addDataAvailableHandler( final DataAvailableHandler handler) { // Deferred call to handler with current row range Scheduler.get().scheduleFinally(() -> { if (!dataSource.isWaitingForData()) { handler.onDataAvailable( new DataAvailableEvent(currentDataAvailable)); } }); return addHandler(handler, DataAvailableEvent.TYPE); } /** * Register a BodyKeyDownHandler to this Grid. The event for this handler is * fired when a KeyDown event occurs while cell focus is in the Body of this * Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addBodyKeyDownHandler( BodyKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a BodyKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Body of this * Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addBodyKeyUpHandler(BodyKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a BodyKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Body of * this Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addBodyKeyPressHandler( BodyKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a HeaderKeyDownHandler to this Grid. The event for this handler * is fired when a KeyDown event occurs while cell focus is in the Header of * this Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addHeaderKeyDownHandler( HeaderKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a HeaderKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Header of this * Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addHeaderKeyUpHandler( HeaderKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a HeaderKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Header * of this Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addHeaderKeyPressHandler( HeaderKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a FooterKeyDownHandler to this Grid. The event for this handler * is fired when a KeyDown event occurs while cell focus is in the Footer of * this Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addFooterKeyDownHandler( FooterKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a FooterKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Footer of this * Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addFooterKeyUpHandler( FooterKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a FooterKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Footer * of this Grid. * * @param handler * the key handler to register * @return the registration for the handler */ public HandlerRegistration addFooterKeyPressHandler( FooterKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a BodyClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Body of this Grid. * * @param handler * the click handler to register * @return the registration for the handler */ public HandlerRegistration addBodyClickHandler(BodyClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a HeaderClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Header of this Grid. * * @param handler * the click handler to register * @return the registration for the handler */ public HandlerRegistration addHeaderClickHandler( HeaderClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a FooterClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Footer of this Grid. * * @param handler * the click handler to register * @return the registration for the handler */ public HandlerRegistration addFooterClickHandler( FooterClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a BodyDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Body of this * Grid. * * @param handler * the double click handler to register * @return the registration for the handler */ public HandlerRegistration addBodyDoubleClickHandler( BodyDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a HeaderDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Header of this * Grid. * * @param handler * the double click handler to register * @return the registration for the handler */ public HandlerRegistration addHeaderDoubleClickHandler( HeaderDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a FooterDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Footer of this * Grid. * * @param handler * the double click handler to register * @return the registration for the handler */ public HandlerRegistration addFooterDoubleClickHandler( FooterDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a column reorder handler to this Grid. The event for this * handler is fired when the Grid's columns are reordered. * * @since 7.5.0 * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addColumnReorderHandler( ColumnReorderHandler handler) { return addHandler(handler, ColumnReorderEvent.getType()); } /** * Register a column visibility change handler to this Grid. The event for * this handler is fired when the Grid's columns change visibility. * * @since 7.5.0 * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addColumnVisibilityChangeHandler( ColumnVisibilityChangeHandler handler) { return addHandler(handler, ColumnVisibilityChangeEvent.getType()); } /** * Register a column resize handler to this Grid. The event for this handler * is fired when the Grid's columns are resized. * * @since 7.6 * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addColumnResizeHandler( ColumnResizeHandler handler) { return addHandler(handler, ColumnResizeEvent.getType()); } /** * Register a enabled status change handler to this Grid. The event for this * handler is fired when the Grid changes from disabled to enabled and * vice-versa. * * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addEnabledHandler(GridEnabledHandler handler) { return addHandler(handler, GridEnabledEvent.TYPE); } /** * Register a selection allowed status change handler to this Grid. The * event for this handler is fired when the Grid changes selection allowed * state. * * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addSelectionAllowedHandler( GridSelectionAllowedHandler handler) { return addHandler(handler, GridSelectionAllowedEvent.TYPE); } /** * Register a row height changed handler to this Grid. The event for this * handler is fired when the row height is changed in the Grid's header, * body or footer. * * @param handler * the handler for the event * @return the registration for the handler */ public HandlerRegistration addRowHeightChangedHandler( RowHeightChangedHandler handler) { return escalator.addHandler(handler, RowHeightChangedEvent.TYPE); } /** * Adds a spacer visibility changed handler to the underlying escalator. * * @param handler * the handler to be called when a spacer's visibility changes * @return the registration object with which the handler can be removed * @since 8.3.2 */ public HandlerRegistration addSpacerVisibilityChangedHandler( SpacerVisibilityChangedHandler handler) { return escalator.addHandler(handler, SpacerVisibilityChangedEvent.TYPE); } /** * Adds a spacer index changed handler to the underlying escalator. * * @param handler * the handler to be called when a spacer's index changes * @return the registration object with which the handler can be removed * @since 8.9 */ public HandlerRegistration addSpacerIndexChangedHandler( SpacerIndexChangedHandler handler) { return escalator.addHandler(handler, SpacerIndexChangedEvent.TYPE); } /** * Adds a low-level DOM event handler to this Grid. The handler is inserted * into the given position in the list of handlers. The handlers are invoked * in order. If the * {@link GridEventHandler#onEvent(Event, EventCellReference) onEvent} * method of a handler returns true, subsequent handlers are not invoked. * * @param index * the index to insert the handler to * @param handler * the handler to add */ public void addBrowserEventHandler(int index, GridEventHandler handler) { browserEventHandlers.add(index, handler); } /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { refreshHeader(); fireEvent(new SortEvent<>(this, Collections.unmodifiableList(sortOrder), userOriginated)); } private int getLastVisibleRowIndex() { int lastRowIndex = escalator.getVisibleRowRange().getEnd(); int footerTop = escalator.getFooter().getElement().getAbsoluteTop(); Element lastRow; do { lastRow = escalator.getBody().getRowElement(--lastRowIndex); } while (lastRow.getAbsoluteTop() > footerTop); return lastRowIndex; } private int getFirstVisibleRowIndex() { int firstRowIndex = escalator.getVisibleRowRange().getStart(); int headerBottom = escalator.getHeader().getElement() .getAbsoluteBottom(); Element firstRow = escalator.getBody().getRowElement(firstRowIndex); while (firstRow.getAbsoluteBottom() < headerBottom) { firstRow = escalator.getBody().getRowElement(++firstRowIndex); } return firstRowIndex; } /** * Adds a scroll handler to this grid. * * @param handler * the scroll handler to add * @return a handler registration for the registered scroll handler */ public HandlerRegistration addScrollHandler(ScrollHandler handler) { return addHandler(handler, ScrollEvent.TYPE); } @Override public boolean isWorkPending() { return escalator.isWorkPending() || dataSource.isWaitingForData() || autoColumnWidthsRecalculator.isScheduled() || editor.isWorkPending(); } /** * Returns whether columns can be reordered with drag and drop. * * @since 7.5.0 * @return true if columns can be reordered, false otherwise */ public boolean isColumnReorderingAllowed() { return columnReorderingAllowed; } /** * Sets whether column reordering with drag and drop is allowed or not. * * @since 7.5.0 * @param columnReorderingAllowed * specifies whether column reordering is allowed */ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { this.columnReorderingAllowed = columnReorderingAllowed; } /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. * * @param orderedColumns * array of columns in wanted order */ @SuppressWarnings("unchecked") public void setColumnOrder(Column... orderedColumns) { setColumnOrder(false, orderedColumns); } @SuppressWarnings("unchecked") private void setColumnOrder(boolean isUserOriginated, Column... orderedColumns) { List> newOrder = new ArrayList<>(); if (selectionColumn != null) { newOrder.add(selectionColumn); } int i = 0; for (Column column : orderedColumns) { if (columns.contains(column)) { newOrder.add(column); ++i; } else { throw new IllegalArgumentException("Given column at index " + i + " does not exist in Grid"); } } if (columns.size() != newOrder.size()) { columns.stream().filter(col -> !newOrder.contains(col)) .forEach(newOrder::add); } if (columns.equals(newOrder)) { // No changes in order. return; } // Prepare event for firing ColumnReorderEvent event = new ColumnReorderEvent<>(columns, newOrder, isUserOriginated); // Trigger ComplexRenderer.destroy for old content ColumnConfiguration conf = getEscalator().getColumnConfiguration(); conf.removeColumns(0, conf.getColumnCount()); // Update the order columns = newOrder; // Do ComplexRenderer.init and render new content conf.insertColumns(0, getVisibleColumns().size()); // Number of frozen columns should be kept same #16901 updateFrozenColumns(); // Update column widths. for (Column column : columns) { column.reapplyWidth(); } // Recalculate all the colspans for (HeaderRow row : header.getRows()) { row.calculateColspans(); } for (FooterRow row : footer.getRows()) { row.calculateColspans(); } columnHider.updateTogglesOrder(); fireEvent(event); } /** * Sets the style generator that is used for generating styles for cells. * * @param cellStyleGenerator * the cell style generator to set, or null to * remove a previously set generator */ public void setCellStyleGenerator( CellStyleGenerator cellStyleGenerator) { this.cellStyleGenerator = cellStyleGenerator; requestRefreshBody(); } /** * Gets the style generator that is used for generating styles for cells. * * @return the cell style generator, or null if no generator is * set */ public CellStyleGenerator getCellStyleGenerator() { return cellStyleGenerator; } /** * Sets the style generator that is used for generating styles for rows. * * @param rowStyleGenerator * the row style generator to set, or null to remove * a previously set generator */ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) { this.rowStyleGenerator = rowStyleGenerator; requestRefreshBody(); } /** * Gets the style generator that is used for generating styles for rows. * * @return the row style generator, or null if no generator is * set */ public RowStyleGenerator getRowStyleGenerator() { return rowStyleGenerator; } private static void setCustomStyleName(Element element, String styleName) { assert element != null; String oldStyleName = element .getPropertyString(CUSTOM_STYLE_PROPERTY_NAME); if (!SharedUtil.equals(oldStyleName, styleName)) { if (oldStyleName != null && !oldStyleName.isEmpty()) { element.removeClassName(oldStyleName); } if (styleName != null && !styleName.isEmpty()) { element.addClassName(styleName); } element.setPropertyString(CUSTOM_STYLE_PROPERTY_NAME, styleName); } } /** * Opens the editor over the row with the given index. * * @param rowIndex * the index of the row to be edited * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is already in edit mode */ public void editRow(int rowIndex) { editor.editRow(rowIndex); } /** * Returns whether the editor is currently open on some row. * * @return {@code true} if the editor is active, {@code false} otherwise. */ public boolean isEditorActive() { return editor.getState() != State.INACTIVE; } /** * Saves any unsaved changes in the editor to the data source. * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is not in edit mode */ public void saveEditor() { editor.save(); } /** * Cancels the currently active edit and hides the editor. Any changes that * are not {@link #saveEditor() saved} are lost. * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is not in edit mode */ public void cancelEditor() { editor.cancel(); } /** * Returns the handler responsible for binding data and editor widgets to * the editor. * * @return the editor handler or null if not set */ public EditorHandler getEditorHandler() { return editor.getHandler(); } /** * Sets the handler responsible for binding data and editor widgets to the * editor. * * @param handler * the new editor handler * * @throws IllegalStateException * if the editor is currently in edit mode */ public void setEditorHandler(EditorHandler handler) { editor.setHandler(handler); } /** * Returns the enabled state of the editor. * * @return true if editing is enabled, false otherwise */ public boolean isEditorEnabled() { return editor.isEnabled(); } /** * Sets the enabled state of the editor. * * @param enabled * true to enable editing, false to disable * * @throws IllegalStateException * if in edit mode and trying to disable * @throws IllegalStateException * if the editor handler is not set */ public void setEditorEnabled(boolean enabled) { editor.setEnabled(enabled); } /** * Returns the editor widget associated with the given column. If the editor * is not active, returns null. * * @param column * the column * @return the widget if the editor is open, null otherwise */ public Widget getEditorWidget(Column column) { return editor.getWidget(column); } /** * Sets the caption on the save button in the Grid editor. * * @param saveCaption * the caption to set * @throws IllegalArgumentException * if {@code saveCaption} is {@code null} */ public void setEditorSaveCaption(String saveCaption) throws IllegalArgumentException { editor.setSaveCaption(saveCaption); } /** * Gets the current caption on the save button in the Grid editor. * * @return the current caption on the save button */ public String getEditorSaveCaption() { return editor.getSaveCaption(); } /** * Sets the caption on the cancel button in the Grid editor. * * @param cancelCaption * the caption to set * @throws IllegalArgumentException * if {@code cancelCaption} is {@code null} */ public void setEditorCancelCaption(String cancelCaption) throws IllegalArgumentException { editor.setCancelCaption(cancelCaption); } /** * Gets the caption on the cancel button in the Grid editor. * * @return the current caption on the cancel button */ public String getEditorCancelCaption() { return editor.getCancelCaption(); } @Override protected void onAttach() { super.onAttach(); // Grid was just attached to DOM. Column widths should be // calculated. recalculateColumnWidths(); if (getEscalator().getBody().getRowCount() == 0 && dataSource != null) { setEscalatorSizeFromDataSource(); } for (int row : reattachVisibleDetails) { setDetailsVisible(row, true); } reattachVisibleDetails.clear(); } @Override protected void onDetach() { Set details = new HashSet<>(visibleDetails); reattachVisibleDetails.clear(); reattachVisibleDetails.addAll(details); for (int row : details) { setDetailsVisible(row, false); } super.onDetach(); } @Override public void onResize() { super.onResize(); /* * Delay calculation to be deferred so Escalator can do it's magic. */ Scheduler.get().scheduleFinally(() -> { if (escalator .getInnerWidth() != autoColumnWidthsRecalculator.lastCalculatedInnerWidth) { recalculateColumnWidths(); } // Vertical resizing could make editor positioning invalid so it // needs to be recalculated on resize if (isEditorActive()) { editor.updateVerticalScrollPosition(); } // if there is a resize, we need to refresh the body to avoid an // off-by-one error which occurs when the user scrolls all the // way to the bottom. refreshBody(); }); } /** * Grid does not support adding Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @param w * irrelevant * @throws UnsupportedOperationException * always */ @Override @Deprecated public void add(Widget w) { throw new UnsupportedOperationException( "Cannot add widgets to Grid with this method"); } /** * Grid does not support clearing Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @throws UnsupportedOperationException * always */ @Override @Deprecated public void clear() { throw new UnsupportedOperationException( "Cannot clear widgets from Grid this way"); } /** * Grid does not support iterating through Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @return never * @throws UnsupportedOperationException * always */ @Override @Deprecated public Iterator iterator() { throw new UnsupportedOperationException( "Cannot iterate through widgets in Grid this way"); } /** * Grid does not support removing Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @return always false */ @Override @Deprecated public boolean remove(Widget w) { /* * This is the method that is the sole reason to have Grid implement * HasWidget - when Vaadin removes a Component from the hierarchy, the * corresponding Widget will call removeFromParent() on itself. GWT will * check there that its parent (i.e. Grid) implements HasWidgets, and * will call this remove(Widget) method. * * tl;dr: all this song and dance to make sure GWT's sanity checks * aren't triggered, even though they effectively do nothing interesting * from Grid's perspective. */ return false; } /** * Accesses the package private method Widget#setParent() * * @param widget * The widget to access * @param parent * The parent to set */ private static final native void setParent(Widget widget, Grid parent) /*-{ widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); }-*/; private static final native void onAttach(Widget widget) /*-{ widget.@Widget::onAttach()(); }-*/; private static final native void onDetach(Widget widget) /*-{ widget.@Widget::onDetach()(); }-*/; @Override protected void doAttachChildren() { if (sidebar.getParent() == this) { onAttach(sidebar); } } @Override protected void doDetachChildren() { if (sidebar.getParent() == this) { onDetach(sidebar); } } private void attachWidget(Widget w, Element parent) { assert w.getParent() == null; parent.appendChild(w.getElement()); setParent(w, this); } private void detachWidget(Widget w) { assert w.getParent() == this; setParent(w, null); w.getElement().removeFromParent(); } /** * Resets all cached pixel sizes and reads new values from the DOM. This * methods should be used e.g. when styles affecting the dimensions of * elements in this grid have been changed. */ public void resetSizesFromDom() { getEscalator().resetSizesFromDom(); } /** * Sets a new details generator for row details. *

* The currently opened row details will be re-rendered. * * @since 7.5.0 * @param detailsGenerator * the details generator to set * @throws IllegalArgumentException * if detailsGenerator is null; */ public void setDetailsGenerator(DetailsGenerator detailsGenerator) throws IllegalArgumentException { if (detailsGenerator == null) { throw new IllegalArgumentException( "Details generator may not be null"); } for (Integer index : visibleDetails) { setDetailsVisible(index, false); } this.detailsGenerator = detailsGenerator; // this will refresh all visible spacers escalator.getBody().setSpacerUpdater(gridSpacerUpdater); } /** * Gets the current details generator for row details. * * @since 7.5.0 * @return the detailsGenerator the current details generator */ public DetailsGenerator getDetailsGenerator() { return detailsGenerator; } /** * Shows or hides the details for a specific row. *

* This method does nothing if trying to set show already-visible details, * or hide already-hidden details. * * @since 7.5.0 * @param rowIndex * the index of the affected row * @param visible * true to show the details, or false * to hide them * @see #isDetailsVisible(int) */ public void setDetailsVisible(int rowIndex, boolean visible) { if (DetailsGenerator.NULL.equals(detailsGenerator)) { return; } Integer rowIndexInteger = Integer.valueOf(rowIndex); /* * We want to prevent opening a details row twice, so any subsequent * openings (or closings) of details is a NOOP. * * When a details row is opened, it is given an arbitrary height * (because Escalator requires a height upon opening). Only when it's * opened, Escalator will ask the generator to generate a widget, which * we then can measure. When measured, we correct the initial height by * the original height. * * Without this check, we would override the measured height, and revert * back to the initial, arbitrary, height which would most probably be * wrong. * * see GridSpacerUpdater.init for implementation details. * * The order of operations isn't entirely stable. Sometimes Escalator * knows about the spacer visibility updates first and doesn't need * updating again but Grid's visibleDetails set still does. */ boolean isVisible = isDetailsVisible(rowIndex); boolean isVisibleInEscalator = escalator.getBody() .spacerExists(rowIndex); if (visible) { if (!isVisibleInEscalator) { escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); } if (!isVisible) { visibleDetails.add(rowIndexInteger); } } else { if (isVisibleInEscalator) { escalator.getBody().setSpacer(rowIndex, -1); } if (isVisible) { visibleDetails.remove(rowIndexInteger); } } } /** * Check whether the details for a row is visible or not. * * @since 7.5.0 * @param rowIndex * the index of the row for which to check details * @return true if the details for the given row is visible * @see #setDetailsVisible(int, boolean) */ public boolean isDetailsVisible(int rowIndex) { return visibleDetails.contains(Integer.valueOf(rowIndex)); } /** * Reset the details row with current contents. * * @since 8.13 * @param rowIndex * the index of the row for which details should be reset */ public void resetVisibleDetails(int rowIndex) { escalator.getBody().resetSpacer(rowIndex); } /** * Update details row height. * * @since 8.1.3 * @param rowIndex * the index of the row for which to update details height * @param height * new height of the details row */ public void setDetailsHeight(int rowIndex, double height) { escalator.getBody().setSpacer(rowIndex, height); } /** * Requests that the column widths should be recalculated. *

* The actual recalculation is not necessarily done immediately so you * cannot rely on the columns being the correct width after the call * returns. * * @since 7.4.1 */ public void recalculateColumnWidths() { autoColumnWidthsRecalculator.schedule(); } /** * Gets the customizable menu bar that is by default used for toggling * column hidability. The application developer is allowed to add their * custom items to the end of the menu, but should try to avoid modifying * the items in the beginning of the menu that control the column hiding if * any columns are marked as hidable. A toggle for opening the menu will be * displayed whenever the menu contains at least one item. * * @since 7.5.0 * @return the menu bar */ public MenuBar getSidebarMenu() { return sidebar.menuBar; } /** * Tests whether the sidebar menu is currently open. * * @since 7.5.0 * @see #getSidebarMenu() * @return true if the sidebar is open; false if * it is closed */ public boolean isSidebarOpen() { return sidebar.isOpen(); } /** * Sets whether the sidebar menu is open. * * * @since 7.5.0 * @see #getSidebarMenu() * @see #isSidebarOpen() * @param sidebarOpen * true to open the sidebar; false to * close it */ public void setSidebarOpen(boolean sidebarOpen) { if (sidebarOpen) { sidebar.open(); } else { sidebar.close(); } } @Override public int getTabIndex() { return FocusUtil.getTabIndex(this); } @Override public void setAccessKey(char key) { FocusUtil.setAccessKey(this, key); } @Override public void setFocus(boolean focused) { FocusUtil.setFocus(this, focused); } @Override public void setTabIndex(int index) { FocusUtil.setTabIndex(this, index); } @Override public void focus() { setFocus(true); } /** * Sets the buffered editor mode. * * @since 7.6 * @param editorBuffered * true to enable buffered editor, * false to disable it */ public void setEditorBuffered(boolean editorBuffered) { editor.setBuffered(editorBuffered); } /** * Gets the buffered editor mode. * * @since 7.6 * @return true if buffered editor is enabled, * false otherwise */ public boolean isEditorBuffered() { return editor.isBuffered(); } /** * Returns the {@link EventCellReference} for the latest event fired from * this Grid. *

* Note: This cell reference will be updated when firing the next event. * * @since 7.5 * @return event cell reference */ public EventCellReference getEventCell() { return eventCell; } /** * Returns a CellReference for the cell to which the given element belongs * to. * * @since 7.6 * @param element * Element to find from the cell's content. * @return CellReference or null if cell was not found. */ public CellReference getCellReference(Element element) { RowContainer container = getEscalator().findRowContainer(element); if (container != null) { Cell cell = container.getCell(element); if (cell != null) { EventCellReference cellRef = new EventCellReference<>(this); cellRef.set(cell, getSectionFromContainer(container)); return cellRef; } } return null; } /** * Returns the selection column for the grid if the selection model is of * type {@link SelectionModelWithSelectionColumn}. * * @return the select all checkbox, or an empty optional if not in use */ public Optional getSelectionColumn() { return Optional.ofNullable(selectionColumn); } }