net.sf.cuf.model.ui.MultiSelectionTableModelAdapter Maven / Gradle / Ivy
package net.sf.cuf.model.ui;
import net.sf.cuf.model.AspectAdapter;
import net.sf.cuf.model.DelegateAccess;
import net.sf.cuf.model.MultiSelectionInList;
import net.sf.cuf.model.ui.ListTableMapper.Mapping;
import net.sf.cuf.ui.builder.SwingXMLBuilder;
import net.sf.cuf.ui.table.TableFilterPlugin;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.EventListenerList;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static net.sf.cuf.model.ui.CufTableRowSorter.convertRowIndexToModel;
import static net.sf.cuf.model.ui.CufTableRowSorter.convertRowIndexToView;
import static net.sf.cuf.ui.table.CufSorterUtil.attachCufSorterWithHeaderRenderer;
/**
* This class maps the content of List of a {@link MultiSelectionInList} ValueModel to a
* JTable TableModel and ListSelectionModel.
* This is the multi selection version of the {@link ListTableMapper}.
*
* Each entry in the list of the {@link MultiSelectionInList} ValueModel describes a
* row, all entries are assumed to be objects from the same class/interface.
* Whenever the either the list or the selection changes, the table is
* adjusted accordingly.
* The table data is read-only, when the user changes the selection in the
* table, the selection of the {@link MultiSelectionInList} is changed accordingly.
*
* The initial selection is taken from the {@link MultiSelectionInList} value model.
*
* FIXME: currently we never unregister from the MultiSelectionInList value model,
* this might lead to memory leaks.
*/
public class MultiSelectionTableModelAdapter implements TableModel, ListSelectionListener
{
// NOTE:
// Most of the code is copied from ListTableMapper and ListTableMapperBase.
// Everything around the Adapters between combos, lists, tables and value models
// feels like it needs a major overhaul or at least some face lifting.
// Comments in the other classes are not accurate and code is very 'grown'.
/** our JTable, never null */
private JTable mTable;
/** value model holding the list, never null */
protected MultiSelectionInList> mMultiSelectionInList;
/** helper for the TableModel callback handling */
private EventListenerList mListenerList;
/** marker if we are inside a selection change, we then ignore selection
* changes from the "other" model */
private boolean mInSelectionChange;
/** List of Mapping objects, contains a DelegateAccess object
* or each column, never null */
private List mColumnMapping;
/** our table filter plugin */
private TableFilterPlugin mTableFilterPlugin;
/** shared no params array */
private static final Class>[] NO_PARAMS = {};
/** shared no args array */
private static final Object[] NO_ARGS = {};
/**
* Create a new adaption object between a JTable and a {@link MultiSelectionInList} value model.
* This object is also set as the JTable's TableModel.
* @param pTable the table for which we provide TableModel behaviour and connect to the {@link ListSelectionModel}
* @param pValueModel the value model that drives the table (data, selection)
* and gets updated by the table (selection only)
* @param pMapping non-null List containing {@link Mapping} objects
* @param pSortable true if table is sortable
* @throws IllegalArgumentException if a parameter is bogus
*/
public MultiSelectionTableModelAdapter(final JTable pTable, final MultiSelectionInList> pValueModel, final List pMapping, final boolean pSortable)
{
this(pTable, pValueModel, pMapping, pSortable, -1);
}
/**
* Create a new adaption object between a JTable and a {@link MultiSelectionInList} value model.
* This object is also set as the JTable's TableModel.
* @param pTable the table for which we provide TableModel behaviour and connect to the {@link ListSelectionModel}
* @param pValueModel the value model that drives the table (data, selection)
* and gets updated by the table (selection only)
* @param pMapping non-null List containing {@link Mapping} objects
* @param pSortable true if table is sortable
* @param pColumnForInitialSorting -1 or the column index for initial sorting
* @throws IllegalArgumentException if a parameter is bogus
*/
public MultiSelectionTableModelAdapter(final JTable pTable,
final MultiSelectionInList> pValueModel,
final List pMapping,
final boolean pSortable,
final int pColumnForInitialSorting)
{
this(pTable, pValueModel, pMapping, pSortable, pColumnForInitialSorting, null);
}
/**
* Create a new adaption object between a JTable and a {@link MultiSelectionInList} value model.
* This object is also set as the JTable's TableModel.
* @param pTable the table for which we provide TableModel behaviour and connect to the {@link ListSelectionModel}
* @param pValueModel the value model that drives the table (data, selection)
* and gets updated by the table (selection only)
* @param pMapping non-null List containing {@link Mapping} objects
* @param pSortable true if table is sortable
* @param pColumnForInitialSorting -1 or the column index for initial sorting
* @param pTableFilterPlugin our table filter plugin, may be null
* @throws IllegalArgumentException if a parameter is bogus
*/
public MultiSelectionTableModelAdapter(final JTable pTable,
final MultiSelectionInList> pValueModel,
final List pMapping,
final boolean pSortable,
final int pColumnForInitialSorting,
final TableFilterPlugin pTableFilterPlugin)
{
mTableFilterPlugin= pTableFilterPlugin;
mColumnMapping= Collections.emptyList();
init(pTable, pValueModel);
setColumnMapping(pTable, pMapping, pSortable, pColumnForInitialSorting);
}
/**
* Getter for TableFilterPlugin
* @return mTableFilterPlugin
*/
public TableFilterPlugin getTableFilterPlugin()
{
return mTableFilterPlugin;
}
/*
* "our" handling code (=no Swing or ValueModel callback/handling stuff)
*/
/**
* Common construction stuff, it creates a new adaption between
* a JTable and a {@link MultiSelectionInList} value model.
* This object is also set as the JTable's TableModel.
* @param pTable the table for which we provide TableModel behaviour and connect to the {@link ListSelectionModel}
* @param pValueModel the value model that drives the table (data, selection)
* and gets updated by the table (selection only)
* @throws IllegalArgumentException if a parameter is bogus
*/
protected void init(final JTable pTable, final MultiSelectionInList> pValueModel)
{
if (pTable==null)
{
throw new IllegalArgumentException("the table must not be null");
}
if (pValueModel==null)
{
throw new IllegalArgumentException("the value model must not be null");
}
mTable = pTable;
mMultiSelectionInList = pValueModel;
mListenerList = new EventListenerList();
mInSelectionChange = false;
// this must stay at the end so that everything is setup first
mMultiSelectionInList.onChangeSend (this, "vmDataChanged");
mMultiSelectionInList.getSelectedIndexSetValueModel().onChangeSend(this, "vmSelectionChanged");
pTable.setModel (this);
pTable.getSelectionModel().addListSelectionListener(this);
}
/**
* The method maps the attributes of our List entry class to column names
* in the table and the alignments of the columns.
* @param pTable the table for which we set the column alignments
* @param pMapping non-null List containing {@link Mapping} objects
* @param pSortable true if table is sortable
* @param pColumnForInitialSorting -1 or the column index for initial sorting
* @throws IllegalArgumentException if pMapping is null or contains invalid mappings
*/
private void setColumnMapping(final JTable pTable, final List pMapping, final boolean pSortable, final int pColumnForInitialSorting)
{
if (pMapping==null)
{
throw new IllegalArgumentException("mapping must not be null");
}
List columnMapping= new ArrayList<>(pMapping.size());
for (Mapping mapping : pMapping)
{
Mapping myMapping= new Mapping(mapping);
columnMapping.add(myMapping);
}
mColumnMapping= columnMapping;
// notify the table to re-create the columns
TableModelEvent e= new TableModelEvent(this, TableModelEvent.HEADER_ROW);
fireTableChanged(e);
// make table sortable with a CufTableRowSorter
if (pSortable)
{
// set sorter and header renderer
CufTableRowSorter> sorter = attachCufSorterWithHeaderRenderer(pTable);
// add Comparator for sorting, if necessary
for (int i = 0, n = pMapping.size(); i < n; i++)
{
Mapping mapping= pMapping.get(i);
if (mapping.getComparatorClass() != null)
{
try
{
Comparator> myComparator =
(Comparator>) mapping
.getComparatorClass()
.getConstructor(NO_PARAMS)
.newInstance(NO_ARGS);
sorter.setColumnComparator(i, myComparator);
}
catch (Exception e1)
{
throw SwingXMLBuilder.createException("could not instantiate ComparatorClass "
+ mapping.getComparatorClass() + " for "
+ mapping.getColumnClass(), e1);
}
}
}
// force initial sorting
if (pColumnForInitialSorting >= 0 && pColumnForInitialSorting < pTable.getModel().getColumnCount())
{
sorter.forceInitialSorting(pColumnForInitialSorting);
}
}
for (int i = 0, n = pMapping.size(); i < n; i++)
{
Mapping mapping = pMapping.get(i);
// set defined column alignment
if(mapping.getColumnAlignment() != null)
{
pTable.getColumnModel().getColumn(i).setCellRenderer(
new ColumnAlignmentRenderer(mapping.getColumnAlignment()));
}
else
{
// if no alignment defined: set default-alignment per column-class
if(mapping.getColumnClass() != null
&& Boolean.class.isAssignableFrom(mapping.getColumnClass()))
{
pTable.getColumnModel().getColumn(i).setCellRenderer(
new ColumnAlignmentRenderer(ColumnAlignmentRenderer.ALIGN_CENTER));
}
else if(mapping.getColumnClass() != null
&& (Number.class.isAssignableFrom(mapping.getColumnClass())
|| Date.class.isAssignableFrom(mapping.getColumnClass())))
{
pTable.getColumnModel().getColumn(i).setCellRenderer(
new ColumnAlignmentRenderer(ColumnAlignmentRenderer.ALIGN_RIGHT));
}
else
{
pTable.getColumnModel().getColumn(i).setCellRenderer(
new ColumnAlignmentRenderer(ColumnAlignmentRenderer.ALIGN_LEFT));
}
}
// set prefered width for table columns
if(mapping.getColumnPrefWidthIntValue() > 0)
{
// if we don't disable the auto resize, preferred with has no consequences
pTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JTextField dummyTextField = new JTextField(mapping.getColumnPrefWidthIntValue());
pTable.getColumnModel().getColumn(i).setPreferredWidth(dummyTextField.getPreferredSize().width);
}
}
}
/******************************************************************/
/*
* TableModel callbacks
*/
public int getColumnCount()
{
return mColumnMapping.size();
}
public String getColumnName(final int pColumnIndex)
{
return mColumnMapping.get(pColumnIndex).getColumnTitle();
}
public Class> getColumnClass(final int pColumnIndex)
{
Class> clazz= mColumnMapping.get(pColumnIndex).getColumnClass();
if (clazz==null)
clazz= Object.class;
return clazz;
}
public Object getValueAt(final int pRowIndex, final int pColumnIndex)
{
Mapping mapping = mColumnMapping.get(pColumnIndex);
DelegateAccess valueModel= mapping.getValueModel();
List> list = mMultiSelectionInList.getValue();
Object value = list.get(pRowIndex);
return valueModel.getValue(value);
}
public Object getRawValueAt(final int pRowIndex, final int pColumnIndex)
{
List> list = mMultiSelectionInList.getValue();
return list.get(pRowIndex);
}
public Object getValueForSortingAt(final int pRowIndex, final int pColumnIndex)
{
Mapping mapping = mColumnMapping.get(pColumnIndex);
DelegateAccess valueModelForSorting= mapping.getValueModelForSorting();
List> list = mMultiSelectionInList.getValue();
Object value = list.get(pRowIndex);
return valueModelForSorting.getValue(value);
}
public boolean isColumnSortable(final int pColumnIndex)
{
Mapping mapping= mColumnMapping.get(pColumnIndex);
return mapping.isSortable();
}
/******************************************************************/
/* Event handling code from the model */
/******************************************************************/
/**
* Called whenever the data of the {@link MultiSelectionInList} changes.
* @param pEvent not used
*/
public void vmDataChanged(final ChangeEvent pEvent)
{
// ignore the changes we triggered
if (mInSelectionChange)
{
return;
}
// notify the table to re-read the data, this will clear the selection,
// so we re-set the selection afterwards
mInSelectionChange= true;
try
{
TableModelEvent e= new TableModelEvent(this);
fireTableChanged(e);
updateSelectionFromModel();
}
finally
{
mInSelectionChange= false;
}
}
/**
* Called whenever the selection of the SelectionInList changes.
* @param pEvent not used
*/
public void vmSelectionChanged(final ChangeEvent pEvent)
{
// ignore the changes we triggered
if (mInSelectionChange)
{
return;
}
mInSelectionChange= true;
try
{
updateSelectionFromModel();
}
finally
{
mInSelectionChange= false;
}
}
/**
* Helper method to update the selection in the table
* based on the current selection in the model.
* The call to this method should be wrapped in
* checking {@link #mInSelectionChange}.
*/
private void updateSelectionFromModel()
{
// Note: the code here is helpless when the model selection
// is incompatible with the selection mode of the table.
// (Since it can't correct the selection of the model.)
// Note: because of the internal behaviour of the
// DefaultListSelectionModel it is nearly impossible
// to keep anchor and lead index the same.
// Right now we don't even try.
Set> selectedIndexes = mMultiSelectionInList.getSelectedIndexSetValueModel().getValue();
ListSelectionModel selectionModel = mTable.getSelectionModel();
selectionModel.clearSelection();
for (final Object selectedIndexe : selectedIndexes)
{
Integer selectedIndex = (Integer) selectedIndexe;
int viewIndex = convertRowIndexToView(mTable, selectedIndex);
if (0 <= viewIndex && viewIndex < mTable.getRowCount())
{
selectionModel.addSelectionInterval(viewIndex, viewIndex);
}
}
}
/******************************************************************/
/* Event handling code from the table */
/******************************************************************/
/**
* Called whenever the selection of the JTable changes, we update
* our SelectionInList index.
* @param pEvent event describing the new selection
*/
public void valueChanged(final ListSelectionEvent pEvent)
{
// ignore the changes we triggered
if (mInSelectionChange)
{
return;
}
// ignore intermediate changes
if (pEvent.getValueIsAdjusting())
{
return;
}
mInSelectionChange= true;
try
{
Set selectionIndexSet = new HashSet<>();
int [] selectedViewRows = mTable.getSelectedRows();
for (int selectedViewRow : selectedViewRows)
{
int tableModelIndex = convertRowIndexToModel(mTable, selectedViewRow);
selectionIndexSet.add(tableModelIndex);
}
// now update in one call
mMultiSelectionInList.setSelectedIndexes(selectionIndexSet);
}
finally
{
mInSelectionChange= false;
}
}
/*
* Swing TableModel methods
*/
public int getRowCount()
{
List> list= mMultiSelectionInList.getValue();
return list==null ? 0 : list.size();
}
/**
* Checks if the cell is editable
* @param pRowIndex the row index
* @param pColumnIndex the column index
* @return the value
*/
@Override
public boolean isCellEditable(final int pRowIndex, final int pColumnIndex)
{
final ListTableMapper.Mapping columnMapping= mColumnMapping.get(pColumnIndex);
return columnMapping.isEditable();
}
/**
* Sets value to the cell
* @param pValue the value
* @param pRowIndex the row index
* @param pColumnIndex the column index
*/
@Override
public void setValueAt(final Object pValue, final int pRowIndex, final int pColumnIndex)
{
final List> list = mMultiSelectionInList.getValue();
final Object listEntry = list.get(pRowIndex);
final ListTableMapper.Mapping columnMapping = mColumnMapping.get(pColumnIndex);
// cast will work, columnMapping.isEditable() will otherwise return false
((AspectAdapter)columnMapping.getValueModel()).getAccessAdapter().setValue(listEntry, pValue);
// and now notify the list of the change
mMultiSelectionInList.signalExternalUpdate();
}
public void addTableModelListener(final TableModelListener pTableModelListener)
{
mListenerList.add(TableModelListener.class, pTableModelListener);
}
public void removeTableModelListener(final TableModelListener pTableModelListener)
{
mListenerList.remove(TableModelListener.class, pTableModelListener);
}
protected void fireTableChanged(final TableModelEvent peEvent)
{
Object[] listeners = mListenerList.getListenerList();
for (int i= listeners.length-2; i>=0; i-=2)
{
if (listeners[i]==TableModelListener.class)
{
((TableModelListener)listeners[i+1]).tableChanged(peEvent);
}
}
}
}