/*
* Copyright 2010 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.view.client;
import com.google.gwt.core.shared.impl.StringCase;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An implementation of {@link CellPreviewEvent.Handler} that adds selection
* support via the spacebar and mouse clicks and handles the control key.
*
* <p>
* If the {@link HasData} source of the selection event uses a
* {@link MultiSelectionModel}, this manager additionally provides support for
* shift key to select a range of values. For all other {@link SelectionModel}s,
* only the control key is supported.
* </p>
*
* @param <T> the data type of records in the list
*/
public class DefaultSelectionEventManager<T> implements
CellPreviewEvent.Handler<T> {
/**
* An event translator that disables selection for the specified blacklisted
* columns.
*
* @param <T> the data type
*/
public static class BlacklistEventTranslator<T> implements EventTranslator<T> {
private final Set<Integer> blacklist = new HashSet<Integer>();
/**
* Construct a new {@link BlacklistEventTranslator}.
*
* @param blacklistedColumns the columns to blacklist
*/
public BlacklistEventTranslator(int... blacklistedColumns) {
if (blacklistedColumns != null) {
for (int i : blacklistedColumns) {
setColumnBlacklisted(i, true);
}
}
}
/**
* Clear all columns from the blacklist.
*/
public void clearBlacklist() {
blacklist.clear();
}
public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
return false;
}
/**
* Check if the specified column is blacklisted.
*
* @param index the column index
* @return true if blacklisted, false if not
*/
public boolean isColumnBlacklisted(int index) {
return blacklist.contains(index);
}
/**
* Set whether or not the specified column in blacklisted.
*
* @param index the column index
* @param isBlacklisted true to blacklist, false to allow selection
*/
public void setColumnBlacklisted(int index, boolean isBlacklisted) {
if (isBlacklisted) {
blacklist.add(index);
} else {
blacklist.remove(index);
}
}
public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
return isColumnBlacklisted(event.getColumn()) ? SelectAction.IGNORE
: SelectAction.DEFAULT;
}
}
/**
* Implementation of {@link EventTranslator} that only triggers selection when
* any checkbox is selected.
*
* @param <T> the data type
*/
public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
/**
* The column index of the checkbox. Other columns are ignored.
*/
private final int column;
/**
* Construct a new {@link CheckboxEventTranslator} that will trigger
* selection when any checkbox in any column is selected.
*/
public CheckboxEventTranslator() {
this(-1);
}
/**
* Construct a new {@link CheckboxEventTranslator} that will trigger
* selection when a checkbox in the specified column is selected.
*
* @param column the column index, or -1 for all columns
*/
public CheckboxEventTranslator(int column) {
this.column = column;
}
public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
return false;
}
public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
// Handle the event.
NativeEvent nativeEvent = event.getNativeEvent();
if (BrowserEvents.CLICK.equals(nativeEvent.getType())) {
// Ignore if the event didn't occur in the correct column.
if (column > -1 && column != event.getColumn()) {
return SelectAction.IGNORE;
}
// Determine if we clicked on a checkbox.
Element target = nativeEvent.getEventTarget().cast();
if ("input".equals(StringCase.toLower(target.getTagName()))) {
final InputElement input = target.cast();
if ("checkbox".equals(StringCase.toLower(input.getType()))) {
// Synchronize the checkbox with the current selection state.
input.setChecked(event.getDisplay().getSelectionModel().isSelected(
event.getValue()));
return SelectAction.TOGGLE;
}
}
return SelectAction.IGNORE;
}
// For keyboard events, do the default action.
return SelectAction.DEFAULT;
}
}
/**
* Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
*/
public static interface EventTranslator<T> {
/**
* Check whether a user selection event should clear all currently selected
* values.
*
* @param event the {@link CellPreviewEvent} to translate
*/
boolean clearCurrentSelection(CellPreviewEvent<T> event);
/**
* Translate the user selection event into a {@link SelectAction}.
*
* @param event the {@link CellPreviewEvent} to translate
*/
SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
}
/**
* The action that controls how selection is handled.
*/
public static enum SelectAction {
DEFAULT, // Perform the default action.
SELECT, // Select the value.
DESELECT, // Deselect the value.
TOGGLE, // Toggle the selected state of the value.
IGNORE; // Ignore the event.
}
/**
* An event translator that allows selection only for the specified
* whitelisted columns.
*
* @param <T> the data type
*/
public static class WhitelistEventTranslator<T> implements EventTranslator<T> {
private final Set<Integer> whitelist = new HashSet<Integer>();
/**
* Construct a new {@link WhitelistEventTranslator}.
*
* @param whitelistedColumns the columns to whitelist
*/
public WhitelistEventTranslator(int... whitelistedColumns) {
if (whitelistedColumns != null) {
for (int i : whitelistedColumns) {
setColumnWhitelisted(i, true);
}
}
}
public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
return false;
}
/**
* Clear all columns from the whitelist.
*/
public void clearWhitelist() {
whitelist.clear();
}
/**
* Check if the specified column is whitelisted.
*
* @param index the column index
* @return true if whitelisted, false if not
*/
public boolean isColumnWhitelisted(int index) {
return whitelist.contains(index);
}
/**
* Set whether or not the specified column in whitelisted.
*
* @param index the column index
* @param isWhitelisted true to whitelist, false to allow disallow selection
*/
public void setColumnWhitelisted(int index, boolean isWhitelisted) {
if (isWhitelisted) {
whitelist.add(index);
} else {
whitelist.remove(index);
}
}
public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
return isColumnWhitelisted(event.getColumn()) ? SelectAction.DEFAULT
: SelectAction.IGNORE;
}
}
/**
* Construct a new {@link DefaultSelectionEventManager} that ignores selection
* for the columns in the specified blacklist.
*
* @param <T> the data type of the display
* @param blacklistedColumns the columns to include in the blacklist
* @return a {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createBlacklistManager(
int... blacklistedColumns) {
return new DefaultSelectionEventManager<T>(new BlacklistEventTranslator<T>(
blacklistedColumns));
}
/**
* Construct a new {@link DefaultSelectionEventManager} that triggers
* selection when any checkbox in any column is clicked.
*
* @param <T> the data type of the display
* @return a {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createCheckboxManager() {
return new DefaultSelectionEventManager<T>(new CheckboxEventTranslator<T>());
}
/**
* Construct a new {@link DefaultSelectionEventManager} that triggers
* selection when a checkbox in the specified column is clicked.
*
* @param <T> the data type of the display
* @param column the column to handle
* @return a {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createCheckboxManager(
int column) {
return new DefaultSelectionEventManager<T>(new CheckboxEventTranslator<T>(
column));
}
/**
* Create a new {@link DefaultSelectionEventManager} using the specified
* {@link EventTranslator} to control which {@link SelectAction} to take for
* each event.
*
* @param <T> the data type of the display
* @param translator the {@link EventTranslator} to use
* @return a {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createCustomManager(
EventTranslator<T> translator) {
return new DefaultSelectionEventManager<T>(translator);
}
/**
* Create a new {@link DefaultSelectionEventManager} that handles selection
* via user interactions.
*
* @param <T> the data type of the display
* @return a new {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createDefaultManager() {
return new DefaultSelectionEventManager<T>(null);
}
/**
* Construct a new {@link DefaultSelectionEventManager} that allows selection
* only for the columns in the specified whitelist.
*
* @param <T> the data type of the display
* @param whitelistedColumns the columns to include in the whitelist
* @return a {@link DefaultSelectionEventManager} instance
*/
public static <T> DefaultSelectionEventManager<T> createWhitelistManager(
int... whitelistedColumns) {
return new DefaultSelectionEventManager<T>(new WhitelistEventTranslator<T>(
whitelistedColumns));
}
/**
* The last {@link HasData} that was handled.
*/
private HasData<T> lastDisplay;
/**
* The last page start.
*/
private int lastPageStart;
/**
* The last selected row index.
*/
private int lastSelectedIndex = -1;
/**
* A boolean indicating that the last shift selection was additive.
*/
private boolean shiftAdditive;
/**
* The last place where the user clicked without holding shift. Multi
* selections that use the shift key are rooted at the anchor.
*/
private int shiftAnchor = -1;
/**
* The {@link EventTranslator} that controls how selection is handled.
*/
private final EventTranslator<T> translator;
/**
* Construct a new {@link DefaultSelectionEventManager} using the specified
* {@link EventTranslator} to control which {@link SelectAction} to take for
* each event.
*
* @param translator the {@link EventTranslator} to use
*/
protected DefaultSelectionEventManager(EventTranslator<T> translator) {
this.translator = translator;
}
/**
* Update the selection model based on a user selection event.
*
* @param selectionModel the selection model to update
* @param row the absolute index of the selected row
* @param rowValue the selected row value
* @param action the {@link SelectAction} to apply
* @param selectRange true to select the range from the last selected row
* @param clearOthers true to clear the current selection
*/
public void doMultiSelection(MultiSelectionModel<? super T> selectionModel,
HasData<T> display, int row, T rowValue, SelectAction action,
boolean selectRange, boolean clearOthers) {
// Determine if we will add or remove selection.
boolean addToSelection = true;
if (action != null) {
switch (action) {
case IGNORE:
// Ignore selection.
return;
case SELECT:
addToSelection = true;
break;
case DESELECT:
addToSelection = false;
break;
case TOGGLE:
addToSelection = !selectionModel.isSelected(rowValue);
break;
}
}
// Determine which rows will be newly selected.
int pageStart = display.getVisibleRange().getStart();
if (selectRange && pageStart == lastPageStart && lastSelectedIndex > -1
&& shiftAnchor > -1 && display == lastDisplay) {
/*
* Get the new shift bounds based on the existing shift anchor and the
* selected row.
*/
int start = Math.min(shiftAnchor, row); // Inclusive.
int end = Math.max(shiftAnchor, row); // Inclusive.
if (lastSelectedIndex < start) {
// Revert previous selection if the user reselects a smaller range.
setRangeSelection(selectionModel, display, new Range(lastSelectedIndex,
start - lastSelectedIndex), !shiftAdditive, false);
} else if (lastSelectedIndex > end) {
// Revert previous selection if the user reselects a smaller range.
setRangeSelection(selectionModel, display, new Range(end + 1,
lastSelectedIndex - end), !shiftAdditive, false);
} else {
// Remember if we are adding or removing rows.
shiftAdditive = addToSelection;
}
// Update the last selected row, but do not move the shift anchor.
lastSelectedIndex = row;
// Select the range.
setRangeSelection(selectionModel, display, new Range(start, end - start
+ 1), shiftAdditive, clearOthers);
} else {
/*
* If we are not selecting a range, save the last row and set the shift
* anchor.
*/
lastDisplay = display;
lastPageStart = pageStart;
lastSelectedIndex = row;
shiftAnchor = row;
selectOne(selectionModel, rowValue, addToSelection, clearOthers);
}
}
public void onCellPreview(CellPreviewEvent<T> event) {
// Early exit if selection is already handled or we are editing.
if (event.isCellEditing() || event.isSelectionHandled()) {
return;
}
// Early exit if we do not have a SelectionModel.
HasData<T> display = event.getDisplay();
SelectionModel<? super T> selectionModel = display.getSelectionModel();
if (selectionModel == null) {
return;
}
// Check for user defined actions.
SelectAction action = (translator == null) ? SelectAction.DEFAULT
: translator.translateSelectionEvent(event);
// Handle the event based on the SelectionModel type.
if (selectionModel instanceof MultiSelectionModel<?>) {
// Add shift key support for MultiSelectionModel.
handleMultiSelectionEvent(event, action,
(MultiSelectionModel<? super T>) selectionModel);
} else {
// Use the standard handler.
handleSelectionEvent(event, action, selectionModel);
}
}
/**
* Removes all items from the selection.
*
* @param selectionModel the {@link MultiSelectionModel} to clear
*/
protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
selectionModel.clear();
}
/**
* Handle an event that could cause a value to be selected for a
* {@link MultiSelectionModel}. This overloaded method adds support for both
* the control and shift keys. If the shift key is held down, all rows between
* the previous selected row and the current row are selected.
*
* @param event the {@link CellPreviewEvent} that triggered selection
* @param action the action to handle
* @param selectionModel the {@link SelectionModel} to update
*/
protected void handleMultiSelectionEvent(CellPreviewEvent<T> event,
SelectAction action, MultiSelectionModel<? super T> selectionModel) {
NativeEvent nativeEvent = event.getNativeEvent();
String type = nativeEvent.getType();
if (BrowserEvents.CLICK.equals(type)) {
/*
* Update selection on click. Selection is toggled only if the user
* presses the ctrl key. If the user does not press the control key,
* selection is additive.
*/
boolean shift = nativeEvent.getShiftKey();
boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
boolean clearOthers = (translator == null) ? !ctrlOrMeta
: translator.clearCurrentSelection(event);
if (action == null || action == SelectAction.DEFAULT) {
action = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
}
doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
event.getValue(), action, shift, clearOthers);
} else if (BrowserEvents.KEYUP.equals(type)) {
int keyCode = nativeEvent.getKeyCode();
if (keyCode == 32) {
/*
* Update selection when the space bar is pressed. The spacebar always
* toggles selection, regardless of whether the control key is pressed.
*/
boolean shift = nativeEvent.getShiftKey();
boolean clearOthers = (translator == null) ? false
: translator.clearCurrentSelection(event);
if (action == null || action == SelectAction.DEFAULT) {
action = SelectAction.TOGGLE;
}
doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
event.getValue(), action, shift, clearOthers);
}
}
}
/**
* Handle an event that could cause a value to be selected. This method works
* for any {@link SelectionModel}. Pressing the space bar or ctrl+click will
* toggle the selection state. Clicking selects the row if it is not selected.
*
* @param event the {@link CellPreviewEvent} that triggered selection
* @param action the action to handle
* @param selectionModel the {@link SelectionModel} to update
*/
protected void handleSelectionEvent(CellPreviewEvent<T> event,
SelectAction action, SelectionModel<? super T> selectionModel) {
// Handle selection overrides.
T value = event.getValue();
if (action != null) {
switch (action) {
case IGNORE:
return;
case SELECT:
selectionModel.setSelected(value, true);
return;
case DESELECT:
selectionModel.setSelected(value, false);
return;
case TOGGLE:
selectionModel.setSelected(value, !selectionModel.isSelected(value));
return;
}
}
// Handle default selection.
NativeEvent nativeEvent = event.getNativeEvent();
String type = nativeEvent.getType();
if (BrowserEvents.CLICK.equals(type)) {
if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
// Toggle selection on ctrl+click.
selectionModel.setSelected(value, !selectionModel.isSelected(value));
} else {
// Select on click.
selectionModel.setSelected(value, true);
}
} else if (BrowserEvents.KEYUP.equals(type)) {
// Toggle selection on space.
int keyCode = nativeEvent.getKeyCode();
if (keyCode == 32) {
selectionModel.setSelected(value, !selectionModel.isSelected(value));
}
}
}
/**
* Selects the given item, optionally clearing any prior selection.
*
* @param selectionModel the {@link MultiSelectionModel} to update
* @param target the item to select
* @param selected true to select, false to deselect
* @param clearOthers true to clear all other selected items
*/
protected void selectOne(MultiSelectionModel<? super T> selectionModel,
T target, boolean selected, boolean clearOthers) {
if (clearOthers) {
clearSelection(selectionModel);
}
selectionModel.setSelected(target, selected);
}
/**
* Select or deselect a range of row indexes, optionally deselecting all other
* values.
*
* @param selectionModel the {@link MultiSelectionModel} to update
* @param display the {@link HasData} source of the selection event
* @param range the {@link Range} of rows to select or deselect
* @param addToSelection true to select, false to deselect the range
* @param clearOthers true to deselect rows not in the range
*/
protected void setRangeSelection(
MultiSelectionModel<? super T> selectionModel, HasData<T> display,
Range range, boolean addToSelection, boolean clearOthers) {
// Get the list of values to select.
List<T> toUpdate = new ArrayList<T>();
int itemCount = display.getVisibleItemCount();
int relativeStart = range.getStart() - display.getVisibleRange().getStart();
int relativeEnd = relativeStart + range.getLength();
for (int i = relativeStart; i < relativeEnd && i < itemCount; i++) {
toUpdate.add(display.getVisibleItem(i));
}
// Clear all other values.
if (clearOthers) {
clearSelection(selectionModel);
}
// Update the state of the values.
for (T value : toUpdate) {
selectionModel.setSelected(value, addToSelection);
}
}
}