package au.com.vaadinutils.ui;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.SingularAttribute;
import com.google.common.base.Preconditions;
import com.vaadin.addon.jpacontainer.JPAContainer;
import com.vaadin.addon.jpacontainer.util.DefaultQueryModifierDelegate;
import com.vaadin.data.Buffered;
import com.vaadin.data.Container.Filter;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.util.BeanContainer;
import com.vaadin.data.util.filter.Compare;
import com.vaadin.data.util.filter.Not;
import com.vaadin.data.util.filter.Or;
import com.vaadin.data.util.filter.SimpleStringFilter;
import com.vaadin.event.ItemClickEvent;
import com.vaadin.event.ItemClickEvent.ItemClickListener;
import com.vaadin.server.FontAwesome;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomField;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.RowDescriptionGenerator;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import au.com.vaadinutils.crud.CrudEntity;
import au.com.vaadinutils.crud.GridHeadingPropertySet;
import au.com.vaadinutils.crud.SearchableGrid;
import au.com.vaadinutils.dao.EntityManagerProvider;
import au.com.vaadinutils.dao.JpaBaseDao;
import au.com.vaadinutils.dao.NullFilter;
public class TwinColumnSelect<C extends CrudEntity> extends CustomField<Collection<C>>
{
private static final long serialVersionUID = -4316521010865902678L;
private boolean initialised;
private SingularAttribute<C, ?> itemCaptionProperty;
private Class<C> itemClass;
private Collection<C> sourceValue;
@SuppressWarnings("rawtypes")
private Class<? extends Collection> valueClass;
private SearchableGrid<C, JPAContainer<C>> availableGrid;
private JPAContainer<C> availableContainer;
private Grid selectedGrid;
private BeanContainer<Long, C> selectedBeans;
private SingularAttribute<C, Long> beanIdField;
private HorizontalLayout mainLayout = new HorizontalLayout();
private Button addNewButton = new Button(FontAwesome.PLUS);
private Button addButton = new Button(">");
private Button removeButton = new Button("<");
private Button removeAllButton = new Button("<<");
private Button addAllButton = new Button(">>");
private Filter baselineFilter;
private Filter selectedFilter;
private String availableColumnHeader = "Available";
private String selectedColumnHeader = "Selected";
private LinkedHashSet<ValueChangeListener<C>> valueChangeListeners = null;
private CreateNewCallback<C> createNewCallback;
private boolean sortAscending = true;
private boolean showAddRemoveAll;
private static final float BUTTON_LAYOUT_WIDTH = 50;
private static final float BUTTON_WIDTH = 45;
private static final float DEFAULT_COLUMN_WIDTH = 200;
private static final float DEFAULT_COLUMN_HEIGHT = 300;
public TwinColumnSelect(String caption, SingularAttribute<C, ?> itemCaptionProperty)
{
this.setCaption(caption);
this.itemCaptionProperty = itemCaptionProperty;
itemClass = itemCaptionProperty.getDeclaringType().getJavaType();
createSelectedGrid();
createAvailableGrid();
addNewButton.setVisible(false);
}
private void createSelectedGrid()
{
selectedGrid = new Grid();
selectedGrid.setContainerDataSource(createSelectedContainer());
selectedGrid.setWidth(DEFAULT_COLUMN_WIDTH, Unit.PIXELS);
selectedGrid.setHeight(DEFAULT_COLUMN_HEIGHT, Unit.PIXELS);
selectedGrid.addItemClickListener(new ItemClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void itemClick(ItemClickEvent event)
{
if (event.isDoubleClick())
{
removeButton.click();
}
}
});
}
private BeanContainer<Long, C> createSelectedContainer()
{
final Metamodel metaModel = EntityManagerProvider.getEntityManager().getMetamodel();
final EntityType<C> type = metaModel.entity(itemClass);
beanIdField = type.getDeclaredId(Long.class);
selectedBeans = new BeanContainer<Long, C>(itemClass);
selectedBeans.setBeanIdProperty(beanIdField.getName());
sortSelectedBeans();
return selectedBeans;
}
protected void createAvailableGrid()
{
createAvailableContainer();
// TODO: Add proper uniqueId
availableGrid = new SearchableGrid<C, JPAContainer<C>>(this.getClass().getSimpleName())
{
private static final long serialVersionUID = 1L;
@Override
public GridHeadingPropertySet getHeadingPropertySet()
{
return new GridHeadingPropertySet.Builder<C>()
.addColumn(availableColumnHeader, itemCaptionProperty.getName(), true, true).build();
}
@Override
public JPAContainer<C> getContainer()
{
return availableContainer;
}
@Override
protected Filter getContainerFilter(String filterString, boolean advancedSearchActive)
{
Filter searchFilter = null;
if (filterString != null && filterString.length() > 0)
{
searchFilter = getSearchFilter(filterString);
}
return NullFilter.and(baselineFilter, selectedFilter, searchFilter);
}
@Override
protected String getTitle()
{
return null;
}
};
// Needs to be here after availableContainer creation,
// otherwise sorting goes away
sortAvailableContainer();
availableGrid.addItemClickListener(new ItemClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void itemClick(ItemClickEvent event)
{
if (event.isDoubleClick())
{
addButton.click();
}
}
});
availableGrid.setWidth(DEFAULT_COLUMN_WIDTH, Unit.PIXELS);
availableGrid.setHeight(DEFAULT_COLUMN_HEIGHT, Unit.PIXELS);
}
private JPAContainer<C> createAvailableContainer()
{
availableContainer = JpaBaseDao.getGenericDao(itemClass).createVaadinContainer();
sortAvailableContainer();
return availableContainer;
}
@Override
public void beforeClientResponse(boolean initial)
{
super.beforeClientResponse(initial);
if (!initialised)
{
// TODO: Add proper uniqueId
new GridHeadingPropertySet.Builder<C>()
.addColumn(selectedColumnHeader, itemCaptionProperty.getName(), true, true).build()
.applyToGrid(selectedGrid, this.getClass().getSimpleName());
initialised = true;
}
}
@Override
protected Component initContent()
{
mainLayout.addComponent(availableGrid);
mainLayout.addComponent(buildButtons());
mainLayout.addComponent(selectedGrid);
mainLayout.setExpandRatio(availableGrid, 1);
mainLayout.setExpandRatio(selectedGrid, 1);
return mainLayout;
}
private Component buildButtons()
{
final VerticalLayout layout = new VerticalLayout();
layout.setWidth(BUTTON_LAYOUT_WIDTH, Unit.PIXELS);
addButton.setWidth(BUTTON_WIDTH, Unit.PIXELS);
removeButton.setWidth(BUTTON_WIDTH, Unit.PIXELS);
addAllButton.setWidth(BUTTON_WIDTH, Unit.PIXELS);
removeAllButton.setWidth(BUTTON_WIDTH, Unit.PIXELS);
addNewButton.setWidth(BUTTON_WIDTH, Unit.PIXELS);
addButton.addClickListener(addClickListener());
removeButton.addClickListener(removeClickListener());
addAllButton.addClickListener(addAllClickListener());
removeAllButton.addClickListener(removeAllClickListener());
addNewButton.addClickListener(addNewClickListener());
layout.addComponent(addButton);
layout.addComponent(removeButton);
layout.addComponent(addAllButton);
layout.addComponent(removeAllButton);
layout.addComponent(addNewButton);
layout.setComponentAlignment(addButton, Alignment.MIDDLE_CENTER);
layout.setComponentAlignment(removeButton, Alignment.MIDDLE_CENTER);
layout.setComponentAlignment(addAllButton, Alignment.MIDDLE_CENTER);
layout.setComponentAlignment(removeAllButton, Alignment.MIDDLE_CENTER);
layout.setComponentAlignment(addNewButton, Alignment.MIDDLE_CENTER);
return layout;
}
public void setEnabledAddAllButton(boolean enabled)
{
addAllButton.setVisible(enabled);
addAllButton.setEnabled(enabled);
}
public void setEnabledAddButton(boolean enabled)
{
// issue encountered : even when setting visible to false, the button
// still appears on the page but setEnabled has no issue
addButton.setVisible(enabled);
addButton.setEnabled(enabled);
}
public void setShowAddRemoveAll(boolean show)
{
showAddRemoveAll = show;
addAllButton.setVisible(show);
removeAllButton.setVisible(show);
}
public void setAddNewAllowed(CreateNewCallback<C> createNewCallback)
{
addNewButton.setVisible(true);
this.createNewCallback = createNewCallback;
}
@Override
public void setWidth(float width, Unit unit)
{
super.setWidth(width, unit);
if (mainLayout != null && selectedGrid != null && availableGrid != null)
{
mainLayout.setWidth(width, unit);
selectedGrid.setWidth(((width - 5) / 2) - (BUTTON_LAYOUT_WIDTH / 2), unit);
availableGrid.setWidth(((width - 5) / 2) - (BUTTON_LAYOUT_WIDTH / 2), unit);
}
}
@Override
public void setHeight(String height)
{
super.setHeight(height);
selectedGrid.setHeight(height);
availableGrid.setHeight(height);
mainLayout.setHeight(height);
}
@Override
public void setSizeFull()
{
super.setSizeFull();
mainLayout.setSizeFull();
selectedGrid.setSizeFull();
availableGrid.setSizeFull();
}
@Override
public void setReadOnly(boolean b)
{
super.setReadOnly(b);
selectedGrid.setReadOnly(b);
availableGrid.setVisible(!b);
addButton.setVisible(!b);
removeButton.setVisible(!b);
if (showAddRemoveAll)
{
addAllButton.setVisible(!b);
removeAllButton.setVisible(!b);
}
}
public void sortAvailableContainer()
{
availableContainer.sort(new Object[]
{ itemCaptionProperty.getName() }, new boolean[]
{ sortAscending });
}
public void sortSelectedBeans()
{
selectedBeans.sort(new Object[]
{ itemCaptionProperty.getName() }, new boolean[]
{ sortAscending });
}
public interface ValueChangeListener<C>
{
void valueChanged(Collection<C> value);
}
public void addValueChangeListener(ValueChangeListener<C> listener)
{
if (valueChangeListeners == null)
{
valueChangeListeners = new LinkedHashSet<ValueChangeListener<C>>();
}
valueChangeListeners.add(listener);
}
public void setFilter(Filter filter)
{
baselineFilter = filter;
availableContainer.setFireContainerItemSetChangeEvents(false);
availableContainer.removeAllContainerFilters();
availableContainer.setFireContainerItemSetChangeEvents(true);
availableContainer.addContainerFilter(filter);
}
public void setFilterDelegate(DefaultQueryModifierDelegate defaultQueryModifierDelegate)
{
availableContainer.setQueryModifierDelegate(defaultQueryModifierDelegate);
}
@Override
public void commit() throws Buffered.SourceException, InvalidValueException
{
super.commit();
final Collection<C> tmp = getConvertedValue();
// avoid possible npe
if (sourceValue == null)
{
sourceValue = tmp;
}
// add missing
for (C c : tmp)
{
if (!sourceValue.contains(c))
{
sourceValue.add(c);
}
}
// remove unneeded
Set<C> toRemove = new HashSet<>();
for (C c : sourceValue)
{
if (!tmp.contains(c))
{
toRemove.add(c);
}
}
sourceValue.removeAll(toRemove);
}
@Override
public boolean isModified()
{
Collection<C> convertedValue = getConvertedValue();
Preconditions.checkNotNull(convertedValue,
"If you look at getConvertedValue, you'll see convertedValue can never be null");
if ((sourceValue == null || sourceValue.size() == 0) && (convertedValue.size() > 0))
{
return true;
}
if ((sourceValue == null || sourceValue.size() == 0) && (convertedValue.size() == 0))
{
return false;
}
boolean equal = convertedValue.containsAll(sourceValue) && sourceValue.containsAll(convertedValue);
return !equal;
}
@Override
protected void setInternalValue(Collection<C> newValue)
{
if (newValue != null)
{
valueClass = newValue.getClass();
}
super.setInternalValue(newValue);
selectedBeans.removeAllItems();
if (newValue != null)
{
selectedBeans.addAll(newValue);
}
sourceValue = getConvertedValue();
refreshSelected();
sortSelectedBeans();
}
@Override
public Collection<C> getConvertedValue()
{
Collection<C> selected;
if (valueClass != null && List.class.isAssignableFrom(valueClass))
{
selected = new LinkedList<>();
}
else
{
selected = new HashSet<>();
}
for (Long id : selectedBeans.getItemIds())
{
selected.add(selectedBeans.getItem(id).getBean());
}
return selected;
}
@Override
public Collection<C> getValue()
{
return getConvertedValue();
}
@Override
public Collection<C> getInternalValue()
{
return getConvertedValue();
}
@SuppressWarnings("unchecked")
@Override
public Class<Collection<C>> getType()
{
// Had to remove this as maven can't compile it.
// return (Class<? extends Collection<C>>) a.getClass();
return (Class<Collection<C>>) (Class<?>) Collection.class;
}
private void refreshSelected()
{
final List<Long> selectedIds = selectedBeans.getItemIds();
if (selectedIds.size() == 1)
{
selectedFilter = new Not(new Compare.Equal(beanIdField.getName(), selectedIds.get(0)));
availableGrid.triggerFilter();
return;
}
final Vector<Filter> filters = new Vector<>();
for (Long id : selectedIds)
{
filters.add(new Compare.Equal(beanIdField.getName(), id));
}
selectedFilter = new Not(new Or(filters.toArray(new Filter[filters.size()])));
availableGrid.triggerFilter();
}
public Collection<C> getSourceValue()
{
return sourceValue;
}
protected Grid getSelectedGrid()
{
return selectedGrid;
}
protected void addAction(Long id)
{
final C item = JpaBaseDao.getGenericDao(itemClass).findById(id);
if (item != null)
{
selectedBeans.addBean(item);
if (valueChangeListeners != null)
{
for (ValueChangeListener<C> listener : valueChangeListeners)
{
listener.valueChanged(getValue());
}
}
}
}
protected void removeAction(Long id)
{
selectedBeans.removeItem(id);
if (valueChangeListeners != null)
{
for (ValueChangeListener<C> listener : valueChangeListeners)
{
listener.valueChanged(getValue());
}
}
}
protected void removeAllAction()
{
selectedBeans.removeAllItems();
if (valueChangeListeners != null)
{
for (ValueChangeListener<C> listener : valueChangeListeners)
{
listener.valueChanged(getValue());
}
}
}
public void setSelectedRowDescriptionGenerator(final RowDescriptionGenerator generator)
{
selectedGrid.setRowDescriptionGenerator(generator);
}
public String getAvailableColumnHeader()
{
return availableColumnHeader;
}
public void setAvailableColumnHeader(String availableColumnHeader)
{
this.availableColumnHeader = availableColumnHeader;
}
public SearchableGrid<C, JPAContainer<C>> getAvailableGrid()
{
return availableGrid;
}
public void setAvailableGrid(SearchableGrid<C, JPAContainer<C>> availableGrid)
{
this.availableGrid = availableGrid;
}
public String getSelectedColumnHeader()
{
return selectedColumnHeader;
}
public void setSelectedColumnHeader(String selectedColumnHeader)
{
this.selectedColumnHeader = selectedColumnHeader;
}
protected ClickListener addClickListener()
{
return new ClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event)
{
final Collection<Object> ids = availableGrid.getSelectedRows();
if (!ids.isEmpty())
{
for (Object id : ids)
{
addAction((Long) id);
}
availableGrid.select(null);
refreshSelected();
sortSelectedBeans();
}
}
};
}
protected ClickListener addAllClickListener()
{
return new ClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event)
{
final Collection<Object> ids = availableContainer.getItemIds();
if (!ids.isEmpty())
{
for (Object id : ids)
{
addAction((Long) id);
}
availableGrid.select(null);
refreshSelected();
sortSelectedBeans();
}
}
};
}
protected ClickListener removeClickListener()
{
return new ClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event)
{
final Collection<Object> ids = selectedGrid.getSelectedRows();
if (!ids.isEmpty())
{
for (Object id : ids)
{
removeAction((Long) id);
}
selectedGrid.select(null);
refreshSelected();
}
}
};
}
protected ClickListener removeAllClickListener()
{
return new ClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event)
{
removeAllAction();
refreshSelected();
}
};
}
protected ClickListener addNewClickListener()
{
return new ClickListener()
{
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event)
{
createNewCallback.createNew(new RefreshCallback()
{
@Override
public void refresh()
{
availableContainer.refresh();
}
});
}
};
}
protected Filter getSearchFilter(final String filterString)
{
return new SimpleStringFilter(itemCaptionProperty.getName(), filterString, true, false);
}
protected void triggerFilterForAvailableGrid()
{
availableGrid.triggerFilter();
}
}