/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util.model;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import javax.swing.AbstractListModel;
import javax.swing.ComboBoxModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import com.servoy.j2db.dataprocessing.CustomValueList;
import com.servoy.j2db.dataprocessing.IFoundSetInternal;
import com.servoy.j2db.dataprocessing.IModificationListener;
import com.servoy.j2db.dataprocessing.IRecordInternal;
import com.servoy.j2db.dataprocessing.IValueList;
import com.servoy.j2db.dataprocessing.LookupValueList;
import com.servoy.j2db.dataprocessing.ModificationEvent;
import com.servoy.j2db.persistence.IDataProvider;
import com.servoy.j2db.persistence.ValueList;
import com.servoy.j2db.util.ScopesUtils;
/**
* @author jcompagner
*/
public class ComboModelListModelWrapper<E> extends AbstractListModel implements ComboBoxModel, IEditListModel, List<E>, IModificationListener
{
protected IValueList listModel;
protected final ListModelListener listener;
protected Object selectedObject; // TODO can't we just remove this and count on selectedSet? otherwise we always need to keep all selection stuff in sync
protected boolean valueListChanging;
protected boolean hideFirstValue;
private Object realSelectedObject; // TODO can't we just remove this and count on selectedSet? otherwise we always need to keep all selection stuff in sync
private final boolean shouldHideEmptyValueIfPresent;
private IRecordInternal parentState;
private IRecordInternal relatedRecord;
private String relatedFoundsetLookup;
protected Set<Integer> selectedSet;
public ComboModelListModelWrapper(IValueList listModel, boolean shouldHideEmptyValueIfPresent, boolean isSeparatorAware)
{
if (isSeparatorAware)
{
this.listModel = new SeparatorProcessingValueList(listModel);
}
else
{
this.listModel = listModel;
}
this.shouldHideEmptyValueIfPresent = shouldHideEmptyValueIfPresent;
listener = new ListModelListener();
listModel.addListDataListener(listener);
this.hideFirstValue = (listModel.getAllowEmptySelection() && shouldHideEmptyValueIfPresent);
}
public void deregister()
{
listModel.removeListDataListener(listener);
listModel.deregister();
}
public void register(IValueList vlModel)
{
IValueList newModel = vlModel;
if (newModel instanceof LookupValueList)
{
// We never expect a LookupValueList here, these are not made for listing all values.
// When we get a LookupValueList, this is probably a value list that was used as fallback (we create LookupValueList), replace with real value list.
newModel = ((LookupValueList)newModel).getRealValueList();
}
deregister();
Set<Integer> newSelectedSet = Collections.synchronizedSet(new HashSet<Integer>());
if (selectedSet != null && selectedSet.size() > 0)
{
for (Integer selected : selectedSet)
{
Object obj = getRealElementAt(selected.intValue());
int newRow = newModel.realValueIndexOf(obj);
if (newRow >= 0)
{
if (newRow > 0 && hideFirstValue && newModel.getAllowEmptySelection()) newRow--;
newSelectedSet.add(Integer.valueOf(newRow));
}
}
}
int prevSize = getSize();
if (listModel instanceof SeparatorProcessingValueList) ((SeparatorProcessingValueList)listModel).setWrapped(newModel);
else listModel = newModel;
selectedSet = newSelectedSet;
listModel.addListDataListener(listener);
this.hideFirstValue = (listModel.getAllowEmptySelection() && shouldHideEmptyValueIfPresent);
int currentSize = getSize();
if (currentSize > prevSize)
{
this.fireIntervalAdded(this, prevSize, currentSize - 1);
this.fireContentsChanged(this, 0, prevSize - 1);
}
else if (prevSize != currentSize)
{
this.fireIntervalRemoved(this, currentSize, prevSize - 1);
this.fireContentsChanged(this, 0, currentSize - 1);
}
else
{
this.fireContentsChanged(this, 0, currentSize - 1);
}
}
public Object getRealElementAt(int row)
{
if (hideFirstValue) return listModel.getRealElementAt(row + 1);
return listModel.getRealElementAt(row);
}
/**
* @return
*/
public boolean hasRealValues()
{
return listModel.hasRealValues();
}
public int realValueIndexOf(Object obj)
{
int i = listModel.realValueIndexOf(obj);
if (hideFirstValue && i != -1) i--;
return i;
}
public String getRelationName()
{
return listModel.getRelationName();
}
public void fill(IRecordInternal ps)
{
this.parentState = ps;
if (relatedRecord != null)
{
relatedRecord.removeModificationListener(this);
relatedRecord = null;
}
boolean fireSelectionChange = false;
if (selectedSet != null && selectedSet.size() > 0)
{
selectedSet.clear();
fireSelectionChange = true;
}
Object obj = getSelectedItem();
if (relatedFoundsetLookup == null || ps == null)
{
listModel.fill(ps);
}
else
{
IFoundSetInternal relatedFoundSet = ps.getRelatedFoundSet(relatedFoundsetLookup);
if (relatedFoundSet == null || relatedFoundSet.getSize() == 0)
{
listModel.fill(null);
}
else
{
relatedRecord = relatedFoundSet.getRecord(relatedFoundSet.getSelectedIndex());
if (relatedRecord != null) relatedRecord.addModificationListener(this);
listModel.fill(relatedRecord);
}
}
if (obj != null)
{
if (listModel.indexOf(obj) == -1 && hasRealValues())
{
selectedObject = null;
realSelectedObject = null;
fireSelectionChange = true;
}
else
{
selectedObject = null;
realSelectedObject = null;
setSelectedItem(obj);
}
}
// fire selection change after state is changed
if (fireSelectionChange)
{
fireContentsChanged(this, -1, -1);
}
}
/*
* (non-Javadoc)
*
* @see com.servoy.j2db.dataprocessing.IModificationListener#valueChanged(com.servoy.j2db.dataprocessing.ModificationEvent)
*/
public void valueChanged(ModificationEvent e)
{
valueListChanging = true;
try
{
fill(parentState);
}
finally
{
valueListChanging = false;
}
}
/**
* @param dataProviderID the dataProviderID to set
*/
public void setDataProviderID(String dataProviderID)
{
int index = (dataProviderID == null || ScopesUtils.isVariableScope(dataProviderID)) ? -1 : dataProviderID.lastIndexOf('.');
if (index != -1)
{
this.relatedFoundsetLookup = dataProviderID.substring(0, index);
}
else
{
this.relatedFoundsetLookup = null;
}
}
/**
* Set the real selected object, in case the listModel currently does not have a mapping for the real value, save the real value for a listmodel change.
* @param realSelectedObject
*/
public void setRealSelectedObject(Object realSelectedObject)
{
this.realSelectedObject = realSelectedObject;
}
public void setSelectedItem(Object anObject)
{
setSelectedItem(anObject, false);
}
private void setSelectedItem(Object anObject, boolean onlyUpdateValues)
{
if ((selectedObject != null && !selectedObject.equals(anObject)) || (selectedObject == null && anObject != null))
{
selectedObject = anObject;
int listIndex = listModel.indexOf(selectedObject);
if (!onlyUpdateValues)
{
Set<Integer> srows = getSelectedRows();
srows.clear();
int index = listIndex;
if (hideFirstValue) index--;
if (index >= 0)
{
srows.add(Integer.valueOf(index));
}
}
realSelectedObject = selectedObject;
if (selectedObject != null && listIndex != -1)
{
realSelectedObject = listModel.getRealElementAt(listIndex);
}
if (!onlyUpdateValues) fireContentsChanged(this, -1, -1);
}
}
public Object getSelectedItem()
{
return selectedObject;
}
public Object getRealSelectedItem()
{
return realSelectedObject;
}
/*
* (non-Javadoc)
*
* @see javax.swing.ListModel#getSize()
*/
public int getSize()
{
return size();
}
public E getElementAt(int index)
{
int idx = index;
if (hideFirstValue) idx++;
if (idx < listModel.getSize()) return (E)listModel.getElementAt(idx);
return null;
}
public boolean isCellEditable(int rowIndex)
{
return true;
}
public void setElementAt(Object aValue, int rowIndex)
{
setElementAt(aValue, rowIndex, listModel.getAllowEmptySelection());
}
/**
* @param false1
* @param i
* @param b
*/
public void setElementAt(Object aValue, int rowIndex, boolean allowEmptySelection)
{
// this index is the ui index (so 0 is 1 in the list model if hideFirstValue = true)
Integer i = new Integer(rowIndex);
getSelectedRows();//to be sure it present
boolean b = ((Boolean)aValue).booleanValue();
if (b)
{
if (selectedSet.contains(i))
{
// not changed return.
return;
}
if (!multiValueSelect && selectedSet.size() > 0)
{
selectedSet.clear();
}
selectedSet.add(i);
}
else
{
if (selectedSet.size() == 1 && selectedSet.contains(i) && !allowEmptySelection)
{
// not allowed.
return;
}
if (!selectedSet.contains(i))
{
// not changed return.
return;
}
selectedSet.remove(i);
}
setSelectedItem(selectedSet.size() > 0 ? getElementAt(selectedSet.iterator().next().intValue()) : null, true);
// when firing tmp remove the listener from the relatedRecord
// so that we don't get a modification change from our own fire.
IRecordInternal tmp = null;
if (relatedRecord != null)
{
relatedRecord.removeModificationListener(this);
tmp = relatedRecord;
relatedRecord = null;
}
// data changed
fireContentsChanged(this, rowIndex, rowIndex);
// selection changed
fireContentsChanged(this, -1, -1);
if (tmp != null && relatedRecord == null)
{
relatedRecord = tmp;
relatedRecord.addModificationListener(this);
}
}
public boolean isRowSelected(int index)
{
if (selectedSet == null)
{
return false;
}
Integer i = new Integer(index);
return selectedSet.contains(i);
}
protected boolean multiValueSelect;
public void setMultiValueSelect(boolean b)
{
multiValueSelect = b;
}
public int getSelectedRow()
{
if (selectedSet != null && selectedSet.size() > 0)
{
return selectedSet.iterator().next().intValue();
}
return -1;
}
public Set<Integer> getSelectedRows()
{
if (selectedSet == null)
{
selectedSet = Collections.synchronizedSet(new HashSet<Integer>());
}
return selectedSet;
}
protected class ListModelListener implements ListDataListener
{
public void intervalAdded(ListDataEvent e)
{
valueListChanging = true;
try
{
if (hideFirstValue)
{
if (e.getIndex0() != 0 || e.getIndex1() != 0)
{
//avoid getting a -1 index value when index0 or index1 = 0
fireIntervalAdded(this, Math.max(0, e.getIndex0() - 1), Math.max(0, e.getIndex1() - 1));
}
//if both index0 and index1 are 0, no action needed
}
else
{
fireIntervalAdded(this, e.getIndex0(), e.getIndex1());
}
}
finally
{
valueListChanging = false;
}
}
public void intervalRemoved(ListDataEvent e)
{
valueListChanging = true;
try
{
if (hideFirstValue)
{
if (e.getIndex0() != 0 || e.getIndex1() != 0)
{
//avoid getting a -1 index value when index0 or index1 = 0
fireIntervalRemoved(this, Math.max(0, e.getIndex0() - 1), Math.max(0, e.getIndex1() - 1));
}
//if both index0 and index1 are 0, no action needed
}
else
{
fireIntervalRemoved(this, e.getIndex0(), e.getIndex1());
}
}
finally
{
valueListChanging = false;
}
}
public void contentsChanged(ListDataEvent e)
{
valueListChanging = true;
try
{
if (hasRealValues() && realSelectedObject != null)
{
int index = listModel.realValueIndexOf(realSelectedObject);
if (index == -1)
{
setSelectedItem(null);
}
else
{
setSelectedItem(getElementAt(index));
}
}
if (hideFirstValue)
{
if (e.getIndex0() != 0 || e.getIndex1() != 0)
{
//avoid getting a -1 index value when index0 or index1 = 0
fireContentsChanged(this, Math.max(0, e.getIndex0() - 1), Math.max(0, e.getIndex1() - 1));
}
//if both index0 and index1 are 0, no action needed
}
else
{
fireContentsChanged(this, e.getIndex0(), e.getIndex1());
}
}
finally
{
valueListChanging = false;
}
}
}
/**
* @return Returns the valueListChanging.
*/
public boolean isValueListChanging()
{
return valueListChanging;
}
public String getName()
{
return listModel.getName();
}
public int getValueType()
{
IValueList list = (listModel instanceof SeparatorProcessingValueList) ? ((SeparatorProcessingValueList)listModel).getWrapped() : listModel;
if (list instanceof CustomValueList)
{
return ((CustomValueList)list).getValueType();
}
return 0;
}
/*
* _____________________________________________________________ Methods for java.util.List
*/
public void add(int index, Object element)
{
throw new UnsupportedOperationException("add not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean add(Object o)
{
throw new UnsupportedOperationException("add not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean addAll(Collection< ? extends E> c)
{
throw new UnsupportedOperationException("add not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean addAll(int index, Collection< ? extends E> c)
{
throw new UnsupportedOperationException("add not supported on valuelist wrapper"); //$NON-NLS-1$
}
public void clear()
{
throw new UnsupportedOperationException("clear not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean contains(Object o)
{
return listModel.indexOf(o) != -1;
}
public boolean containsAll(Collection< ? > c)
{
throw new UnsupportedOperationException("containsAll not supported on valuelist wrapper"); //$NON-NLS-1$
}
public E get(int index)
{
return getElementAt(index);
}
public int indexOf(Object o)
{
int index = listModel.indexOf(o);
if (hideFirstValue) index--;
return index;
}
public boolean isEmpty()
{
int size = listModel.getSize();
if (hideFirstValue && size > 0) size--;
return (size == 0);
}
public Iterator<E> iterator()
{
throw new UnsupportedOperationException("iterator not supported on valuelist wrapper"); //$NON-NLS-1$
}
public int lastIndexOf(Object o)
{
throw new UnsupportedOperationException("lastIndexOf not supported on valuelist wrapper"); //$NON-NLS-1$
}
public ListIterator<E> listIterator()
{
throw new UnsupportedOperationException("iterator not supported on valuelist wrapper"); //$NON-NLS-1$
}
public ListIterator<E> listIterator(int index)
{
throw new UnsupportedOperationException("iterator not supported on valuelist wrapper"); //$NON-NLS-1$
}
public E remove(int index)
{
throw new UnsupportedOperationException("remove not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean remove(Object o)
{
throw new UnsupportedOperationException("remove not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean removeAll(Collection< ? > c)
{
throw new UnsupportedOperationException("removeall not supported on valuelist wrapper"); //$NON-NLS-1$
}
public boolean retainAll(Collection< ? > c)
{
throw new UnsupportedOperationException("retainAll not supported on valuelist wrapper"); //$NON-NLS-1$
}
public E set(int index, E element)
{
throw new UnsupportedOperationException("set not supported on valuelist wrapper"); //$NON-NLS-1$
}
public int size()
{
int size = listModel.getSize();
if (hideFirstValue && size > 0) size--;
return size;
}
public List<E> subList(int fromIndex, int toIndex)
{
throw new UnsupportedOperationException("subList not supported on valuelist wrapper"); //$NON-NLS-1$
}
public Object[] toArray()
{
throw new UnsupportedOperationException("toArray not supported on valuelist wrapper"); //$NON-NLS-1$
}
public <T> T[] toArray(T[] a)
{
throw new UnsupportedOperationException("toArray not supported on valuelist wrapper"); //$NON-NLS-1$
}
/**
* A wrapper for valuelists that helps combos deal with situations where "-" should be a valid value in the valuelist (not just a separator) and should also reach the dataprovider this way (and not as \-).
* It converts the "-" that is interpreted as separator into a separator Object and the "\-" that is supposed to be interpreted as a real "-" to "-".
*
* "-" is only considered a separator if present as display value in the valuelist.
*/
private static class SeparatorProcessingValueList implements IValueList
{
private IValueList wrapped;
public SeparatorProcessingValueList(IValueList listModel)
{
this.wrapped = listModel;
}
/**
* @return the wrapped
*/
public IValueList getWrapped()
{
return wrapped;
}
/**
* @param wrapped the wrapped to set
*/
public void setWrapped(IValueList wrapped)
{
this.wrapped = wrapped;
}
private Object convertToRuntimeSeparator(Object o)
{
if (SEPARATOR_DESIGN_VALUE.equals(o)) return SEPARATOR;
if (ESCAPED_SEPARATOR_DESIGN_VALUE.equals(o)) return SEPARATOR_DESIGN_VALUE;
return o;
}
private Object convertFromRuntimeSeparator(Object o)
{
if (SEPARATOR.equals(o)) return SEPARATOR_DESIGN_VALUE;
if (SEPARATOR_DESIGN_VALUE.equals(o)) return ESCAPED_SEPARATOR_DESIGN_VALUE;
return o;
}
public Object getRealElementAt(int row)
{
// if list does not have real values, then the real values will be display values... so separator affects those
return wrapped.hasRealValues() ? wrapped.getRealElementAt(row) : convertToRuntimeSeparator(wrapped.getRealElementAt(row));
}
public int realValueIndexOf(Object o)
{
// if list does not have real values, then the real values will be display values... so separator affects those
return wrapped.hasRealValues() ? wrapped.realValueIndexOf(o) : wrapped.realValueIndexOf(convertFromRuntimeSeparator(o));
}
public int indexOf(Object o)
{
return wrapped.indexOf(convertFromRuntimeSeparator(o));
}
public Object getElementAt(int index)
{
return convertToRuntimeSeparator(wrapped.getElementAt(index));
}
// the rest of the methods are forwarded to listModel
// --------------------------------------------------
public void setFallbackValueList(IValueList list)
{
wrapped.setFallbackValueList(list);
}
public void addListDataListener(ListDataListener l)
{
wrapped.addListDataListener(l);
}
public String getRelationName()
{
return wrapped.getRelationName();
}
public boolean hasRealValues()
{
return wrapped.hasRealValues();
}
public int getSize()
{
return wrapped.getSize();
}
public void removeListDataListener(ListDataListener l)
{
wrapped.removeListDataListener(l);
}
public void deregister()
{
wrapped.deregister();
}
public void fill(IRecordInternal rec)
{
wrapped.fill(rec);
}
public boolean getAllowEmptySelection()
{
return wrapped.getAllowEmptySelection();
}
public IValueList getFallbackValueList()
{
return wrapped.getFallbackValueList();
}
public String getName()
{
return wrapped.getName();
}
public ValueList getValueList()
{
return wrapped.getValueList();
}
@Override
public IDataProvider[] getDependedDataProviders()
{
return wrapped.getDependedDataProviders();
}
}
}