blob: fe84f8f76c77425c9e6db262082b07e6f168931d [file] [log] [blame]
/*
* 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.BrowserEvents;
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 (BrowserEvents.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 (BrowserEvents.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 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 (BrowserEvents.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.setInnerSafeHtml(html);
// 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.setInnerSafeHtml(CellBasedWidgetImpl.get().processHtml(html));
// 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(BrowserEvents.FOCUS);
eventTypes.add(BrowserEvents.BLUR);
eventTypes.add(BrowserEvents.KEYDOWN); // Used for keyboard navigation.
eventTypes.add(BrowserEvents.KEYUP); // Used by subclasses for selection.
eventTypes.add(BrowserEvents.CLICK); // Used by subclasses for selection.
eventTypes.add(BrowserEvents.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;
}
/**
* Get the key for the specified value. If a keyProvider is not specified or the value is null,
* the value is returned. If the key provider is specified, it is used to get the key from
* the value.
*
* @param value the value
* @return the key
*/
public Object getValueKey(T value) {
ProvidesKey<T> keyProvider = getKeyProvider();
return (keyProvider == null || value == null) ? value : keyProvider.getKey(value);
}
@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 (BrowserEvents.FOCUS.equals(eventType)) {
// Remember the focus state.
isFocused = true;
onFocus();
} else if (BrowserEvents.BLUR.equals(eventType)) {
// Remember the blur state.
isFocused = false;
onBlur();
} else if (BrowserEvents.KEYDOWN.equals(eventType)) {
// A key event indicates that we already have focus.
isFocused = true;
} else if (BrowserEvents.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();
/**
* 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);
}
}
}