/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.gen2.table.client;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.gen2.event.shared.HandlerRegistration;
import com.google.gwt.gen2.table.event.client.CellHighlightEvent;
import com.google.gwt.gen2.table.event.client.CellHighlightHandler;
import com.google.gwt.gen2.table.event.client.CellUnhighlightEvent;
import com.google.gwt.gen2.table.event.client.CellUnhighlightHandler;
import com.google.gwt.gen2.table.event.client.HasCellHighlightHandlers;
import com.google.gwt.gen2.table.event.client.HasCellUnhighlightHandlers;
import com.google.gwt.gen2.table.event.client.HasRowHighlightHandlers;
import com.google.gwt.gen2.table.event.client.HasRowSelectionHandlers;
import com.google.gwt.gen2.table.event.client.HasRowUnhighlightHandlers;
import com.google.gwt.gen2.table.event.client.RowHighlightEvent;
import com.google.gwt.gen2.table.event.client.RowHighlightHandler;
import com.google.gwt.gen2.table.event.client.RowSelectionEvent;
import com.google.gwt.gen2.table.event.client.RowSelectionHandler;
import com.google.gwt.gen2.table.event.client.RowUnhighlightEvent;
import com.google.gwt.gen2.table.event.client.RowUnhighlightHandler;
import com.google.gwt.gen2.table.event.client.TableEvent.Row;
import com.google.gwt.gen2.table.override.client.Grid;
import com.google.gwt.gen2.table.override.client.OverrideDOM;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* A variation of the {@link Grid} that supports row or cell highlight and row
* selection.
*
* <h3>CSS Style Rules</h3>
*
* <ul class="css">
*
* <li>tr.selected { applied to selected rows }</li>
*
* <li>tr.highlighted { applied to row currently being highlighted }</li>
*
* <li>td.highlighted { applied to cell currently being highlighted }</li>
*
* </ul>
*/
public class SelectionGrid extends Grid implements HasRowHighlightHandlers,
HasRowUnhighlightHandlers, HasCellHighlightHandlers,
HasCellUnhighlightHandlers, HasRowSelectionHandlers {
/**
* This class contains methods used to format a table's cells.
*/
public class SelectionGridCellFormatter extends CellFormatter {
@Override
protected Element getRawElement(int row, int column) {
if (selectionPolicy.hasInputColumn()) {
column += 1;
}
return super.getRawElement(row, column);
}
}
/**
* This class contains methods used to format a table's rows.
*/
public class SelectionGridRowFormatter extends RowFormatter {
@Override
protected Element getRawElement(int row) {
return super.getRawElement(row);
}
}
/**
* Selection policies.
*
* <ul>
* <li>ONE_ROW - one row can be selected at a time</li>
* <li>MULTI_ROW - multiple rows can be selected at a time</li>
* <li>CHECKBOX - multiple rows can be selected using checkboxes</li>
* <li>RADIO - one row can be selected using radio buttons</li>
* </ul>
*/
public static enum SelectionPolicy {
ONE_ROW(null), MULTI_ROW(null), CHECKBOX("<input type='checkbox'/>"), RADIO(
"<input name='%NAME%' type='radio'/>");
private String inputHtml;
private SelectionPolicy(String inputHtml) {
this.inputHtml = inputHtml;
}
/**
* @return true if the policy requires a selection column
*/
public boolean hasInputColumn() {
return inputHtml != null;
}
/**
* @return the HTML string used for this policy
*/
private String getInputHtml() {
return inputHtml;
}
}
/**
* Unique IDs assigned to the grids.
*/
private static int uniqueID = 0;
/**
* The cell element currently being highlight.
*/
private Element highlightedCellElem = null;
/**
* The index of the cell currently being highlight.
*/
private int highlightedCellIndex = -1;
/**
* The row element currently being highlight.
*/
private Element highlightedRowElem = null;
/**
* The index of the row currently being highlight.
*/
private int highlightedRowIndex = -1;
/**
* Unique ID of this grid.
*/
private int id;
/**
* The index of the row that the user selected last.
*/
private int lastSelectedRowIndex = -1;
/**
* The rows that are currently selected.
*/
private Map<Integer, Element> selectedRows = new HashMap<Integer, Element>();
/**
* A boolean indicating if selection is enabled disabled.
*/
private boolean selectionEnabled = true;
/**
* The selection policy determines if the user can select zero, one, or many
* rows.
*/
private SelectionPolicy selectionPolicy = SelectionPolicy.MULTI_ROW;
/**
* Construct a new {@link SelectionGrid}.
*/
public SelectionGrid() {
super();
id = uniqueID++;
setCellFormatter(new SelectionGridCellFormatter());
setRowFormatter(new SelectionGridRowFormatter());
// Sink highlight and selection events
sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT | Event.ONMOUSEDOWN
| Event.ONCLICK);
}
/**
* Constructs a {@link SelectionGrid} with the requested size.
*
* @param rows the number of rows
* @param columns the number of columns
* @throws IndexOutOfBoundsException
*/
public SelectionGrid(int rows, int columns) {
this();
resize(rows, columns);
}
public HandlerRegistration addCellHighlightHandler(
CellHighlightHandler handler) {
return addHandler(CellHighlightEvent.TYPE, handler);
}
public HandlerRegistration addCellUnhighlightHandler(
CellUnhighlightHandler handler) {
return addHandler(CellUnhighlightEvent.TYPE, handler);
}
public HandlerRegistration addRowHighlightHandler(RowHighlightHandler handler) {
return addHandler(RowHighlightEvent.TYPE, handler);
}
public HandlerRegistration addRowSelectionHandler(RowSelectionHandler handler) {
return addHandler(RowSelectionEvent.TYPE, handler);
}
public HandlerRegistration addRowUnhighlightHandler(
RowUnhighlightHandler handler) {
return addHandler(RowUnhighlightEvent.TYPE, handler);
}
/**
* Deselect all selected rows in the data table.
*/
public void deselectAllRows() {
deselectAllRows(true);
}
/**
* Deselect a row in the grid. This method is safe to call even if the row is
* not selected, or doesn't exist (out of bounds).
*
* @param row the row index
*/
public void deselectRow(int row) {
deselectRow(row, true);
}
/**
* @return the set of selected row indexes
*/
public Set<Integer> getSelectedRows() {
return selectedRows.keySet();
}
/**
* Explicitly gets the {@link SelectionGridCellFormatter}. The results of
* {@link com.google.gwt.gen2.table.override.client.HTMLTable#getCellFormatter()}
* may also be downcast to a {@link SelectionGridCellFormatter}.
*
* @return the FlexTable's cell formatter
*/
public SelectionGridCellFormatter getSelectionGridCellFormatter() {
return (SelectionGridCellFormatter) getCellFormatter();
}
/**
* Explicitly gets the {@link SelectionGridRowFormatter}. The results of
* {@link com.google.gwt.gen2.table.override.client.HTMLTable#getRowFormatter()}
* may also be downcast to a {@link SelectionGridRowFormatter}.
*
* @return the FlexTable's cell formatter
*/
public SelectionGridRowFormatter getSelectionGridRowFormatter() {
return (SelectionGridRowFormatter) getRowFormatter();
}
/**
* @return the selection policy
*/
public SelectionPolicy getSelectionPolicy() {
return selectionPolicy;
}
@Override
public int insertRow(int beforeRow) {
deselectAllRows();
return super.insertRow(beforeRow);
}
/**
* @param row the row index
* @return true if the row is selected, false if not
*/
public boolean isRowSelected(int row) {
return selectedRows.containsKey(new Integer(row));
}
/**
* @return true if selection is enabled
*/
public boolean isSelectionEnabled() {
return selectionEnabled;
}
@Override
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
switch (DOM.eventGetType(event)) {
// Highlight the cell on mouse over
case Event.ONMOUSEOVER:
Element cellElem = getEventTargetCell(event);
if (cellElem != null) {
highlightCell(cellElem);
}
break;
// Unhighlight on mouse out
case Event.ONMOUSEOUT:
Element toElem = DOM.eventGetToElement(event);
if (highlightedRowElem != null
&& (toElem == null || !highlightedRowElem.isOrHasChild(toElem))) {
// Check that the coordinates are not directly over the cell
int clientX = event.getClientX() + Window.getScrollLeft();
int clientY = event.getClientY() + Window.getScrollTop();
int rowLeft = highlightedRowElem.getAbsoluteLeft();
int rowTop = highlightedRowElem.getAbsoluteTop();
int rowWidth = highlightedRowElem.getOffsetWidth();
int rowHeight = highlightedRowElem.getOffsetHeight();
int rowBottom = rowTop + rowHeight;
int rowRight = rowLeft + rowWidth;
if (clientX > rowLeft && clientX < rowRight && clientY > rowTop
&& clientY < rowBottom) {
return;
}
// Unhighlight the current cell
highlightCell(null);
}
break;
// Prevent native inputs from being checked
case Event.ONCLICK:
onMouseClick(event);
break;
}
}
protected void onMouseClick(Event event) {
Element targetRow = null;
Element targetCell = null;
// Ignore if selection is disabled
if (!selectionEnabled) {
return;
}
// Get the target row
targetCell = getEventTargetCell(event);
if (targetCell == null) {
return;
}
targetRow = DOM.getParent(targetCell);
int targetRowIndex = getRowIndex(targetRow);
// Select the row
if (selectionPolicy == SelectionPolicy.MULTI_ROW) {
boolean shiftKey = DOM.eventGetShiftKey(event);
boolean ctrlKey = DOM.eventGetCtrlKey(event)
|| DOM.eventGetMetaKey(event);
// Prevent default text selection
if (ctrlKey || shiftKey) {
event.preventDefault();
}
// Select the rows
selectRow(targetRowIndex, ctrlKey, shiftKey);
} else if (selectionPolicy == SelectionPolicy.ONE_ROW
|| (selectionPolicy == SelectionPolicy.RADIO && targetCell == targetRow.getFirstChild())) {
selectRow(-1, targetRow, true, true);
lastSelectedRowIndex = targetRowIndex;
} else if (selectionPolicy == SelectionPolicy.CHECKBOX
&& targetCell == targetRow.getFirstChild()) {
selectRow(targetRowIndex, true, DOM.eventGetShiftKey(event));
}
}
@Override
public void removeRow(int row) {
deselectAllRows();
super.removeRow(row);
}
/**
* Select all rows in the table.
*/
public void selectAllRows() {
// Get the currently selected rows
Set<Row> oldRowSet = getSelectedRowsSet();
// Select all rows
RowFormatter rowFormatter = getRowFormatter();
int rowCount = getRowCount();
for (int i = 0; i < rowCount; i++) {
if (!selectedRows.containsKey(i)) {
selectRow(i, rowFormatter.getElement(i), false, false);
}
}
// Trigger the event
fireRowSelectionEvent(oldRowSet);
}
/**
* Select a row in the data table.
*
* @param row the row index
* @param unselectAll unselect all other rows first
* @throws IndexOutOfBoundsException
*/
public void selectRow(int row, boolean unselectAll) {
selectRow(row, getRowFormatter().getElement(row), unselectAll, true);
}
/**
* Select a row in the data table. Simulate the effect of a shift click and/or
* control click. This method ignores the selection policy, which only applies
* to user selection via mouse events.
*
* @param row the row index
* @param ctrlKey true to simulate a control click
* @param shiftKey true to simulate a shift selection
* @throws IndexOutOfBoundsException
*/
public void selectRow(int row, boolean ctrlKey, boolean shiftKey) {
// Check the row bounds
checkRowBounds(row);
// Get the old list of selected rows
Set<Row> oldRowList = getSelectedRowsSet();
// Deselect all rows
if (!ctrlKey) {
deselectAllRows(false);
}
boolean isSelected = selectedRows.containsKey(new Integer(row));
if (shiftKey && (lastSelectedRowIndex > -1)) {
// Shift+select rows
SelectionGridRowFormatter formatter = getSelectionGridRowFormatter();
int firstRow = Math.min(row, lastSelectedRowIndex);
int lastRow = Math.max(row, lastSelectedRowIndex);
lastRow = Math.min(lastRow, getRowCount() - 1);
for (int curRow = firstRow; curRow <= lastRow; curRow++) {
if (isSelected) {
deselectRow(curRow, false);
} else {
selectRow(curRow, formatter.getRawElement(curRow), false, false);
}
}
// Fire Event
lastSelectedRowIndex = row;
fireRowSelectionEvent(oldRowList);
} else if (isSelected) {
// Ctrl+unselect a selected row
deselectRow(row, false);
lastSelectedRowIndex = row;
fireRowSelectionEvent(oldRowList);
} else {
// Select the row
SelectionGridRowFormatter formatter = getSelectionGridRowFormatter();
selectRow(row, formatter.getRawElement(row), false, false);
lastSelectedRowIndex = row;
fireRowSelectionEvent(oldRowList);
}
}
/**
* Enable or disable row selection.
*
* @param enabled true to enable, false to disable
*/
public void setSelectionEnabled(boolean enabled) {
selectionEnabled = enabled;
// Update the input elements
if (selectionPolicy.hasInputColumn()) {
SelectionGridCellFormatter formatter = getSelectionGridCellFormatter();
int rowCount = getRowCount();
for (int i = 0; i < rowCount; i++) {
Element td = formatter.getRawElement(i, -1);
setInputEnabled(selectionPolicy, td, enabled);
}
}
}
/**
* Set the selection policy, which determines if the user can select zero,
* one, or multiple rows.
*
* @param selectionPolicy the selection policy
*/
public void setSelectionPolicy(SelectionPolicy selectionPolicy) {
if (this.selectionPolicy == selectionPolicy) {
return;
}
deselectAllRows();
// Update the input column
if (selectionPolicy.hasInputColumn()) {
if (this.selectionPolicy.hasInputColumn()) {
// Update the existing input column
String inputHtml = getInputHtml(selectionPolicy);
for (int i = 0; i < numRows; i++) {
Element tr = getRowFormatter().getElement(i);
tr.getFirstChildElement().setInnerHTML(inputHtml);
}
} else {
// Add an input column to every row
String inputHtml = getInputHtml(selectionPolicy);
Element td = createCell();
td.setInnerHTML(inputHtml);
for (int i = 0; i < numRows; i++) {
Element tr = getRowFormatter().getElement(i);
tr.insertBefore(td.cloneNode(true), tr.getFirstChildElement());
}
}
} else if (this.selectionPolicy.hasInputColumn()) {
// Remove the input column from every row
for (int i = 0; i < numRows; i++) {
Element tr = getRowFormatter().getElement(i);
tr.removeChild(tr.getFirstChildElement());
}
}
this.selectionPolicy = selectionPolicy;
// Update the enabled state
setSelectionEnabled(selectionEnabled);
}
@Override
protected Element createRow() {
Element tr = super.createRow();
if (selectionPolicy.hasInputColumn()) {
Element td = createCell();
td.setPropertyString("align", "center");
td.setInnerHTML(getInputHtml(selectionPolicy));
DOM.insertChild(tr, td, 0);
if (!selectionEnabled) {
setInputEnabled(selectionPolicy, td, false);
}
}
return tr;
}
/**
* Deselect all selected rows in the data table.
*
* @param fireEvent true to fire events
*/
protected void deselectAllRows(boolean fireEvent) {
// Get the old list of selected rows
Set<Row> oldRows = null;
if (fireEvent) {
oldRows = getSelectedRowsSet();
}
// Deselect all rows
boolean hasInputColumn = selectionPolicy.hasInputColumn();
for (Element rowElem : selectedRows.values()) {
setStyleName(rowElem, "selected", false);
if (hasInputColumn) {
setInputSelected(getSelectionPolicy(),
(Element) rowElem.getFirstChildElement(), false);
}
}
// Clear out the rows
selectedRows.clear();
// Fire event
if (fireEvent) {
fireRowSelectionEvent(oldRows);
}
}
/**
* Deselect a row in the grid. This method is safe to call even if the row is
* not selected, or doesn't exist (out of bounds).
*
* @param row the row index
* @param fireEvent true to fire events
*/
protected void deselectRow(int row, boolean fireEvent) {
Element rowElem = selectedRows.remove(new Integer(row));
if (rowElem != null) {
// Get the old list of selected rows
Set<Row> oldRows = null;
if (fireEvent) {
oldRows = getSelectedRowsSet();
}
// Deselect the row
setStyleName(rowElem, "selected", false);
if (selectionPolicy.hasInputColumn()) {
setInputSelected(getSelectionPolicy(),
(Element) rowElem.getFirstChildElement(), false);
}
// Fire Event
if (fireEvent) {
fireRowSelectionEvent(oldRows);
}
}
}
/**
* Fire a {@link RowSelectionEvent}. This method will automatically add the
* currently selected rows.
*
* @param oldRowSet the set of previously selected rows
*/
protected void fireRowSelectionEvent(Set<Row> oldRowSet) {
Set<Row> newRowList = getSelectedRowsSet();
if (newRowList.equals(oldRowSet)) {
return;
}
fireEvent(new RowSelectionEvent(oldRowSet, newRowList));
}
@Override
protected int getCellIndex(Element rowElem, Element cellElem) {
int index = super.getCellIndex(rowElem, cellElem);
if (selectionPolicy.hasInputColumn()) {
index--;
}
return index;
}
@Override
protected int getDOMCellCount(int row) {
int count = super.getDOMCellCount(row);
if (getSelectionPolicy().hasInputColumn()) {
count--;
}
return count;
}
/**
* Get the html used to create the native input selection element.
*
* @param selectionPolicy the associated {@link SelectionPolicy}
* @return the html representation of the input element
*/
protected String getInputHtml(SelectionPolicy selectionPolicy) {
String inputHtml = selectionPolicy.getInputHtml();
if (inputHtml != null) {
inputHtml = inputHtml.replace("%NAME%", "__gwtSelectionGrid" + id);
}
return inputHtml;
}
/**
* @return a map or selected row indexes to their elements
*/
protected Map<Integer, Element> getSelectedRowsMap() {
return selectedRows;
}
/**
* @return a list of selected rows to pass into a {@link RowSelectionEvent}
*/
protected Set<Row> getSelectedRowsSet() {
Set<Row> rowSet = new TreeSet<Row>();
for (Integer rowIndex : selectedRows.keySet()) {
rowSet.add(new Row(rowIndex.intValue()));
}
return rowSet;
}
/**
* Set the current highlighted cell.
*
* @param cellElem the cell element
*/
protected void highlightCell(Element cellElem) {
// Ignore if the cell is already being highlighted
if (cellElem == highlightedCellElem) {
return;
}
// Get the row element
Element rowElem = null;
if (cellElem != null) {
rowElem = DOM.getParent(cellElem);
}
// Unhighlight the current cell
if (highlightedCellElem != null) {
setStyleName(highlightedCellElem, "highlighted", false);
fireEvent(new CellUnhighlightEvent(highlightedRowIndex,
highlightedCellIndex));
highlightedCellElem = null;
highlightedCellIndex = -1;
// Unhighlight the current row if it changed
if (rowElem != highlightedRowElem) {
setStyleName(highlightedRowElem, "highlighted", false);
fireEvent(new RowUnhighlightEvent(highlightedRowIndex));
highlightedRowElem = null;
highlightedRowIndex = -1;
}
}
// Highlight the cell
if (cellElem != null) {
setStyleName(cellElem, "highlighted", true);
highlightedCellElem = cellElem;
highlightedCellIndex = OverrideDOM.getCellIndex(cellElem);
// Highlight the row if it changed
if (highlightedRowElem == null) {
setStyleName(rowElem, "highlighted", true);
highlightedRowElem = rowElem;
highlightedRowIndex = getRowIndex(highlightedRowElem);
fireEvent(new RowHighlightEvent(highlightedRowIndex));
}
// Fire listeners
fireEvent(new CellHighlightEvent(highlightedRowIndex,
highlightedCellIndex));
}
}
/**
* Select a row in the data table.
*
* @param row the row index, or -1 if unknown
* @param rowElem the row element
* @param unselectAll true to unselect all currently selected rows
* @param fireEvent true to fire the select event to listeners
*/
protected void selectRow(int row, Element rowElem, boolean unselectAll,
boolean fireEvent) {
// Get the row index if needed
if (row < 0) {
row = getRowIndex(rowElem);
}
// Ignore request if row already selected
Integer rowI = new Integer(row);
if (selectedRows.containsKey(rowI)) {
return;
}
// Get the old list of selected rows
Set<Row> oldRowSet = null;
if (fireEvent) {
oldRowSet = getSelectedRowsSet();
}
// Deselect current rows
if (unselectAll) {
deselectAllRows(false);
}
// Select the new row
selectedRows.put(rowI, rowElem);
setStyleName(rowElem, "selected", true);
if (selectionPolicy.hasInputColumn()) {
setInputSelected(getSelectionPolicy(),
(Element) rowElem.getFirstChildElement(), true);
}
// Fire grid listeners
if (fireEvent) {
fireRowSelectionEvent(oldRowSet);
}
}
@Override
protected void setBodyElement(Element element) {
super.setBodyElement(element);
if (!selectionEnabled) {
setSelectionEnabled(selectionEnabled);
}
}
/**
* Enabled or disabled the native input element in the given cell. This method
* should correspond with the HTML returned from {@link #getInputHtml}.
*
* @param selectionPolicy the associated {@link SelectionPolicy}
* @param td the cell containing the element
* @param enabled true to enable, false to disable
*/
protected void setInputEnabled(SelectionPolicy selectionPolicy, Element td,
boolean enabled) {
((InputElement) td.getFirstChild()).setDisabled(!enabled);
}
/**
* Select the native input element in the given cell. This method should
* correspond with the HTML returned from {@link #getInputHtml}.
*
* @param selectionPolicy the associated {@link SelectionPolicy}
* @param td the cell containing the element
* @param selected true to select, false to deselect
*/
protected void setInputSelected(SelectionPolicy selectionPolicy, Element td,
boolean selected) {
((InputElement) td.getFirstChild()).setChecked(selected);
}
}