/*
* Copyright 2017 Matti Tahvonen.
*
* 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.vaadin.viritin.fields;
import com.vaadin.data.Binder;
import com.vaadin.server.FontAwesome;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.themes.ValoTheme;
import org.vaadin.viritin.button.ConfirmButton;
import org.vaadin.viritin.button.MButton;
import org.vaadin.viritin.layouts.MGridLayout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.vaadin.viritin.form.AbstractForm;
/**
* NOTE, this V8 compatible version of this class should still be considered experimental.
* <p>
* A field suitable for editing collection of referenced objects tied to parent
* object only. E.g. OneToMany/ElementCollection fields in JPA world.
* <p>
* Some features/restrictions:
* <ul>
* <li>The field is valid when all elements are valid.
* <li>The field is always non buffered
* <li>The element type needs to have an empty parameter constructor or user
* must provide an Instantiator.
* </ul>
*
* Elements in the edited collection are modified with BeanFieldGroup. Fields
* should defined in a class. A simple usage example for editing
* List>Address< adresses:
* <pre><code>
* public static class AddressRow {
* EnumSelect type = new EnumSelect();
* MTextField street = new MTextField();
* MTextField city = new MTextField();
* MTextField zipCode = new MTextField();
* }
*
* public static class PersonForm<Person> extends AbstractForm {
* private final ElementCollectionField<Address> addresses
* = new ElementCollectionField<Address>(Address.class,
* AddressRow.class).withCaption("Addressess");
*
* </code></pre>
*
* <p>
* Components in row model class don't need to match properties in the edited
* entity. So you can add "custom columns" just by introducing them in your
* editor row.
* <p>
* By default the field always contains an empty instance to create new rows. If
* instances are added with some other method (or UI shouldn't add them at all),
* you can configure this with setAllowNewItems. Deletions can be configured
* with setAllowRemovingItems.
* <p>
* If developer needs to do some additional logic during element
* addition/removal, one can subscribe to related events using
* addElementAddedListener/addElementRemovedListener.
*
*
* @author Matti Tahvonen
* @param <ET> The type in the entity collection. The type must have empty
* parameter constructor or you have to provide Instantiator.
* @param <CT>
*
*/
public class ElementCollectionField<ET, CT extends Collection<ET>> extends AbstractElementCollection<ET,CT> {
private static final long serialVersionUID = 8573373104105052804L;
List<ET> items = new ArrayList<>();
boolean inited = false;
MGridLayout layout = new MGridLayout();
private boolean visibleHeaders = true;
private boolean requireVerificationForRemoval;
private AbstractForm<ET> popupEditor;
public ElementCollectionField(Class<ET> elementType,
Class<?> formType) {
super(elementType, formType);
}
public ElementCollectionField(Class<ET> elementType, Instantiator i,
Class<?> formType) {
super(elementType, i, formType);
}
@Override
public void addInternalElement(final ET v) {
ensureInited();
items.add(v);
Binder<ET> fg = getFieldGroupFor(v);
for (Object property : getVisibleProperties()) {
Optional<Binder.Binding<ET, ?>> binding = fg.getBinding(property.toString());
Component c = null;
if (!binding.isPresent()) {
c = getComponentFor(v, property.toString());
Logger.getLogger(ElementCollectionField.class.getName())
.log(Level.WARNING, "No editor field for{0}", property);
} else {
// TODO, should always use getComponentFor ??
c = (Component) binding.get().getField();
}
layout.addComponent(c);
layout.setComponentAlignment(c, Alignment.MIDDLE_LEFT);
}
if (getPopupEditor() != null) {
MButton b = new MButton(FontAwesome.EDIT)
.withStyleName(ValoTheme.BUTTON_ICON_ONLY)
.withListener(new Button.ClickListener() {
private static final long serialVersionUID = 5019806363620874205L;
@Override
public void buttonClick(Button.ClickEvent event) {
editInPopup(v);
}
});
layout.add(b);
}
if (isAllowRemovingItems()) {
layout.add(createRemoveButton(v));
}
if (!isAllowEditItems()) {
fg.setReadOnly(true);
}
}
protected Component createRemoveButton(final ET v) {
Button b;
if (requireVerificationForRemoval) {
b = new ConfirmButton();
} else {
b = new MButton();
}
b.setIcon(FontAwesome.TRASH_O);
b.addStyleName(ValoTheme.BUTTON_ICON_ONLY);
b.addStyleName(ValoTheme.BUTTON_DANGER);
b.addClickListener(new Button.ClickListener() {
private static final long serialVersionUID = 5019806363620874205L;
@Override
public void buttonClick(Button.ClickEvent event) {
removeElement(v);
}
});
return b;
}
@Override
public void removeInternalElement(ET v) {
int index = itemsIdentityIndexOf(v);
items.remove(index);
int row = index + 1;
layout.removeRow(row);
}
@Override
public GridLayout getLayout() {
return layout;
}
@Override
public void setPersisted(ET v, boolean persisted) {
int row = itemsIdentityIndexOf(v) + 1;
if (isAllowRemovingItems()) {
Button c = (Button) layout.getComponent(layout.getColumns() - 1, row);
if (persisted) {
c.setDescription(getDeleteElementDescription());
} else {
for (int i = 0; i < getVisibleProperties().size(); i++) {
try {
AbstractField f = (AbstractField) layout.
getComponent(i,
row);
// FIXME
//f.setValidationVisible(false);
} catch (Exception e) {
}
}
c.setDescription(getDisabledDeleteElementDescription());
}
c.setEnabled(persisted);
}
}
private int itemsIdentityIndexOf(Object o) {
for (int index = 0; index < items.size(); index++) {
if (items.get(index) == o) {
return index;
}
}
return -1;
}
private void ensureInited() {
if (!inited) {
layout.setSpacing(true);
int columns = getVisibleProperties().size();
if (isAllowRemovingItems()) {
columns++;
}
if(getPopupEditor() != null) {
columns++;
}
layout.setColumns(columns);
if (visibleHeaders) {
for (Object property : getVisibleProperties()) {
Component header = createHeader(property);
layout.addComponent(header);
}
if (isAllowRemovingItems()) {
// leave last header slot empty, "actions" colunn
layout.newLine();
}
}
inited = true;
}
}
/**
* Creates the header for given property. By default a simple Label is used.
* Override this method to style it or to replace it with something more
* complex.
*
* @param property the property for which header is to be created.
* @return the component used for header
*/
protected Component createHeader(Object property) {
Label header = new Label(getPropertyHeader(property.
toString()));
header.setWidthUndefined();
return header;
}
public ElementCollectionField<ET,CT> withEditorInstantiator(
Instantiator instantiator) {
setEditorInstantiator(instantiator);
return this;
}
public ElementCollectionField<ET,CT> withNewEditorInstantiator(
EditorInstantiator<?, ET> instantiator) {
setNewEditorInstantiator(instantiator);
return this;
}
public ElementCollectionField<ET,CT> withVisibleHeaders(boolean visibleHeaders) {
this.visibleHeaders = visibleHeaders;
return this;
}
@Override
public void clear() {
if (inited) {
items.clear();
int rows = inited ? 1 : 0;
while (layout.getRows() > rows) {
layout.removeRow(rows);
}
}
}
public String getDisabledDeleteElementDescription() {
return disabledDeleteThisElementDescription;
}
public void setDisabledDeleteThisElementDescription(
String disabledDeleteThisElementDescription) {
this.disabledDeleteThisElementDescription = disabledDeleteThisElementDescription;
}
private String disabledDeleteThisElementDescription = "Fill this row to add a new element, currently ignored";
public String getDeleteElementDescription() {
return deleteThisElementDescription;
}
private String deleteThisElementDescription = "Delete this element";
public void setDeleteThisElementDescription(
String deleteThisElementDescription) {
this.deleteThisElementDescription = deleteThisElementDescription;
}
@Override
public void onElementAdded() {
if (isAllowNewItems()) {
newInstance = createInstance();
addInternalElement(newInstance);
setPersisted(newInstance, false);
}
}
@Override
public ElementCollectionField<ET,CT> setPropertyHeader(String propertyName,
String propertyHeader) {
super.setPropertyHeader(propertyName, propertyHeader);
return this;
}
@Override
public ElementCollectionField<ET,CT> setVisibleProperties(
List<String> properties, List<String> propertyHeaders) {
super.setVisibleProperties(properties, propertyHeaders);
return this;
}
@Override
public ElementCollectionField<ET,CT> setVisibleProperties(
List<String> properties) {
super.setVisibleProperties(properties);
return this;
}
@Override
public ElementCollectionField<ET,CT> setAllowNewElements(
boolean allowNewItems) {
super.setAllowNewElements(allowNewItems);
return this;
}
@Override
public ElementCollectionField<ET,CT> setAllowRemovingItems(
boolean allowRemovingItems) {
super.setAllowRemovingItems(allowRemovingItems);
return this;
}
@Override
public ElementCollectionField<ET,CT> withCaption(String caption) {
super.withCaption(caption);
return this;
}
@Override
public ElementCollectionField<ET,CT> removeElementRemovedListener(
ElementRemovedListener listener) {
super.removeElementRemovedListener(listener);
return this;
}
@Override
public ElementCollectionField<ET,CT> addElementRemovedListener(
ElementRemovedListener<ET> listener) {
super.addElementRemovedListener(listener);
return this;
}
@Override
public ElementCollectionField<ET,CT> removeElementAddedListener(
ElementAddedListener listener) {
super.removeElementAddedListener(listener);
return this;
}
@Override
public ElementCollectionField<ET,CT> addElementAddedListener(
ElementAddedListener<ET> listener) {
super.addElementAddedListener(listener);
return this;
}
/**
* Expands the column with given property id
*
* @param propertyId the id of column that should be expanded in the UI
* @return the element collection field
*/
public ElementCollectionField<ET,CT> expand(String... propertyId) {
for (String propertyId1 : propertyId) {
int index = getVisibleProperties().indexOf(propertyId1);
if (index == -1) {
throw new IllegalArgumentException(
"The expanded property must available");
}
layout.setColumnExpandRatio(index, 1);
}
if (layout.getWidth() == -1) {
layout.setWidth(100, Unit.PERCENTAGE);
}
// TODO should also make width of elements automatically 100%, both
// existing and added, now obsolete config needed for row model
return this;
}
public ElementCollectionField<ET,CT> withFullWidth() {
setWidth(100, Unit.PERCENTAGE);
return this;
}
public ElementCollectionField<ET,CT> withId(String id) {
setId(id);
return this;
}
public ElementCollectionField<ET,CT> setRequireVerificationForRemoving(boolean requireVerification) {
requireVerificationForRemoval = requireVerification;
return this;
}
public AbstractForm<ET> getPopupEditor() {
return popupEditor;
}
/**
* Method to set form to allow editing more properties than it would be
* convenient inline.
*
* @param newPopupEditor the popup editor to be used to edit instances
*/
public void setPopupEditor(AbstractForm<ET> newPopupEditor) {
this.popupEditor = newPopupEditor;
if (newPopupEditor != null) {
newPopupEditor.setSavedHandler(new AbstractForm.SavedHandler<ET>() {
private static final long serialVersionUID = 389618696563816566L;
@Override
public void onSave(ET entity) {
// TODO, figure out what to do with this...
// MBeanFieldGroup<ET> fg = getFieldGroupFor(entity);
// fg.setItemDataSource(entity);
// fg.setBeanModified(true);
// TODO refresh binding
popupEditor.getPopup().close();
}
});
}
}
/**
* Opens a (possibly configured) popup editor to edit given entity.
*
* @param entity the entity to be edited
*/
public void editInPopup(ET entity) {
getPopupEditor().setEntity(entity);
getPopupEditor().openInModalPopup();
}
}