/*
* Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.ui.client.widget;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.enterprise.context.Dependent;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.ui.ValueChangeManager;
import org.jboss.errai.common.client.util.CreationalCallback;
import org.jboss.errai.databinding.client.BindableListWrapper;
import org.jboss.errai.databinding.client.api.handler.list.BindableListChangeHandler;
import org.jboss.errai.databinding.client.components.ListComponent;
import org.jboss.errai.ioc.client.container.IOC;
import org.jboss.errai.ioc.client.container.SyncToAsyncBeanManagerAdapter;
import org.jboss.errai.ioc.client.container.async.AsyncBeanDef;
import org.jboss.errai.ioc.client.container.async.AsyncBeanManager;
import org.jboss.errai.ui.client.local.spi.InvalidBeanScopeException;
import org.jboss.errai.ui.shared.TemplateWidget;
import org.jboss.errai.ui.shared.TemplateWidgetMapper;
import org.jboss.errai.ui.shared.api.annotations.Templated;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.InsertPanel;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
/**
* A type of widget that displays and manages a child component for each item in a
* list of model objects. The widget instances are managed by Errai's IOC
* container and are arranged in a {@link ComplexPanel}. By default, a
* {@link FlowPanel} is used, but an alternative can be specified using
* {@link #ListWidget(ComplexPanel)}.
*
* @param <M>
* the model type
* @param <C>
* the item component type, needs to implement {@link HasModel} for
* associating the widget instance with the corresponding model
* instance. This component must be either a {@link Widget} or
* a {@link Templated} bean.
* @deprecated Replaced by {@link ListComponent}.
*
* @author Christian Sadilek <csadilek@redhat.com>
*/
@Deprecated
public abstract class ListWidget<M, C extends HasModel<M>> extends Composite
implements HasValue<List<M>>, BindableListChangeHandler<M> {
private final ComplexPanel panel;
private BindableListWrapper<M> items;
private final List<BindableListChangeHandler<M>> handlers = new ArrayList<>();
private final Collection<HandlerRegistration> registrations = new ArrayList<>();
private final List<ComponentCreationalCallback> callbacks = new LinkedList<>();
private int pendingCallbacks;
private final ValueChangeManager<List<M>, ListWidget<M, C>> valueChangeManager = new ValueChangeManager<>(this);
protected ListWidget() {
this(new FlowPanel());
}
protected ListWidget(ComplexPanel panel) {
this.panel = Assert.notNull(panel);
handlers.add(this);
initWidget(panel);
}
/**
* Returns the class object for the item component type {@code C} to look up new
* instances of the widget using the client-side bean manager.
*
* @return the item widget type.
*/
protected abstract Class<C> getItemComponentType();
/**
* Called after all item components have been rendered. By default, this is a
* NOOP, but subclasses can add behaviour if needed.
* <p>
* Using the standard synchronous bean manager this method is invoked before
* {@link #setItems(List)} returns. However, when using the asynchronous bean
* manager and declaring @LoadAsync on the item component, this method might be
* called after {@link #setItems(List)} returns and after the corresponding
* JavaScript code has been downloaded.
*
* @param items
* the rendered item list. Every change to this list will update the
* corresponding rendered item components.
*/
protected void onItemsRendered(List<M> items) {
}
/**
* Returns the panel that contains all item widgets.
*
* @return the item widget panel, never null.
*/
protected ComplexPanel getPanel() {
return panel;
}
/**
* Sets the list of model objects. A component instance of type {@code C} will be added
* to the panel for each object in the list. The list will be wrapped in an
* {@link BindableListWrapper} to make direct changes to the list observable.
* <p>
* If the standard synchronous bean manager is used it is guaranteed that all
* components have been added to the panel when this method returns. In case the
* asynchronous bean manager is used this method might return before the
* widgets have been added to the panel. See {@link #onItemsRendered(List)}.
*
* @param items
* The list of model objects. If null or empty all existing child
* components will be removed.
*/
public void setItems(final List<M> items) {
final boolean changed = this.items != items;
if (items instanceof BindableListWrapper) {
this.items = (BindableListWrapper<M>) items;
}
else {
if (items != null) {
this.items = new BindableListWrapper<M>(items);
}
else {
this.items = new BindableListWrapper<M>(new ArrayList<M>());
}
}
if (changed) {
initializeHandlers();
init();
}
}
private void initializeHandlers() {
for (final HandlerRegistration reg : registrations) {
reg.removeHandler();
}
registrations.clear();
for (final BindableListChangeHandler<M> handler : handlers) {
registrations.add(this.items.addChangeHandler(handler));
}
}
private void init() {
// The AsyncBeanManager API works in both synchronous and asynchronous IOC mode
AsyncBeanManager bm = IOC.getAsyncBeanManager();
// In the case that this method is executed before the first call has
// successfully processed all of its callbacks, we must cancel those
// uncompleted callbacks in flight to prevent duplicate data in the
// ListWidget.
for (ComponentCreationalCallback callback : callbacks) {
callback.discard();
}
callbacks.clear();
pendingCallbacks = 0;
// clean up the old widgets before we add new ones (this will eventually
// become a feature of the framework: ERRAI-375)
Iterator<Widget> it = panel.iterator();
while (it.hasNext()) {
bm.destroyBean(getComponentFromWidget(it.next()));
it.remove();
}
if (items == null)
return;
pendingCallbacks = items.size();
AsyncBeanDef<C> itemBeanDef = bm.lookupBean(getItemComponentType());
if (!itemBeanDef.getScope().equals(Dependent.class))
throw new InvalidBeanScopeException("ListWidget cannot contain ApplicationScoped widgets");
for (final M item : items) {
final ComponentCreationalCallback callback = new ComponentCreationalCallback(item);
callbacks.add(callback);
itemBeanDef.getInstance(callback);
}
}
/**
* Returns the component at the specified index.
*
* @param index
* the index to be retrieved
*
* @return the widget at the specified index
*
* @throws IndexOutOfBoundsException
* if the index is out of range
*/
public C getComponent(int index) {
final C component = getComponentFromWidget(panel.getWidget(index));
return component;
}
/**
* Returns the component currently displaying the provided model.
*
* @param model
* the model displayed by the widget
*
* @return the widget displaying the provided model instance, null if no
* widget was found for the model.
*/
public C getComponent(M model) {
int index = items.indexOf(model);
return getComponent(index);
}
/**
* Returns the number of components currently being displayed.
*
* @return the number of widgets.
*/
public int getComponentCount() {
return getPanel().getWidgetCount();
}
@SuppressWarnings("unchecked")
private C getComponentFromWidget(final Widget widget) {
if (widget instanceof TemplateWidget) {
return (C) TemplateWidgetMapper.reverseGet((TemplateWidget) widget);
} else {
return (C) widget;
}
}
public HandlerRegistration addBindableListChangeHandler(final BindableListChangeHandler<M> handler) {
ensureItemsInitialized();
handlers.add(handler);
final HandlerRegistration wrapperHandlerRegistration = items.addChangeHandler(handler);
return new HandlerRegistration() {
@Override
public void removeHandler() {
ensureItemsInitialized();
handlers.remove(handler);
wrapperHandlerRegistration.removeHandler();
}
};
}
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<M>> handler) {
return valueChangeManager.addValueChangeHandler(handler);
}
@Override
public List<M> getValue() {
ensureItemsInitialized();
return items;
}
private void ensureItemsInitialized() {
if (items == null) {
items = new BindableListWrapper<M>(new ArrayList<M>());
initializeHandlers();
}
}
@Override
public void setValue(List<M> value) {
setValue(value, false);
}
@Override
public void setValue(List<M> value, boolean fireEvents) {
List<M> oldValue = getValue();
setItems(value);
if (fireEvents) {
ValueChangeEvent.fireIfNotEqual(this, oldValue, value);
}
}
/**
* A callback invoked by the {@link AsyncBeanManager} or
* {@link SyncToAsyncBeanManagerAdapter} when the component instance was created.
* It will associate the corresponding model instance with the component and add
* the component to the panel.
*/
private class ComponentCreationalCallback implements CreationalCallback<C> {
private boolean discard;
private final M item;
private ComponentCreationalCallback(M item) {
this.item = item;
}
@Override
public void callback(C component) {
if (!discard) {
component.setModel(item);
final IsWidget widget;
widget = getWidgetForComponent(component);
panel.add(widget);
if (--pendingCallbacks == 0) {
onItemsRendered(items);
}
}
}
public void discard() {
this.discard = true;
}
}
private IsWidget getWidgetForComponent(final C component) {
final IsWidget widget;
if (component instanceof IsWidget) {
widget = (IsWidget) component;
}
else if (TemplateWidgetMapper.containsKey(component)) {
widget = TemplateWidgetMapper.get(component);
}
else {
throw new RuntimeException("Cannot display component of type " + getItemComponentType().getName()
+ ". Must be a Widget, a native element, or @Templated.");
}
return widget;
}
@Override
public void onItemAdded(List<M> oldList, M item) {
addWidget(item);
}
@Override
public void onItemAddedAt(List<M> oldList, int index, M item) {
if (panel instanceof InsertPanel) {
addWidgetAt(index, items.get(index));
}
else {
for (int i = index; i < items.size(); i++) {
addAndReplaceWidget(index, i);
}
}
}
@Override
public void onItemsAdded(List<M> oldList, Collection<? extends M> items) {
for (M m : items) {
addWidget(m);
}
}
@Override
public void onItemsAddedAt(List<M> oldList, int index, Collection<? extends M> item) {
if (panel instanceof InsertPanel.ForIsWidget) {
for (int i = index; i < index + item.size(); i++) {
addWidgetAt(i, items.get(i));
}
}
else {
for (int i = index; i < items.size(); i++) {
addAndReplaceWidget(index, i);
}
}
}
@Override
public void onItemsCleared(List<M> oldList) {
AsyncBeanManager bm = IOC.getAsyncBeanManager();
Integer widgetCount = panel.getWidgetCount();
Collection<Widget> widgets = new ArrayList<Widget>(widgetCount);
for (int i = 0; i < widgetCount; i++) {
widgets.add(panel.getWidget(i));
}
panel.clear();
Iterator<Widget> itr = widgets.iterator();
while (itr.hasNext()) {
Widget w = itr.next();
bm.destroyBean(getComponentFromWidget(w));
}
}
@Override
public void onItemRemovedAt(List<M> oldList, int index) {
Widget widget = panel.getWidget(index);
panel.remove(index);
IOC.getAsyncBeanManager().destroyBean(getComponentFromWidget(widget));
}
@Override
public void onItemsRemovedAt(List<M> oldList, List<Integer> indexes) {
for (Integer index : indexes) {
Widget widget = panel.getWidget(index);
panel.remove(index);
IOC.getAsyncBeanManager().destroyBean(getComponentFromWidget(widget));
}
}
@Override
public void onItemChanged(List<M> oldList, int index, M item) {
if (oldList.get(index) == item)
return;
for (int i = index; i < items.size(); i++) {
addAndReplaceWidget(index, i);
}
}
private void addAndReplaceWidget(final int startIndex, final int index) {
if (index < panel.getWidgetCount()) {
panel.remove(startIndex);
}
addWidget(items.get(index));
}
private void addWidget(final M m) {
AsyncBeanDef<C> itemBeanDef = IOC.getAsyncBeanManager().lookupBean(getItemComponentType());
itemBeanDef.getInstance(new CreationalCallback<C>() {
@Override
public void callback(C component) {
component.setModel(m);
panel.add(getWidgetForComponent(component));
}
});
}
private void addWidgetAt(final int index, final M m) {
if (!(panel instanceof InsertPanel.ForIsWidget)) {
throw new RuntimeException("Method only supported for panels that implement: "
+ InsertPanel.ForIsWidget.class.getName());
}
AsyncBeanDef<C> itemBeanDef = IOC.getAsyncBeanManager().lookupBean(getItemComponentType());
itemBeanDef.getInstance(new CreationalCallback<C>() {
@Override
public void callback(C component) {
component.setModel(m);
((InsertPanel.ForIsWidget) panel).insert(getWidgetForComponent(component), index);
}
});
}
}