com.google.gwt.user.cellview.client.AbstractHasData.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.cellview.client.AbstractHasData.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.user.cellview.client;

import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent.Type;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.impl.FocusImpl;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.DefaultSelectionEventManager;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.HasKeyProvider;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.RowCountChangeEvent;
import com.google.gwt.view.client.SelectionModel;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * An abstract {@link Widget} that implements {@link HasData}.
 * 
 * @param <T> the data type of each row
 */
public abstract class AbstractHasData<T> extends Composite
        implements HasData<T>, HasKeyProvider<T>, Focusable, HasKeyboardPagingPolicy {

    /**
     * Default implementation of a keyboard navigation handler.
     * 
     * @param <T> the data type of each row
     */
    public static class DefaultKeyboardSelectionHandler<T> implements CellPreviewEvent.Handler<T> {

        /**
         * The number of rows to jump when PAGE_UP or PAGE_DOWN is pressed and the
         * {@link HasKeyboardPagingPolicy.KeyboardPagingPolicy} is
         * {@link HasKeyboardPagingPolicy.KeyboardPagingPolicy#INCREASE_RANGE}.
         */
        private static final int PAGE_INCREMENT = 30;

        private final AbstractHasData<T> display;

        /**
         * Construct a new keyboard selection handler for the specified view.
         * 
         * @param display the display being handled
         */
        public DefaultKeyboardSelectionHandler(AbstractHasData<T> display) {
            this.display = display;
        }

        public AbstractHasData<T> getDisplay() {
            return display;
        }

        @Override
        public void onCellPreview(CellPreviewEvent<T> event) {
            NativeEvent nativeEvent = event.getNativeEvent();
            String eventType = event.getNativeEvent().getType();
            if ("keydown".equals(eventType) && !event.isCellEditing()) {
                /*
                 * Handle keyboard navigation, unless the cell is being edited. If the
                 * cell is being edited, we do not want to change rows.
                 * 
                 * Prevent default on navigation events to prevent default scrollbar
                 * behavior.
                 */
                switch (nativeEvent.getKeyCode()) {
                case KeyCodes.KEY_DOWN:
                    nextRow();
                    handledEvent(event);
                    return;
                case KeyCodes.KEY_UP:
                    prevRow();
                    handledEvent(event);
                    return;
                case KeyCodes.KEY_PAGEDOWN:
                    nextPage();
                    handledEvent(event);
                    return;
                case KeyCodes.KEY_PAGEUP:
                    prevPage();
                    handledEvent(event);
                    return;
                case KeyCodes.KEY_HOME:
                    home();
                    handledEvent(event);
                    return;
                case KeyCodes.KEY_END:
                    end();
                    handledEvent(event);
                    return;
                case 32:
                    // Prevent the list box from scrolling.
                    handledEvent(event);
                    return;
                }
            } else if ("click".equals(eventType)) {
                /*
                 * Move keyboard focus to the clicked row, even if the Cell is being
                 * edited. Unlike key events, we aren't moving the currently selected
                 * row, just updating it based on where the user clicked.
                 */
                int relRow = event.getIndex() - display.getPageStart();
                if (display.getKeyboardSelectedRow() != relRow) {
                    // If a natively focusable element was just clicked, then do not steal
                    // focus.
                    boolean isFocusable = false;
                    Element target = Element.as(event.getNativeEvent().getEventTarget());
                    isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
                    display.setKeyboardSelectedRow(relRow, !isFocusable);

                    // Do not cancel the event as the click may have occurred on a Cell.
                }
            } else if ("focus".equals(eventType)) {
                // Move keyboard focus to match the currently focused element.
                int relRow = event.getIndex() - display.getPageStart();
                if (display.getKeyboardSelectedRow() != relRow) {
                    // Do not steal focus as this was a focus event.
                    display.setKeyboardSelectedRow(event.getIndex(), false);

                    // Do not cancel the event as the click may have occurred on a Cell.
                    return;
                }
            }
        }

        // Visible for testing.
        void end() {
            setKeyboardSelectedRow(display.getRowCount() - 1);
        }

        void handledEvent(CellPreviewEvent<?> event) {
            event.setCanceled(true);
            event.getNativeEvent().preventDefault();
        }

        // Visible for testing.
        void home() {
            setKeyboardSelectedRow(-display.getPageStart());
        }

        // Visible for testing.
        void nextPage() {
            KeyboardPagingPolicy keyboardPagingPolicy = display.getKeyboardPagingPolicy();
            if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
                // 0th index of next page.
                setKeyboardSelectedRow(display.getPageSize());
            } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
                setKeyboardSelectedRow(display.getKeyboardSelectedRow() + PAGE_INCREMENT);
            }
        }

        // Visible for testing.
        void nextRow() {
            setKeyboardSelectedRow(display.getKeyboardSelectedRow() + 1);
        }

        // Visible for testing.
        void prevPage() {
            KeyboardPagingPolicy keyboardPagingPolicy = display.getKeyboardPagingPolicy();
            if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
                // 0th index of previous page.
                setKeyboardSelectedRow(-display.getPageSize());
            } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
                setKeyboardSelectedRow(display.getKeyboardSelectedRow() - PAGE_INCREMENT);
            }
        }

        // Visible for testing.
        void prevRow() {
            setKeyboardSelectedRow(display.getKeyboardSelectedRow() - 1);
        }

        // Visible for testing.
        void setKeyboardSelectedRow(int row) {
            display.setKeyboardSelectedRow(row, true);
        }
    }

    /**
     * Implementation of {@link HasDataPresenter.View} used by this widget.
     * 
     * @param <T> the data type of the view
     */
    private static class View<T> implements HasDataPresenter.View<T> {

        private final AbstractHasData<T> hasData;
        private boolean wasFocused;

        public View(AbstractHasData<T> hasData) {
            this.hasData = hasData;
        }

        @Override
        public <H extends EventHandler> HandlerRegistration addHandler(H handler, Type<H> type) {
            return hasData.addHandler(handler, type);
        }

        @Override
        public void replaceAllChildren(List<T> values, SelectionModel<? super T> selectionModel,
                boolean stealFocus) {
            SafeHtml html = renderRowValues(values, hasData.getPageStart(), selectionModel);

            // Removing elements can fire a blur event, which we ignore.
            hasData.isFocused = hasData.isFocused || stealFocus;
            wasFocused = hasData.isFocused;
            hasData.isRefreshing = true;
            hasData.replaceAllChildren(values, html);
            hasData.isRefreshing = false;

            // Ensure that the keyboard selected element is focusable.
            Element elem = hasData.getKeyboardSelectedElement();
            if (elem != null) {
                hasData.setFocusable(elem, true);
                if (hasData.isFocused) {
                    hasData.onFocus();
                }
            }

            fireValueChangeEvent();
        }

        @Override
        public void replaceChildren(List<T> values, int start, SelectionModel<? super T> selectionModel,
                boolean stealFocus) {
            SafeHtml html = renderRowValues(values, hasData.getPageStart() + start, selectionModel);

            // Removing elements can fire a blur event, which we ignore.
            hasData.isFocused = hasData.isFocused || stealFocus;
            wasFocused = hasData.isFocused;
            hasData.isRefreshing = true;
            hasData.replaceChildren(values, start, html);
            hasData.isRefreshing = false;

            // Ensure that the keyboard selected element is focusable.
            Element elem = hasData.getKeyboardSelectedElement();
            if (elem != null) {
                hasData.setFocusable(elem, true);
                if (hasData.isFocused) {
                    hasData.onFocus();
                }
            }

            fireValueChangeEvent();
        }

        @Override
        public void resetFocus() {
            if (wasFocused) {
                CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
                    @Override
                    public void execute() {
                        if (!hasData.resetFocusOnCell()) {
                            Element elem = hasData.getKeyboardSelectedElement();
                            if (elem != null) {
                                elem.focus();
                            }
                        }
                    }
                });
            }
        }

        @Override
        public void setKeyboardSelected(int index, boolean seleted, boolean stealFocus) {
            hasData.isFocused = hasData.isFocused || stealFocus;
            hasData.setKeyboardSelected(index, seleted, stealFocus);
        }

        @Override
        public void setLoadingState(LoadingState state) {
            hasData.isRefreshing = true;
            hasData.onLoadingStateChanged(state);
            hasData.isRefreshing = false;
        }

        /**
         * Fire a value change event.
         */
        private void fireValueChangeEvent() {
            // Use an anonymous class to override ValueChangeEvents's protected
            // constructor. We can't call ValueChangeEvent.fire() because this class
            // doesn't implement HasValueChangeHandlers.
            hasData.fireEvent(new ValueChangeEvent<List<T>>(hasData.getVisibleItems()) {
            });
        }

        /**
         * Render a list of row values.
         * 
         * @param values the row values
         * @param start the absolute start index of the values
         * @param selectionModel the {@link SelectionModel}
         * @return null, unless the implementation renders using SafeHtml
         */
        private SafeHtml renderRowValues(List<T> values, int start, SelectionModel<? super T> selectionModel) {
            try {
                SafeHtmlBuilder sb = new SafeHtmlBuilder();
                hasData.renderRowValues(sb, values, start, selectionModel);
                return sb.toSafeHtml();
            } catch (UnsupportedOperationException e) {
                // If renderRowValues throws, the implementation will render directly in
                // the replaceChildren method.
                return null;
            }
        }
    }

    /**
     * The temporary element use to convert HTML to DOM.
     */
    private static com.google.gwt.user.client.Element tmpElem;

    /**
     * Convenience method to convert the specified HTML into DOM elements and
     * return the parent of the DOM elements.
     * 
     * @param html the HTML to convert
     * @param tmpElem a temporary element
     * @return the parent element
     */
    static Element convertToElements(Widget widget, com.google.gwt.user.client.Element tmpElem, SafeHtml html) {
        // Attach an event listener so we can catch synchronous load events from
        // cached images.
        DOM.setEventListener(tmpElem, widget);

        tmpElem.setInnerHTML(html.asString());

        // Detach the event listener.
        DOM.setEventListener(tmpElem, null);

        return tmpElem;
    }

    /**
     * Convenience method to replace all children of a Widget.
     * 
     * @param widget the widget who's contents will be replaced
     * @param childContainer the container that holds the contents
     * @param html the html to set
     */
    static void replaceAllChildren(Widget widget, Element childContainer, SafeHtml html) {
        // If the widget is not attached, attach an event listener so we can catch
        // synchronous load events from cached images.
        if (!widget.isAttached()) {
            DOM.setEventListener(widget.getElement(), widget);
        }

        // Render the HTML.
        childContainer.setInnerHTML(CellBasedWidgetImpl.get().processHtml(html).asString());

        // Detach the event listener.
        if (!widget.isAttached()) {
            DOM.setEventListener(widget.getElement(), null);
        }
    }

    /**
     * Convenience method to convert the specified HTML into DOM elements and
     * replace the existing elements starting at the specified index. If the
     * number of children specified exceeds the existing number of children, the
     * remaining children should be appended.
     * 
     * @param widget the widget who's contents will be replaced
     * @param childContainer the container that holds the contents
     * @param newChildren an element containing the new children
     * @param start the start index to replace
     * @param html the HTML to convert
     */
    static void replaceChildren(Widget widget, Element childContainer, Element newChildren, int start,
            SafeHtml html) {
        // Get the first element to be replaced.
        int childCount = childContainer.getChildCount();
        Element toReplace = null;
        if (start < childCount) {
            toReplace = childContainer.getChild(start).cast();
        }

        // Replace the elements.
        int count = newChildren.getChildCount();
        for (int i = 0; i < count; i++) {
            if (toReplace == null) {
                // The child will be removed from tmpElem, so always use index 0.
                childContainer.appendChild(newChildren.getChild(0));
            } else {
                Element nextSibling = toReplace.getNextSiblingElement();
                childContainer.replaceChild(newChildren.getChild(0), toReplace);
                toReplace = nextSibling;
            }
        }
    }

    /**
     * Return the temporary element used to create elements.
     */
    private static com.google.gwt.user.client.Element getTmpElem() {
        if (tmpElem == null) {
            tmpElem = Document.get().createDivElement().cast();
        }
        return tmpElem;
    }

    /**
     * A boolean indicating that the widget has focus.
     */
    boolean isFocused;

    private char accessKey = 0;

    /**
     * A boolean indicating that the widget is refreshing, so all events should be
     * ignored.
     */
    private boolean isRefreshing;

    private final HasDataPresenter<T> presenter;
    private HandlerRegistration keyboardSelectionReg;
    private HandlerRegistration selectionManagerReg;
    private int tabIndex;

    /**
     * Constructs an {@link AbstractHasData} with the given page size.
     * 
     * @param elem the parent {@link Element}
     * @param pageSize the page size
     * @param keyProvider the key provider, or null
     */
    public AbstractHasData(final Element elem, final int pageSize, final ProvidesKey<T> keyProvider) {
        this(new Widget() {
            {
                setElement(elem);
            }
        }, pageSize, keyProvider);
    }

    /**
     * Constructs an {@link AbstractHasData} with the given page size.
     * 
     * @param widget the parent {@link Widget}
     * @param pageSize the page size
     * @param keyProvider the key provider, or null
     */
    public AbstractHasData(Widget widget, final int pageSize, final ProvidesKey<T> keyProvider) {
        initWidget(widget);
        this.presenter = new HasDataPresenter<T>(this, new View<T>(this), pageSize, keyProvider);

        // Sink events.
        Set<String> eventTypes = new HashSet<String>();
        eventTypes.add("focus");
        eventTypes.add("blur");
        eventTypes.add("keydown"); // Used for keyboard navigation.
        eventTypes.add("keyup"); // Used by subclasses for selection.
        eventTypes.add("click"); // Used by subclasses for selection.
        eventTypes.add("mousedown"); // No longer used, but here for legacy support.
        CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);

        // Add a default selection event manager.
        selectionManagerReg = addCellPreviewHandler(DefaultSelectionEventManager.<T>createDefaultManager());

        // Add a default keyboard selection handler.
        setKeyboardSelectionHandler(new DefaultKeyboardSelectionHandler<T>(this));
    }

    @Override
    public HandlerRegistration addCellPreviewHandler(CellPreviewEvent.Handler<T> handler) {
        return presenter.addCellPreviewHandler(handler);
    }

    /**
     * Add a {@link LoadingStateChangeEvent.Handler} to be notified of changes in
     * the loading state.
     * 
     * @param handler the handle
     * @return the registration for the handler
     */
    public HandlerRegistration addLoadingStateChangeHandler(LoadingStateChangeEvent.Handler handler) {
        return presenter.addLoadingStateChangeHandler(handler);
    }

    @Override
    public HandlerRegistration addRangeChangeHandler(RangeChangeEvent.Handler handler) {
        return presenter.addRangeChangeHandler(handler);
    }

    @Override
    public HandlerRegistration addRowCountChangeHandler(RowCountChangeEvent.Handler handler) {
        return presenter.addRowCountChangeHandler(handler);
    }

    /**
     * Get the access key.
     * 
     * @return the access key, or -1 if not set
     * @see #setAccessKey(char)
     */
    public char getAccessKey() {
        return accessKey;
    }

    /**
     * Get the row value at the specified visible index. Index 0 corresponds to
     * the first item on the page.
     * 
     * @param indexOnPage the index on the page
     * @return the row value
     * @deprecated use {@link #getVisibleItem(int)} instead
     */
    @Deprecated
    public T getDisplayedItem(int indexOnPage) {
        return getVisibleItem(indexOnPage);
    }

    /**
     * Return the row values that the widget is currently displaying as an
     * immutable list.
     * 
     * @return a List of displayed items
     * @deprecated use {@link #getVisibleItems()} instead
     */
    @Deprecated
    public List<T> getDisplayedItems() {
        return getVisibleItems();
    }

    @Override
    public KeyboardPagingPolicy getKeyboardPagingPolicy() {
        return presenter.getKeyboardPagingPolicy();
    }

    /**
     * Get the index of the row that is currently selected via the keyboard,
     * relative to the page start index.
     * 
     * <p>
     * This is not same as the selected row in the {@link SelectionModel}. The
     * keyboard selected row refers to the row that the user navigated to via the
     * keyboard or mouse.
     * </p>
     * 
     * @return the currently selected row, or -1 if none selected
     */
    public int getKeyboardSelectedRow() {
        return presenter.getKeyboardSelectedRow();
    }

    @Override
    public KeyboardSelectionPolicy getKeyboardSelectionPolicy() {
        return presenter.getKeyboardSelectionPolicy();
    }

    @Override
    public ProvidesKey<T> getKeyProvider() {
        return presenter.getKeyProvider();
    }

    /**
     * Return the range size.
     * 
     * @return the size of the range as an int
     * 
     * @see #getVisibleRange()
     * @see #setPageSize(int)
     */
    public final int getPageSize() {
        return getVisibleRange().getLength();
    }

    /**
     * Return the range start.
     * 
     * @return the start of the range as an int
     * 
     * @see #getVisibleRange()
     * @see #setPageStart(int)
     */
    public final int getPageStart() {
        return getVisibleRange().getStart();
    }

    /**
     * Return the outer element that contains all of the rendered row values. This
     * method delegates to {@link #getChildContainer()};
     * 
     * @return the {@link Element} that contains the rendered row values
     */
    public Element getRowContainer() {
        presenter.flush();
        return getChildContainer();
    }

    @Override
    public int getRowCount() {
        return presenter.getRowCount();
    }

    @Override
    public SelectionModel<? super T> getSelectionModel() {
        return presenter.getSelectionModel();
    }

    @Override
    public int getTabIndex() {
        return tabIndex;
    }

    @Override
    public T getVisibleItem(int indexOnPage) {
        checkRowBounds(indexOnPage);
        return presenter.getVisibleItem(indexOnPage);
    }

    @Override
    public int getVisibleItemCount() {
        return presenter.getVisibleItemCount();
    }

    /**
     * Return the row values that the widget is currently displaying as an
     * immutable list.
     * 
     * @return a List of displayed items
     */
    @Override
    public List<T> getVisibleItems() {
        return presenter.getVisibleItems();
    }

    @Override
    public Range getVisibleRange() {
        return presenter.getVisibleRange();
    }

    @Override
    public boolean isRowCountExact() {
        return presenter.isRowCountExact();
    }

    /**
     * Handle browser events. Subclasses should override
     * {@link #onBrowserEvent2(Event)} if they want to extend browser event
     * handling.
     * 
     * @see #onBrowserEvent2(Event)
     */
    @Override
    public final void onBrowserEvent(Event event) {
        CellBasedWidgetImpl.get().onBrowserEvent(this, event);

        // Ignore spurious events (such as onblur) while we refresh the table.
        if (isRefreshing) {
            return;
        }

        // Verify that the target is still a child of this widget. IE fires focus
        // events even after the element has been removed from the DOM.
        EventTarget eventTarget = event.getEventTarget();
        if (!Element.is(eventTarget)) {
            return;
        }
        Element target = Element.as(eventTarget);
        if (!getElement().isOrHasChild(Element.as(eventTarget))) {
            return;
        }
        super.onBrowserEvent(event);

        String eventType = event.getType();
        if ("focus".equals(eventType)) {
            // Remember the focus state.
            isFocused = true;
            onFocus();
        } else if ("blur".equals(eventType)) {
            // Remember the blur state.
            isFocused = false;
            onBlur();
        } else if ("keydown".equals(eventType)) {
            // A key event indicates that we already have focus.
            isFocused = true;
        } else if ("mousedown".equals(eventType) && CellBasedWidgetImpl.get().isFocusable(Element.as(target))) {
            // If a natively focusable element was just clicked, then we must have
            // focus.
            isFocused = true;
        }

        // Let subclasses handle the event now.
        onBrowserEvent2(event);
    }

    /**
     * Redraw the widget using the existing data.
     */
    public void redraw() {
        presenter.redraw();
    }

    /**
     * Redraw a single row using the existing data.
     * 
     * @param absRowIndex the absolute row index to redraw
     */
    public void redrawRow(int absRowIndex) {
        int relRowIndex = absRowIndex - getPageStart();
        checkRowBounds(relRowIndex);
        setRowData(absRowIndex, Collections.singletonList(getVisibleItem(relRowIndex)));
    }

    /**
     * {@inheritDoc}
     * 
     * @see #getAccessKey()
     */
    @Override
    public void setAccessKey(char key) {
        this.accessKey = key;
        setKeyboardSelected(getKeyboardSelectedRow(), true, false);
    }

    @Override
    public void setFocus(boolean focused) {
        Element elem = getKeyboardSelectedElement();
        if (elem != null) {
            if (focused) {
                elem.focus();
            } else {
                elem.blur();
            }
        }
    }

    @Override
    public void setKeyboardPagingPolicy(KeyboardPagingPolicy policy) {
        presenter.setKeyboardPagingPolicy(policy);
    }

    /**
     * Set the keyboard selected row. The row index is the index relative to the
     * current page start index.
     * 
     * <p>
     * If keyboard selection is disabled, this method does nothing.
     * </p>
     * 
     * <p>
     * If the keyboard selected row is outside of the range of the current page
     * (that is, less than 0 or greater than or equal to the page size), the page
     * or range will be adjusted depending on the keyboard paging policy. If the
     * keyboard paging policy is limited to the current range, the row index will
     * be clipped to the current page.
     * </p>
     * 
     * @param row the row index relative to the page start
     */
    public final void setKeyboardSelectedRow(int row) {
        setKeyboardSelectedRow(row, true);
    }

    /**
     * Set the keyboard selected row and optionally focus on the new row.
     * 
     * @param row the row index relative to the page start
     * @param stealFocus true to focus on the new row
     * @see #setKeyboardSelectedRow(int)
     */
    public void setKeyboardSelectedRow(int row, boolean stealFocus) {
        presenter.setKeyboardSelectedRow(row, stealFocus, true);
    }

    /**
     * Set the handler that handles keyboard selection/navigation.
     */
    public void setKeyboardSelectionHandler(CellPreviewEvent.Handler<T> keyboardSelectionReg) {
        // Remove the old manager.
        if (this.keyboardSelectionReg != null) {
            this.keyboardSelectionReg.removeHandler();
            this.keyboardSelectionReg = null;
        }

        // Add the new manager.
        if (keyboardSelectionReg != null) {
            this.keyboardSelectionReg = addCellPreviewHandler(keyboardSelectionReg);
        }
    }

    @Override
    public void setKeyboardSelectionPolicy(KeyboardSelectionPolicy policy) {
        presenter.setKeyboardSelectionPolicy(policy);
    }

    /**
     * Set the number of rows per page and refresh the view.
     * 
     * @param pageSize the page size
     * @see #setVisibleRange(Range)
     * @see #getPageSize()
     */
    public final void setPageSize(int pageSize) {
        setVisibleRange(getPageStart(), pageSize);
    }

    /**
     * Set the starting index of the current visible page. The actual page start
     * will be clamped in the range [0, getSize() - 1].
     * 
     * @param pageStart the index of the row that should appear at the start of
     *          the page
     * @see #setVisibleRange(Range)
     * @see #getPageStart()
     */
    public final void setPageStart(int pageStart) {
        setVisibleRange(pageStart, getPageSize());
    }

    @Override
    public final void setRowCount(int count) {
        setRowCount(count, true);
    }

    @Override
    public void setRowCount(int size, boolean isExact) {
        presenter.setRowCount(size, isExact);
    }

    /**
     * <p>
     * Set the complete list of values to display on one page.
     * </p>
     * <p>
     * Equivalent to calling {@link #setRowCount(int)} with the length of the list
     * of values, {@link #setVisibleRange(Range)} from 0 to the size of the list
     * of values, and {@link #setRowData(int, List)} with a start of 0 and the
     * specified list of values.
     * </p>
     * 
     * @param values
     */
    public final void setRowData(List<? extends T> values) {
        setRowCount(values.size());
        setVisibleRange(0, values.size());
        setRowData(0, values);
    }

    @Override
    public void setRowData(int start, List<? extends T> values) {
        presenter.setRowData(start, values);
    }

    /**
     * Set the {@link SelectionModel} used by this {@link HasData}.
     * 
     * <p>
     * By default, selection occurs when the user clicks on a Cell or presses the
     * spacebar. If you need finer control over selection, you can specify a
     * {@link DefaultSelectionEventManager} using
     * {@link #setSelectionModel(SelectionModel, com.google.gwt.view.client.CellPreviewEvent.Handler)}. {@link DefaultSelectionEventManager} provides some default
     * implementations to handle checkbox based selection, as well as a blacklist
     * or whitelist of columns to prevent or allow selection.
     * </p>
     * 
     * @param selectionModel the {@link SelectionModel}
     * @see #setSelectionModel(SelectionModel,
     *      com.google.gwt.view.client.CellPreviewEvent.Handler)
     * @see #getSelectionModel()
     */
    @Override
    public void setSelectionModel(SelectionModel<? super T> selectionModel) {
        presenter.setSelectionModel(selectionModel);
    }

    /**
     * Set the {@link SelectionModel} that defines which items are selected and
     * the {@link com.google.gwt.view.client.CellPreviewEvent.Handler} that
     * controls how user selection is handled.
     * 
     * @param selectionModel the {@link SelectionModel} that defines selection
     * @param selectionEventManager the handler that controls user selection
     */
    public void setSelectionModel(SelectionModel<? super T> selectionModel,
            CellPreviewEvent.Handler<T> selectionEventManager) {
        // Remove the old manager.
        if (this.selectionManagerReg != null) {
            this.selectionManagerReg.removeHandler();
            this.selectionManagerReg = null;
        }

        // Add the new manager.
        if (selectionEventManager != null) {
            this.selectionManagerReg = addCellPreviewHandler(selectionEventManager);
        }

        // Set the selection model.
        setSelectionModel(selectionModel);
    }

    @Override
    public void setTabIndex(int index) {
        this.tabIndex = index;
        setKeyboardSelected(getKeyboardSelectedRow(), true, false);
    }

    @Override
    public final void setVisibleRange(int start, int length) {
        setVisibleRange(new Range(start, length));
    }

    @Override
    public void setVisibleRange(Range range) {
        presenter.setVisibleRange(range);
    }

    @Override
    public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) {
        presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
    }

    /**
     * Check if a cell consumes the specified event type.
     * 
     * @param cell the cell
     * @param eventType the event type to check
     * @return true if consumed, false if not
     */
    protected boolean cellConsumesEventType(Cell<?> cell, String eventType) {
        Set<String> consumedEvents = cell.getConsumedEvents();
        return consumedEvents != null && consumedEvents.contains(eventType);
    }

    /**
     * Check that the row is within the correct bounds.
     * 
     * @param row row index to check
     * @throws IndexOutOfBoundsException
     */
    protected void checkRowBounds(int row) {
        if (!isRowWithinBounds(row)) {
            throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: " + getRowCount());
        }
    }

    /**
     * Convert the specified HTML into DOM elements and return the parent of the
     * DOM elements.
     * 
     * @param html the HTML to convert
     * @return the parent element
     */
    protected Element convertToElements(SafeHtml html) {
        return convertToElements(this, getTmpElem(), html);
    }

    /**
     * Check whether or not the cells in the view depend on the selection state.
     * 
     * @return true if cells depend on selection, false if not
     */
    protected abstract boolean dependsOnSelection();

    /**
     * Return the element that holds the rendered cells.
     * 
     * @return the container {@link Element}
     */
    protected abstract Element getChildContainer();

    /**
     * Get the element that represents the specified index.
     * 
     * @param index the index of the row value
     * @return the the child element, or null if it does not exist
     */
    protected Element getChildElement(int index) {
        Element childContainer = getChildContainer();
        int childCount = childContainer.getChildCount();
        return (index < childCount) ? childContainer.getChild(index).<Element>cast() : null;
    }

    /**
     * Get the element that has keyboard selection.
     * 
     * @return the keyboard selected element
     */
    protected abstract Element getKeyboardSelectedElement();

    /**
     * Get the key for the specified value.
     * 
     * @param value the value
     * @return the key
     */
    protected Object getValueKey(T value) {
        ProvidesKey<T> keyProvider = getKeyProvider();
        return (keyProvider == null || value == null) ? value : keyProvider.getKey(value);
    }

    /**
     * Check if keyboard navigation is being suppressed, such as when the user is
     * editing a cell.
     * 
     * @return true if suppressed, false if not
     */
    protected abstract boolean isKeyboardNavigationSuppressed();

    /**
     * Checks that the row is within bounds of the view.
     * 
     * @param row row index to check
     * @return true if within bounds, false if not
     */
    protected boolean isRowWithinBounds(int row) {
        return row >= 0 && row < presenter.getVisibleItemCount();
    }

    /**
     * Called when the widget is blurred.
     */
    protected void onBlur() {
    }

    /**
     * Called after {@link #onBrowserEvent(Event)} completes.
     * 
     * @param event the event that was fired
     */
    protected void onBrowserEvent2(Event event) {
    }

    /**
     * Called when the widget is focused.
     */
    protected void onFocus() {
    }

    /**
     * Called when the loading state changes. By default, this implementation
     * fires a {@link LoadingStateChangeEvent}.
     * 
     * @param state the new loading state
     */
    protected void onLoadingStateChanged(LoadingState state) {
        fireEvent(new LoadingStateChangeEvent(state));
    }

    @Override
    protected void onUnload() {
        isFocused = false;
        super.onUnload();
    }

    /**
     * Render all row values into the specified {@link SafeHtmlBuilder}.
     * 
     * <p>
     * Subclasses can optionally throw an {@link UnsupportedOperationException} if
     * they prefer to render the rows in
     * {@link #replaceAllChildren(List, SafeHtml)} and
     * {@link #replaceChildren(List, int, SafeHtml)}. In this case, the
     * {@link SafeHtml} argument will be null. Though a bit hacky, this is
     * designed to supported legacy widgets that use {@link SafeHtmlBuilder}, and
     * newer widgets that use other builders, such as the ElementBuilder API.
     * </p>
     * 
     * @param sb the {@link SafeHtmlBuilder} to render into
     * @param values the row values
     * @param start the absolute start index of the values
     * @param selectionModel the {@link SelectionModel}
     * @throws UnsupportedOperationException if the values will be rendered in
     *           {@link #replaceAllChildren(List, SafeHtml)} and
     *           {@link #replaceChildren(List, int, SafeHtml)}
     */
    protected abstract void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
            SelectionModel<? super T> selectionModel) throws UnsupportedOperationException;

    /**
     * Replace all children with the specified html.
     * 
     * @param values the values of the new children
     * @param html the html to render, or null if
     *          {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
     *          throws an {@link UnsupportedOperationException}
     */
    protected void replaceAllChildren(List<T> values, SafeHtml html) {
        replaceAllChildren(this, getChildContainer(), html);
    }

    /**
     * Convert the specified HTML into DOM elements and replace the existing
     * elements starting at the specified index. If the number of children
     * specified exceeds the existing number of children, the remaining children
     * should be appended.
     * 
     * @param values the values of the new children
     * @param start the start index to be replaced, relative to the page start
     * @param html the html to render, or null if
     *          {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
     *          throws an {@link UnsupportedOperationException}
     */
    protected void replaceChildren(List<T> values, int start, SafeHtml html) {
        Element newChildren = convertToElements(html);
        replaceChildren(this, getChildContainer(), newChildren, start, html);
    }

    /**
     * Reset focus on the currently focused cell.
     * 
     * @return true if focus is taken, false if not
     */
    protected abstract boolean resetFocusOnCell();

    /**
     * Make an element focusable or not.
     * 
     * @param elem the element
     * @param focusable true to make focusable, false to make unfocusable
     */
    protected void setFocusable(Element elem, boolean focusable) {
        if (focusable) {
            FocusImpl focusImpl = FocusImpl.getFocusImplForWidget();
            com.google.gwt.user.client.Element rowElem = elem.cast();
            focusImpl.setTabIndex(rowElem, getTabIndex());
            if (accessKey != 0) {
                focusImpl.setAccessKey(rowElem, accessKey);
            }
        } else {
            // Chrome: Elements remain focusable after removing the tabIndex, so set
            // it to -1 first.
            elem.setTabIndex(-1);
            elem.removeAttribute("tabIndex");
            elem.removeAttribute("accessKey");
        }
    }

    /**
     * Update an element to reflect its keyboard selected state.
     * 
     * @param index the index of the element
     * @param selected true if selected, false if not
     * @param stealFocus true if the row should steal focus, false if not
     */
    protected abstract void setKeyboardSelected(int index, boolean selected, boolean stealFocus);

    /**
     * Update an element to reflect its selected state.
     * 
     * @param elem the element to update
     * @param selected true if selected, false if not
     * @deprecated this method is never called by AbstractHasData, render the
     *             selected styles in
     *             {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
     */
    @Deprecated
    protected void setSelected(Element elem, boolean selected) {
        // Never called.
    }

    /**
     * Add a {@link ValueChangeHandler} that is called when the display values
     * change. Used by {@link CellBrowser} to detect when the displayed data
     * changes.
     * 
     * @param handler the handler
     * @return a {@link HandlerRegistration} to remove the handler
     */
    final HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<T>> handler) {
        return addHandler(handler, ValueChangeEvent.getType());
    }

    /**
     * Adopt the specified widget.
     * 
     * @param child the child to adopt
     */
    native void adopt(Widget child) /*-{
                                    child.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(this);
                                    }-*/;

    /**
     * Attach a child.
     * 
     * @param child the child to attach
     */
    native void doAttach(Widget child) /*-{
                                       child.@com.google.gwt.user.client.ui.Widget::onAttach()();
                                       }-*/;

    /**
     * Detach a child.
     * 
     * @param child the child to detach
     */
    native void doDetach(Widget child) /*-{
                                       child.@com.google.gwt.user.client.ui.Widget::onDetach()();
                                       }-*/;

    HasDataPresenter<T> getPresenter() {
        return presenter;
    }

    /**
     * Show or hide an element.
     * 
     * @param element the element
     * @param show true to show, false to hide
     */
    void showOrHide(Element element, boolean show) {
        if (element == null) {
            return;
        }
        if (show) {
            element.getStyle().clearDisplay();
        } else {
            element.getStyle().setDisplay(Display.NONE);
        }
    }
}