org.gss_project.gss.web.client.GSSSelectionEventManager.java Source code

Java tutorial

Introduction

Here is the source code for org.gss_project.gss.web.client.GSSSelectionEventManager.java

Source

/*
 * Copyright 2011 Electronic Business Systems Ltd.
 *
 * This file is part of GSS.
 *
 * GSS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GSS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GSS.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.gss_project.gss.web.client;

/**
 * @author kman
 *
 */

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.MultiSelectionModel;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.SelectionModel;

/**
 * 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 GSSSelectionEventManager<T> implements CellPreviewEvent.Handler<T> {

    /**
     * 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.
    }

    /**
     * Construct a new {@link GSSSelectionEventManager} that triggers
     * selection when any checkbox in any column is clicked.
     * 
     * @param <T> the data type of the display
     * @return a {@link GSSSelectionEventManager} instance
     */
    public static <T> GSSSelectionEventManager<T> createCheckboxManager() {
        return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>());
    }

    /**
     * Construct a new {@link GSSSelectionEventManager} 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 GSSSelectionEventManager} instance
     */
    public static <T> GSSSelectionEventManager<T> createCheckboxManager(int column) {
        return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>(column));
    }

    /**
     * Create a new {@link GSSSelectionEventManager} 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 GSSSelectionEventManager} instance
     */
    public static <T> GSSSelectionEventManager<T> createCustomManager(EventTranslator<T> translator) {
        return new GSSSelectionEventManager<T>(translator);
    }

    /**
     * Create a new {@link GSSSelectionEventManager} that handles selection
     * via user interactions.
     * 
     * @param <T> the data type of the display
     * @return a new {@link GSSSelectionEventManager} instance
     */
    public static <T> GSSSelectionEventManager<T> createDefaultManager() {
        return new GSSSelectionEventManager<T>(null);
    }

    /**
     * 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 GSSSelectionEventManager} using the specified
     * {@link EventTranslator} to control which {@link SelectAction} to take for
     * each event.
     * 
     * @param translator the {@link EventTranslator} to use
     */
    protected GSSSelectionEventManager(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 selected row index relative to the page start
     * @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();
        boolean rightclick = "mousedown".equals(type) && nativeEvent.getButton() == NativeEvent.BUTTON_RIGHT;
        if (rightclick) {
            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;
            }
            //if the row is selected then do nothing
            if (selectionModel.isSelected(event.getValue())) {
                return;
            }
            doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(), event.getValue(), action, shift,
                    clearOthers);
        } else 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);
            if (ctrlOrMeta) {
                event.setCanceled(true);
            }
        } 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 start = range.getStart();
        int end = start + range.getLength();
        for (int i = start; i < end; i++) {
            toUpdate.add(display.getVisibleItem(i - display.getVisibleRange().getStart()));
        }
        // Clear all other values.
        if (clearOthers) {
            clearSelection(selectionModel);
        }

        // Update the state of the values.
        for (T value : toUpdate) {
            selectionModel.setSelected(value, addToSelection);
        }
    }
}