blob: 2caf563c3fd7ffb9bbf6ec07adeeee395c198849 [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.cell.client.Cell.Context;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.DivElement;
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.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AttachDetachException;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionModel;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* A single column list of cells.
*
* <p>
* <h3>Examples</h3>
* <dl>
* <dt>Trivial example</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellListExample}</dd>
* <dt>Handling user input with ValueUpdater</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellListValueUpdaterExample}</dd>
* <dt>Pushing data with List Data Provider (backed by {@link List})</dt>
* <dd>{@example com.google.gwt.examples.view.ListDataProviderExample}</dd>
* <dt>Pushing data asynchronously with Async Data Provider</dt>
* <dd>{@example com.google.gwt.examples.view.AsyncDataProviderExample}</dd>
* <dt>Writing a custom data provider</dt>
* <dd>{@example com.google.gwt.examples.view.RangeChangeHandlerExample}</dd>
* <dt>Using a key provider to track objects as they change</dt>
* <dd>{@example com.google.gwt.examples.view.KeyProviderExample}</dd>
* </dl>
* </p>
*
* @param <T> the data type of list items
*/
public class CellList<T> extends AbstractHasData<T> {
/**
* A ClientBundle that provides images for this widget.
*/
public interface Resources extends ClientBundle {
/**
* The background used for selected items.
*/
@ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
ImageResource cellListSelectedBackground();
/**
* The styles used in this widget.
*/
@Source(Style.DEFAULT_CSS)
Style cellListStyle();
}
/**
* Styles used by this widget.
*/
@ImportedWithPrefix("gwt-CellList")
public interface Style extends CssResource {
/**
* The path to the default CSS styles used by this resource.
*/
String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellList.css";
/**
* Applied to even items.
*/
String cellListEvenItem();
/**
* Applied to the keyboard selected item.
*/
String cellListKeyboardSelectedItem();
/**
* Applied to odd items.
*/
String cellListOddItem();
/**
* Applied to selected items.
*/
String cellListSelectedItem();
/**
* Applied to the widget.
*/
String cellListWidget();
}
interface Template extends SafeHtmlTemplates {
@Template("<div __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" >{2}</div>")
SafeHtml div(int idx, String classes, SafeHtml cellContents);
}
/**
* The default page size.
*/
private static final int DEFAULT_PAGE_SIZE = 25;
private static Resources DEFAULT_RESOURCES;
private static final Template TEMPLATE = GWT.create(Template.class);
private static Resources getDefaultResources() {
if (DEFAULT_RESOURCES == null) {
DEFAULT_RESOURCES = GWT.create(Resources.class);
}
return DEFAULT_RESOURCES;
}
private final Cell<T> cell;
private boolean cellIsEditing;
private final Element childContainer;
private SafeHtml emptyListMessage = SafeHtmlUtils.fromSafeConstant("");
private final SimplePanel emptyListWidgetContainer = new SimplePanel();
private final SimplePanel loadingIndicatorContainer = new SimplePanel();
/**
* A {@link DeckPanel} to hold widgets associated with various loading states.
*/
private final DeckPanel messagesPanel = new DeckPanel();
private final Style style;
private ValueUpdater<T> valueUpdater;
/**
* Construct a new {@link CellList}.
*
* @param cell the cell used to render each item
*/
public CellList(final Cell<T> cell) {
this(cell, getDefaultResources(), null);
}
/**
* Construct a new {@link CellList} with the specified {@link Resources}.
*
* @param cell the cell used to render each item
* @param resources the resources used for this widget
*/
public CellList(final Cell<T> cell, Resources resources) {
this(cell, resources, null);
}
/**
* Construct a new {@link CellList} with the specified {@link ProvidesKey key
* provider}.
*
* @param cell the cell used to render each item
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public CellList(final Cell<T> cell, ProvidesKey<T> keyProvider) {
this(cell, getDefaultResources(), keyProvider);
}
/**
* Construct a new {@link CellList} with the specified {@link Resources} and
* {@link ProvidesKey key provider}.
*
* @param cell the cell used to render each item
* @param resources the resources used for this widget
* @param keyProvider an instance of ProvidesKey<T>, or null if the record
* object should act as its own key
*/
public CellList(final Cell<T> cell, Resources resources, ProvidesKey<T> keyProvider) {
super(Document.get().createDivElement(), DEFAULT_PAGE_SIZE, keyProvider);
this.cell = cell;
this.style = resources.cellListStyle();
this.style.ensureInjected();
String widgetStyle = this.style.cellListWidget();
if (widgetStyle != null) {
// The widget style is null when used in CellBrowser.
addStyleName(widgetStyle);
}
// Add the child container.
childContainer = Document.get().createDivElement();
DivElement outerDiv = getElement().cast();
outerDiv.appendChild(childContainer);
// Attach the message panel.
outerDiv.appendChild(messagesPanel.getElement());
adopt(messagesPanel);
messagesPanel.add(emptyListWidgetContainer);
messagesPanel.add(loadingIndicatorContainer);
// Sink events that the cell consumes.
CellBasedWidgetImpl.get().sinkEvents(this, cell.getConsumedEvents());
}
/**
* Get the message that is displayed when there is no data.
*
* @return the empty message
* @see #setEmptyListMessage(SafeHtml)
* @deprecated as of GWT 2.3, use {@link #getEmptyListWidget()} instead
*/
@Deprecated
public SafeHtml getEmptyListMessage() {
return emptyListMessage;
}
/**
* Get the widget displayed when the list has no rows.
*
* @return the empty list widget
*/
public Widget getEmptyListWidget() {
return emptyListWidgetContainer.getWidget();
}
/**
* Get the widget displayed when the data is loading.
*
* @return the loading indicator
*/
public Widget getLoadingIndicator() {
return loadingIndicatorContainer.getWidget();
}
/**
* Get the {@link Element} for the specified index. If the element has not
* been created, null is returned.
*
* @param indexOnPage the index on the page
* @return the element, or null if it doesn't exists
* @throws IndexOutOfBoundsException if the index is outside of the current
* page
*/
public Element getRowElement(int indexOnPage) {
getPresenter().flush();
checkRowBounds(indexOnPage);
if (childContainer.getChildCount() > indexOnPage) {
return childContainer.getChild(indexOnPage).cast();
}
return null;
}
/**
* Set the message to display when there is no data.
*
* @param html the message to display when there are no results
* @see #getEmptyListMessage()
* @deprecated as of GWT 2.3, use
* {@link #setEmptyListWidget(com.google.gwt.user.client.ui.Widget)}
* instead
*/
@Deprecated
public void setEmptyListMessage(SafeHtml html) {
this.emptyListMessage = html;
setEmptyListWidget(html == null ? null : new HTML(html));
}
/**
* Set the widget to display when the list has no rows.
*
* @param widget the empty data widget
*/
public void setEmptyListWidget(Widget widget) {
emptyListWidgetContainer.setWidget(widget);
}
/**
* Set the widget to display when the data is loading.
*
* @param widget the loading indicator
*/
public void setLoadingIndicator(Widget widget) {
loadingIndicatorContainer.setWidget(widget);
}
/**
* Set the value updater to use when cells modify items.
*
* @param valueUpdater the {@link ValueUpdater}
*/
public void setValueUpdater(ValueUpdater<T> valueUpdater) {
this.valueUpdater = valueUpdater;
}
@Override
protected boolean dependsOnSelection() {
return cell.dependsOnSelection();
}
@Override
protected void doAttachChildren() {
try {
doAttach(messagesPanel);
} catch (Throwable e) {
throw new AttachDetachException(Collections.singleton(e));
}
}
@Override
protected void doDetachChildren() {
try {
doDetach(messagesPanel);
} catch (Throwable e) {
throw new AttachDetachException(Collections.singleton(e));
}
}
/**
* Fire an event to the cell.
*
* @param context the {@link Context} of the cell
* @param event the event that was fired
* @param parent the parent of the cell
* @param value the value of the cell
*/
protected void fireEventToCell(Context context, Event event, Element parent, T value) {
Set<String> consumedEvents = cell.getConsumedEvents();
if (consumedEvents != null && consumedEvents.contains(event.getType())) {
boolean cellWasEditing = cell.isEditing(context, parent, value);
cell.onBrowserEvent(context, parent, value, event, valueUpdater);
cellIsEditing = cell.isEditing(context, parent, value);
if (cellWasEditing && !cellIsEditing) {
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
setFocus(true);
}
});
}
}
}
/**
* Return the cell used to render each item.
*/
protected Cell<T> getCell() {
return cell;
}
/**
* Get the parent element that wraps the cell from the list item. Override
* this method if you add structure to the element.
*
* @param item the row element that wraps the list item
* @return the parent element of the cell
*/
protected Element getCellParent(Element item) {
return item;
}
@Override
protected Element getChildContainer() {
return childContainer;
}
@Override
protected Element getKeyboardSelectedElement() {
// Do not use getRowElement() because that will flush the presenter.
int rowIndex = getKeyboardSelectedRow();
if (rowIndex >= 0 && childContainer.getChildCount() > rowIndex) {
return childContainer.getChild(rowIndex).cast();
}
return null;
}
@Override
protected boolean isKeyboardNavigationSuppressed() {
return cellIsEditing;
}
@SuppressWarnings("deprecation")
@Override
protected void onBrowserEvent2(Event event) {
// Get the event target.
EventTarget eventTarget = event.getEventTarget();
if (!Element.is(eventTarget)) {
return;
}
final Element target = event.getEventTarget().cast();
// Forward the event to the cell.
String idxString = "";
Element cellTarget = target;
while ((cellTarget != null) && ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
cellTarget = cellTarget.getParentElement();
}
if (idxString.length() > 0) {
// Select the item if the cell does not consume events. Selection occurs
// before firing the event to the cell in case the cell operates on the
// currently selected item.
String eventType = event.getType();
boolean isClick = BrowserEvents.CLICK.equals(eventType);
int idx = Integer.parseInt(idxString);
int indexOnPage = idx - getPageStart();
if (!isRowWithinBounds(indexOnPage)) {
// If the event causes us to page, then the index will be out of bounds.
return;
}
// Get the cell parent before doing selection in case the list is redrawn.
boolean isSelectionHandled =
cell.handlesSelection()
|| KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
Element cellParent = getCellParent(cellTarget);
T value = getVisibleItem(indexOnPage);
Context context = new Context(idx, 0, getValueKey(value));
CellPreviewEvent<T> previewEvent =
CellPreviewEvent.fire(this, event, this, context, value, cellIsEditing,
isSelectionHandled);
// Fire the event to the cell if the list has not been refreshed.
if (!previewEvent.isCanceled()) {
fireEventToCell(context, event, cellParent, value);
}
}
}
/**
* Called when the loading state changes.
*
* @param state the new loading state
*/
@Override
protected void onLoadingStateChanged(LoadingState state) {
Widget message = null;
if (state == LoadingState.LOADING) {
// Loading indicator.
message = loadingIndicatorContainer;
} else if (state == LoadingState.LOADED && getPresenter().isEmpty()) {
// Empty table.
message = emptyListWidgetContainer;
}
// Switch out the message to display.
if (message != null) {
messagesPanel.showWidget(messagesPanel.getWidgetIndex(message));
}
// Show the correct container.
showOrHide(getChildContainer(), message == null);
messagesPanel.setVisible(message != null);
// Fire an event.
super.onLoadingStateChanged(state);
}
@Override
protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
SelectionModel<? super T> selectionModel) {
String keyboardSelectedItem = " " + style.cellListKeyboardSelectedItem();
String selectedItem = " " + style.cellListSelectedItem();
String evenItem = style.cellListEvenItem();
String oddItem = style.cellListOddItem();
int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
int length = values.size();
int end = start + length;
for (int i = start; i < end; i++) {
T value = values.get(i - start);
boolean isSelected = selectionModel == null ? false : selectionModel.isSelected(value);
StringBuilder classesBuilder = new StringBuilder();
classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
if (isSelected) {
classesBuilder.append(selectedItem);
}
if (i == keyboardSelectedRow) {
classesBuilder.append(keyboardSelectedItem);
}
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
Context context = new Context(i, 0, getValueKey(value));
cell.render(context, value, cellBuilder);
sb.append(TEMPLATE.div(i, classesBuilder.toString(), cellBuilder.toSafeHtml()));
}
}
@Override
protected boolean resetFocusOnCell() {
int row = getKeyboardSelectedRow();
if (isRowWithinBounds(row)) {
Element rowElem = getKeyboardSelectedElement();
Element cellParent = getCellParent(rowElem);
T value = getVisibleItem(row);
Context context = new Context(row + getPageStart(), 0, getValueKey(value));
return cell.resetFocus(context, cellParent, value);
}
return false;
}
@Override
protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
if (!isRowWithinBounds(index)) {
return;
}
Element elem = getRowElement(index);
if (!selected || isFocused || stealFocus) {
setStyleName(elem, style.cellListKeyboardSelectedItem(), selected);
}
setFocusable(elem, selected);
if (selected && stealFocus && !cellIsEditing) {
elem.focus();
onFocus();
}
}
/**
* @deprecated this method is never called by AbstractHasData, render the
* selected styles in
* {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
*/
@Override
@Deprecated
protected void setSelected(Element elem, boolean selected) {
setStyleName(elem, style.cellListSelectedItem(), selected);
}
}