package org.ovirt.engine.ui.common.widget;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.gwtbootstrap3.client.ui.Button;
import org.gwtbootstrap3.client.ui.constants.IconType;
import org.ovirt.engine.core.common.utils.Pair;
import org.ovirt.engine.ui.common.view.popup.FocusableComponentsContainer;
import org.ovirt.engine.ui.common.widget.uicommon.popup.AbstractModelBoundPopupWidget;
import org.ovirt.engine.ui.uicommonweb.HasCleanup;
import org.ovirt.engine.ui.uicommonweb.models.ListModel;
import org.ovirt.engine.ui.uicompat.EventArgs;
import org.ovirt.engine.ui.uicompat.IEventListener;
import org.ovirt.engine.ui.uicompat.PropertyChangedEventArgs;
import com.google.gwt.dom.client.Style.Clear;
import com.google.gwt.dom.client.Style.Float;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HasEnabled;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
/**
* This model-backed widget may be used to display a list of values of type T, where each value is display using a
* widget of type V. Existing values may be removed by pressing minus-signed buttons located next to them, while new
* values may be added by pressing a plus-signed button located next to a special "ghost" entry. This special entry is
* half-disabled, and only becomes enabled when its value is set to a valid one; it will be overlooked when flushing.
*
* @param <M>
* the model backing this widget.
* @param <T>
* the type of the values contained in the backing model.
* @param <V>
* the type of widget used to display each value.
*/
public abstract class AddRemoveRowWidget<M extends ListModel<T>, T, V extends Widget & HasValueChangeHandlers<T> & HasCleanup>
extends AbstractModelBoundPopupWidget<M> implements HasEnabled {
public interface WidgetStyle extends CssResource {
String buttonStyle();
}
@UiField
public FlowPanel contentPanel;
@UiField
public WidgetStyle style;
protected final List<Pair<T, V>> items;
private final IEventListener<EventArgs> itemsChangedListener;
private final IEventListener<PropertyChangedEventArgs> propertyChangedListener;
private M model;
private Collection<T> modelItems;
private boolean enabled;
protected boolean showGhost = true;
protected boolean showAddButton = true;
protected boolean usePatternFly = false;
public AddRemoveRowWidget() {
items = new LinkedList<>();
itemsChangedListener = (ev, sender, args) -> init(model);
propertyChangedListener = (ev, sender, args) -> {
if ("IsChangable".equals(args.propertyName)) { //$NON-NLS-1$
setEnabled(model.getIsChangable());
updateEnabled();
}
};
}
/**
* This method initializes the entries of the widget, by creating an entry for each value in the backing model and
* an additional "ghost" entry. It is called whenever this widget is edited or an ItemsChangedEvent is raised from
* the backing model.
*
* @param model
* the model backing this widget.
*/
protected void init(M model) {
items.clear();
cleanContentPanel();
cleanupModelItems();
modelItems = model.getItems();
if (modelItems == null) {
modelItems = new LinkedList<>();
model.setItems(modelItems); // this will invoke init() again with the empty list as values instead of null
return;
}
if (modelItems.isEmpty() && showGhost) {
T ghostValue = addGhostEntry().getFirst();
modelItems.add(ghostValue);
} else {
Iterator<T> i = modelItems.iterator();
while (i.hasNext()) {
T value = i.next();
addEntry(value, !i.hasNext());
}
}
}
protected void cleanupModelItems() {
if (modelItems != null) {
for (T item : modelItems) {
if (item instanceof HasCleanup) {
((HasCleanup) item).cleanup();
}
}
}
}
public void setUsePatternFly(boolean use) {
usePatternFly = use;
}
private void updateEnabled() {
for (Pair<T, V> item : items) {
toggleEnabled(item.getFirst(), item.getSecond());
}
}
@Override
public void edit(final M model) {
// guard against multiple calls to edit()
if (this.model != null) {
this.model.getItemsChangedEvent().removeListener(itemsChangedListener);
this.model.getPropertyChangedEvent().removeListener(propertyChangedListener);
}
this.model = model;
model.getItemsChangedEvent().addListener(itemsChangedListener);
model.getPropertyChangedEvent().addListener(propertyChangedListener);
enabled = model.getIsChangable();
setVisible(model.getIsAvailable());
init(model);
}
@Override
public M flush() {
modelItems.clear();
for (Pair<T, V> item : items) {
T value = item.getFirst();
if (!isGhost(value)) {
modelItems.add(value);
}
}
return model;
}
public M getModel() {
return model;
}
private void cleanContentPanel() {
for (int i = 0; i < contentPanel.getWidgetCount(); i++) {
Widget widget = contentPanel.getWidget(i);
if (widget instanceof HasCleanup) {
((HasCleanup) widget).cleanup();
}
}
contentPanel.clear();
}
@Override
public void cleanup() {
if (model != null) {
model.cleanup();
model = null;
}
cleanupModelItems();
cleanupNonGhostItems();
cleanContentPanel();
}
private void cleanupNonGhostItems() {
for (Pair<T, V> item : items) {
T value = item.getFirst();
if (!isGhost(value)) {
if (item instanceof HasCleanup) {
((HasCleanup) value).cleanup();
}
}
}
}
protected Pair<T, V> addGhostEntry() {
T value = createGhostValue();
V widget = addEntry(value, true);
return new Pair<>(value, widget);
}
private V addEntry(final T value, boolean lastItem) {
final V widget = createWidget(value);
Pair<T, V> item = new Pair<>(value, widget);
items.add(item);
contentPanel.add(createAddRemoveRowPanel(lastItem, widget, item));
toggleEnabled(value, widget);
widget.addValueChangeHandler(new ValueChangeHandler<T>() {
private boolean wasGhost = isGhost(value);
@Override
public void onValueChange(ValueChangeEvent<T> event) {
T value = event.getValue();
boolean becomingGhost = isGhost(value);
if (becomingGhost != wasGhost) {
wasGhost = becomingGhost;
if (enabled) {
toggleGhost(value, widget, becomingGhost);
}
}
}
});
return widget;
}
private AddRemoveRowPanel createAddRemoveRowPanel(boolean lastItem, V widget, Pair<T, V> item) {
boolean shouldCreateAddButton = lastItem && showAddButton;
List<Button> buttons = new ArrayList<>(2);
buttons.add(createMinusButton(item));
if (shouldCreateAddButton) {
buttons.add(createPlusButton(item));
}
return new AddRemoveRowPanel(widget, !usePatternFly, buttons.toArray(new Button[buttons.size()]));
}
private void toggleEnabled(T value, V widget) {
setButtonsEnabled(widget, enabled);
if (widget instanceof HasEnabled) {
((HasEnabled) widget).setEnabled(enabled);
}
if (enabled && isGhost(value)) { // if entry is enabled, it still might need to be rendered as a ghost entry
toggleGhost(value, widget, true);
}
}
private void removeEntry(Pair<T, V> item) {
items.remove(item);
removeWidget(item.getSecond());
}
private Button createMinusButton(final Pair<T, V> item) {
final Button button = createButton(IconType.MINUS, event -> {
final T value = item.getFirst();
final V widget = item.getSecond();
if (vetoRemoveWidget(item, value, widget)) {
return;
}
doRemoveItem(item, value, widget);
});
return button;
}
private Button createPlusButton(final Pair<T, V> item) {
final Button button = createButton(IconType.PLUS, event -> {
V widget = item.getSecond();
getEntry(widget).removeLastButton();
Pair<T, V> item1 = addGhostEntry();
onAdd(item1.getFirst(), item1.getSecond());
});
return button;
}
private Button createButton(IconType iconType, ClickHandler handler) {
final Button button = new Button("", iconType, handler);
button.addStyleName(style.buttonStyle());
return button;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public boolean isEnabled() {
return enabled;
}
protected void doRemoveItem(Pair<T, V> item, T value, V widget) {
if (items.isEmpty()) { // just a precaution; if there's no item, there should be no button
return;
}
//'plus' button is present only on last item. So if removing such, we need to return in onto newly-last item.
boolean removalOfLastItem = item == lastItem();
removeEntry(item);
onRemove(value, widget);
if (removalOfLastItem && !items.isEmpty() && this.showAddButton) {
Pair<T, V> last = lastItem();
V lastItemWidget = last.getSecond();
getEntry(lastItemWidget).appendButton(createPlusButton(last));
}
if (items.isEmpty() && this.showGhost) {
Pair<T, V> ghostItem = addGhostEntry();
onAdd(ghostItem.getFirst(), ghostItem.getSecond());
}
}
private Pair<T, V> lastItem() {
return items.get(items.size() - 1);
}
/**
* Lets to decide if we really want to remove this item or not.
* Everything can be removed by default.
*/
protected boolean vetoRemoveWidget(Pair<T, V> item, T value, V widget) {
return false;
}
@SuppressWarnings("unchecked")
protected AddRemoveRowPanel getEntry(V widget) {
return (AddRemoveRowPanel) widget.getParent();
}
private void removeWidget(V widget) {
contentPanel.remove(getEntry(widget));
}
private void setButtonsEnabled(V widget, boolean enabled) {
AddRemoveRowPanel parent = getEntry(widget);
if (parent != null) {
parent.setButtonsEnabled(enabled);
}
}
protected class AddRemoveRowPanel extends FlowPanel implements HasCleanup {
private List<Button> buttons = new LinkedList<>();
private SimplePanel div = new SimplePanel();
public AddRemoveRowPanel(Widget widget, boolean floatLeft, Button... buttons) {
append(widget);
this.buttons.clear();
for (Button button : buttons) {
append(button, floatLeft);
this.buttons.add(button);
}
div.getElement().getStyle().setClear(Clear.BOTH);
add(div);
}
private void append(Widget widget) {
append(widget, true);
}
private void append(Widget widget, boolean floatLeft) {
if (floatLeft) {
widget.getElement().getStyle().setFloat(Float.LEFT);
} else {
widget.getElement().getStyle().setFloat(Float.RIGHT);
}
add(widget);
}
public void setButtonsEnabled(boolean enabled) {
for (Button button : buttons) {
button.setEnabled(enabled);
}
}
public void removeLastButton() {
remove(buttons.remove(buttons.size() - 1));
}
public void appendButton(Button button) {
buttons.add(button);
remove(div);
append(button, !usePatternFly);
add(div);
}
public void cleanup() {
for (int i = 0; i < getWidgetCount(); i++) {
Widget widget = getWidget(i);
if (widget instanceof HasCleanup) {
((HasCleanup) widget).cleanup();
}
}
clear();
}
}
@Override
public void focusInput() {
super.focusInput();
ListIterator<Pair<T, V>> last = items.listIterator(items.size());
if (last.hasPrevious()) {
V widget = last.previous().getSecond();
if (widget instanceof Focusable) {
((Focusable) widget).setFocus(true);
}
}
}
/**
* This method is called straight after an entry is added by pressing the plus button. Note that this new entry will
* necessarily be a "ghost" entry, as the plus button always adds entries that are initially in ghost state.
* Override to specify implementation-specific behavior.
*
* @param value
* the value added.
* @param widget
* the widget added.
*/
protected void onAdd(T value, V widget) {
modelItems.add(value);
if (widget instanceof Focusable) {
((Focusable) widget).setFocus(true);
}
}
/**
* This method is called straight after an entry is removed by pressing the minues button. Note that this entry will
* necessarily be a non-"ghost" entry, as otherwise the minus button would have been disabled. Override to specify
* implementation-specific behavior.
*
* @param value
* the value removed.
* @param widget
* the widget removed.
*/
protected void onRemove(T value, V widget) {
modelItems.remove(value);
}
/**
* This method should return a new widget of type V backed by a value of type T.
*
* @param value
* the value backing the widget.
* @return a newly-constructed widget of type V.
*/
protected abstract V createWidget(T value);
/**
* This method should manufacture a new object of type T, corresponding to a "ghost" entry as implemented by a
* specific subclass. This object should be distinct from regular values, so that the entry could not be mistaken
* for a regular entry.
*
* @return a value corresponding to a ghost entry.
*/
protected abstract T createGhostValue();
/**
* This method receives a value of type T, and checks whether it corresponds to a "ghost" entry as implemented by a
* specific subclass. Please make sure to implement it in a consistent manner with respect to
* {@link #createGhostValue() }.
*
* @param value
* the value to check.
* @return whether the value corresponds to a ghost entry.
*/
protected abstract boolean isGhost(T value);
/**
* This method is called when the value backing the widget of type V has changed so that the widget transitioned into
* or out of "ghost" state. It should implement the details of the widget's appearance as it moves in or out of
* the ghost state. By default, only the buttons are enabled/disabled when changing state.
*
* @param value
* the value backing the widget.
* @param widget
* the widget which is transitioning to/from ghost state.
* @param becomingGhost
* true if the item is entering ghost state, false if exiting ghost state.
*/
protected void toggleGhost(T value, V widget, boolean becomingGhost) {
setButtonsEnabled(widget, !becomingGhost && enabled);
}
@Override
public int setTabIndexes(int nextTabIndex) {
for (Pair<T, V> item : items) {
V widget = item.getSecond();
if (widget instanceof FocusableComponentsContainer) {
nextTabIndex = ((FocusableComponentsContainer) widget).setTabIndexes(nextTabIndex);
} else if (widget instanceof Focusable) {
((Focusable) widget).setTabIndex(nextTabIndex++);
}
}
return nextTabIndex;
}
}