/**
* Copyright (C) 2015 Valkyrie RCP
*
* 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.valkyriercp.form.binding.swing;
import org.valkyriercp.binding.form.FormModel;
import org.valkyriercp.binding.value.ValueModel;
import org.valkyriercp.binding.value.support.BufferedCollectionValueModel;
import org.valkyriercp.binding.value.support.ListListModel;
import org.valkyriercp.component.ShuttleList;
import org.valkyriercp.form.binding.support.AbstractBinding;
import org.valkyriercp.list.DynamicListModel;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
/**
* Binding to manage a {@link ShuttleList} component.
*
* @author lstreepy
*/
public class ShuttleListBinding extends AbstractBinding {
private ShuttleList list;
private ListModel model;
private ValueModel selectedItemsHolder = null;
private ValueModel selectableItemsHolder;
private Comparator comparator;
private Class selectedItemType;
private Class concreteSelectedType;
private String formId;
/**
* Construct a binding.
*
* @param list ShuttleList to bind
* @param formModel The form model holding the bound property
* @param formPropertyPath Path to the property to bind
*/
public ShuttleListBinding(final ShuttleList list, final FormModel formModel, final String formPropertyPath) {
super(formModel, formPropertyPath, null);
this.list = list;
}
public void setComparator(Comparator comparator) {
this.comparator = comparator;
}
public void setModel(ListModel model) {
this.model = model;
}
public void setRenderer(ListCellRenderer renderer) {
list.setCellRenderer(renderer);
}
public ListCellRenderer getRenderer() {
return list.getCellRenderer();
}
public void setSelectableItemsHolder(ValueModel selectableItemsHolder) {
this.selectableItemsHolder = selectableItemsHolder;
}
public void setSelectedItemsHolder(ValueModel selectedItemsHolder) {
this.selectedItemsHolder = selectedItemsHolder;
}
public void setSelectedItemType(final Class selectedItemType) {
this.selectedItemType = selectedItemType;
}
protected Class getSelectedItemType() {
if (this.selectedItemType == null && this.selectedItemsHolder != null
&& this.selectedItemsHolder.getValue() != null) {
setSelectedItemType(this.selectedItemsHolder.getValue().getClass());
}
return this.selectedItemType;
}
protected JComponent doBindControl() {
list.setModel(createModel());
if (selectedItemsHolder != null) {
setSelectedValue(null);
list.addListSelectionListener(new ListSelectedValueMediator());
}
if (comparator != null) {
list.setComparator(comparator);
}
// Get the icon to use for the edit button
list.setEditIcon(getEditIcon(), getEditIconText());
list.setListLabels(getChosenLabel(), getSourceLabel());
return list;
}
private String getSourceLabel() {
return getMsgText("shuttleList.sourceList.label", null);
}
private String getChosenLabel() {
return getMsgText("shuttleList.chosenList.label", null);
}
/**
* Look for Edit Text by searching the ApplicationServices for:
* formId.shuttleList.editText if nothing try shuttleList.editText
*
* @return string
*/
private String getEditIconText() {
return getMsgText("shuttleList.editText", "Edit...");
}
private String getMsgText(String key, String defaultMsg) {
String text = null;
if (getFormId() != null) {
if (getProperty() != null) {
text = getApplicationConfig().messageSource().getMessage(getFormId() + "." + getProperty() + "." + key, null, null, null);
}
if (text == null) {
text = getApplicationConfig().messageSource().getMessage(getFormId() + "." + key, null, null, null);
}
}
if (text == null) {
text = getApplicationConfig().messageSource().getMessage(key, null, defaultMsg, null);
}
return text;
}
/**
* Using the application services, check for: formId.shuttleList.edit if not
* there shuttleList.edit
*
* @return an Icon
*/
private Icon getEditIcon() {
Icon icon = null;
if (getFormId() != null) {
icon = getApplicationConfig().iconSource().getIcon(getFormId() + ".shuttleList.edit");
}
if (icon == null) {
icon = getApplicationConfig().iconSource().getIcon("shuttleList.edit");
}
return icon;
}
/**
* Determine if the selected item type can be multi-valued (is a collection
* or an array.
*
* @return boolean <code>true</code> if the <code>selectedItemType</code>
* is a Collection or an Array.
*/
protected boolean isSelectedItemAnArray() {
Class itemType = getSelectedItemType();
return itemType != null && itemType.isArray();
}
protected boolean isSelectedItemACollection() {
return getSelectedItemType() != null && Collection.class.isAssignableFrom(getSelectedItemType());
}
protected Class getConcreteSelectedType() {
if (concreteSelectedType == null) {
if (isSelectedItemACollection()) {
concreteSelectedType = BufferedCollectionValueModel.getConcreteCollectionType(getSelectedItemType());
}
else if (isSelectedItemAnArray()) {
concreteSelectedType = getSelectedItemType().getComponentType();
}
}
return concreteSelectedType;
}
protected void setSelectedValue(final PropertyChangeListener silentValueChangeHandler) {
final int[] indices = indicesOf(selectedItemsHolder.getValue());
if (indices.length < 1) {
list.clearSelection();
}
else {
list.setSelectedIndices(indices);
// The selection may now be different than what is reflected in
// collection property if this is SINGLE_INTERVAL_SELECTION, so
// modify if needed...
updateSelectionHolderFromList(silentValueChangeHandler);
}
}
/**
* Return an array of indices in the selectableItems for each element in the
* provided set. The set can be either a Collection or an Array.
*
* @param itemSet Either an array or a Collection of items
* @return array of indices of the elements in itemSet within the
* selectableItems
*/
protected int[] indicesOf(final Object itemSet) {
int[] ret = null;
if (itemSet instanceof Collection) {
Collection collection = (Collection) itemSet;
ret = new int[collection.size()];
int i = 0;
for (Iterator iter = collection.iterator(); iter.hasNext(); i++) {
ret[i] = indexOf(iter.next());
}
}
else if (itemSet == null) {
ret = new int[0];
}
else if (itemSet.getClass().isArray()) {
Object[] items = (Object[]) itemSet;
ret = new int[items.length];
for (int i = 0; i < items.length; i++) {
ret[i] = indexOf(items[i]);
}
}
else {
throw new IllegalArgumentException("itemSet must be eithe an Array or a Collection");
}
return ret;
}
protected int indexOf(final Object o) {
final ListModel listModel = list.getModel();
final int size = listModel.getSize();
for (int i = 0; i < size; i++) {
if (equalByComparator(o, listModel.getElementAt(i))) {
return i;
}
}
return -1;
}
private ListModel createModel() {
if (model != null)
return model;
ListListModel model;
if (selectableItemsHolder != null) {
model = new DynamicListModel(selectableItemsHolder);
}
else {
model = new ListListModel();
}
model.setComparator(comparator);
model.sort();
return model;
}
protected void updateSelectionHolderFromList(final PropertyChangeListener silentValueChangeHandler) {
final Object[] selected = list.getSelectedValues();
if (isSelectedItemACollection()) {
try {
// In order to properly handle buffered forms, we will
// create a new collection to hold the new selection.
final Collection newSelection = (Collection) getConcreteSelectedType().newInstance();
if (selected != null && selected.length > 0) {
for (int i = 0; i < selected.length; i++) {
newSelection.add(selected[i]);
}
}
// Only modify the selectedItemsHolder if the selection is
// actually changed.
final Collection oldSelection = (Collection) selectedItemsHolder.getValue();
if (oldSelection == null || oldSelection.size() != newSelection.size()
|| !collectionsEqual(oldSelection, newSelection)) {
if (silentValueChangeHandler != null) {
selectedItemsHolder.setValueSilently(newSelection, silentValueChangeHandler);
}
else {
selectedItemsHolder.setValue(newSelection);
}
}
}
catch (InstantiationException e1) {
throw new RuntimeException("Unable to instantiate new concrete collection class for new selection.", e1);
}
catch (IllegalAccessException e1) {
throw new RuntimeException(e1);
}
}
else if (isSelectedItemAnArray()) {
final Object[] newSelection = (Object[]) Array.newInstance(getConcreteSelectedType(), selected.length);
for (int i = 0; i < selected.length; i++) {
newSelection[i] = selected[i];
}
// Only modify the selectedItemsHolder if the selection is actually
// changed.
final Object[] oldSelection = (Object[]) selectedItemsHolder.getValue();
if (oldSelection == null || oldSelection.length != newSelection.length
|| !arraysEqual(oldSelection, newSelection)) {
if (silentValueChangeHandler != null) {
selectedItemsHolder.setValueSilently(newSelection, silentValueChangeHandler);
}
else {
selectedItemsHolder.setValue(newSelection);
}
}
}
}
/**
* Compare two arrays for equality using the configured comparator.
*
* @param a1 First array to compare
* @param a2 Second array to compare
* @return boolean true if they are equal
*/
protected boolean arraysEqual(Object[] a1, Object[] a2) {
if (a1 != null && a2 != null && a1.length == a2.length) {
// Loop over each element and compare them using our comparator
for (int i = 0; i < a1.length; i++) {
if (!equalByComparator(a1[i], a2[i])) {
return false;
}
}
return true;
}
else if (a1 == null && a2 == null) {
return true;
}
return false;
}
/**
* Compare two collections for equality using the configured comparator.
* Element order must be the same for the collections to compare equal.
*
* @param a1 First collection to compare
* @param a2 Second collection to compare
* @return boolean true if they are equal
*/
protected boolean collectionsEqual(Collection a1, Collection a2) {
if (a1 != null && a2 != null && a1.size() == a2.size()) {
// Loop over each element and compare them using our comparator
Iterator iterA1 = a1.iterator();
Iterator iterA2 = a2.iterator();
while (iterA1.hasNext()) {
if (!equalByComparator(iterA1.next(), iterA2.next())) {
return false;
}
}
}
else if (a1 == null && a2 == null) {
return true;
}
return false;
}
/**
* Using the configured comparator (or equals if not configured), determine
* if two objects are equal.
*
* @param o1 Object to compare
* @param o2 Object to compare
* @return boolean true if objects are equal
*/
private boolean equalByComparator(Object o1, Object o2) {
return comparator == null ? o1.equals(o2) : comparator.compare(o1, o2) == 0;
}
protected void readOnlyChanged() {
list.setEnabled(isEnabled() && !isReadOnly());
}
protected void enabledChanged() {
list.setEnabled(isEnabled() && !isReadOnly());
}
/**
* @return Returns the formId.
*/
protected String getFormId() {
return formId;
}
/**
* @param formId The formId to set.
*/
protected void setFormId(String formId) {
this.formId = formId;
}
/**
* Inner class to mediate the list selection events into calls to
* {@link #updateSelectionHolderFromList}.
*/
private class ListSelectedValueMediator implements ListSelectionListener {
private final PropertyChangeListener valueChangeHandler;
public ListSelectedValueMediator() {
valueChangeHandler = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
setSelectedValue(valueChangeHandler);
}
};
selectedItemsHolder.addValueChangeListener(valueChangeHandler);
}
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
updateSelectionHolderFromList(valueChangeHandler);
}
}
}
}