| /* |
| * 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.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(target.getTagName().toLowerCase())) { |
| final InputElement input = target.cast(); |
| if ("checkbox".equals(input.getType().toLowerCase())) { |
| // 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); |
| } |
| } |
| } |