com.google.gwt.view.client.DefaultSelectionEventManager.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.view.client.DefaultSelectionEventManager.java

Source

/*
 * 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.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 ("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 ("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 ("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 ("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 ("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);
        }
    }
}