source.ca.odell.glazedlists.swing.EventTableModel Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.swing;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.gui.WritableTableFormat;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
/**
* A {@link TableModel} that holds an {@link EventList}. Each element of the list
* corresponds to a row in the {@link TableModel}. The columns of the table are
* specified using a {@link TableFormat}.
*
* The EventTableModel class is not thread-safe. Unless otherwise
* noted, all methods are only safe to be called from the event dispatch thread.
* To do this programmatically, use {@link SwingUtilities#invokeAndWait(Runnable)}.
*
* @see Glazed Lists Tutorial
*
* @see SwingUtilities#invokeAndWait(Runnable)
* @see Bug 112
* @see Bug 146
* @see Bug 177
*
* @author Jesse Wilson
*/
public class EventTableModel extends AbstractTableModel implements ListEventListener {
/** the proxy moves events to the Swing Event Dispatch thread */
protected TransformedList swingThreadSource;
/** the source of data for this TableModel, which may or may not be {@link #swingThreadSource} */
protected EventList source;
/** specifies how column data is extracted from each row object */
private TableFormat super E> tableFormat;
/** reusable TableModelEvent for broadcasting changes */
private final MutableTableModelEvent tableModelEvent = new MutableTableModelEvent(this);
/**
* Creates a new table model that extracts column data from the given
* source
using the the given tableFormat
.
*
* @param source the EventList that provides the row objects
* @param tableFormat the object responsible for extracting column data
* from the row objects
*/
public EventTableModel(EventList source, TableFormat super E> tableFormat) {
// lock the source list for reading since we want to prevent writes
// from occurring until we fully initialize this EventTableModel
source.getReadWriteLock().readLock().lock();
try {
final TransformedList decorated = createSwingThreadProxyList(source);
// if the create method actually returned a decorated form of the source,
// record it so it may later be disposed
if (decorated != null && decorated != source)
this.source = swingThreadSource = decorated;
else
this.source = source;
this.tableFormat = tableFormat;
// prepare listeners
this.source.addListEventListener(this);
} finally {
source.getReadWriteLock().readLock().unlock();
}
}
/**
* Creates a new table that renders the specified list with an automatically
* generated {@link TableFormat}. It uses JavaBeans and reflection to create
* a {@link TableFormat} as specified.
*
* Note that the classes which will be obfuscated may not work with
* reflection. In this case, implement a {@link TableFormat} manually.
*
* @param source the EventList that provides the row objects
* @param propertyNames an array of property names in the JavaBeans format.
* For example, if your list contains Objects with the methods getFirstName(),
* setFirstName(String), getAge(), setAge(Integer), then this array should
* contain the two strings "firstName" and "age". This format is specified
* by the JavaBeans {@link java.beans.PropertyDescriptor}.
* @param columnLabels the corresponding column names for the listed property
* names. For example, if your columns are "firstName" and "age", then
* your labels might be "First Name" and "Age".
* @param writable an array of booleans specifying which of the columns in
* your table are writable.
*/
public EventTableModel(EventList source, String[] propertyNames, String[] columnLabels, boolean[] writable) {
this(source, GlazedLists.tableFormat(propertyNames, columnLabels, writable));
}
/**
* This method exists as a hook for subclasses that may have custom
* threading needs within their EventTableModels. By default, this method
* will wrap the given source
in a SwingThreadProxyList if it
* is not already a SwingThreadProxyList. Subclasses may replace this logic
* and return either a custom ThreadProxyEventList of their choosing, or
* return null
or the source
unchanged in order
* to indicate that NO ThreadProxyEventList is desired.
* In these cases it is expected that some external mechanism will ensure
* that threading is handled correctly.
*
* @param source the EventList that provides the row objects
* @return the source wrapped in some sort of ThreadProxyEventList if
* Thread-proxying is desired, or either null
or the
* source
unchanged to indicate that NO
* Thread-proxying is desired
*/
protected TransformedList createSwingThreadProxyList(EventList source) {
return GlazedListsSwing.isSwingThreadProxyList(source) ? null : GlazedListsSwing.swingThreadProxyList(source);
}
/**
* Gets the Table Format.
*/
public TableFormat super E> getTableFormat() {
return tableFormat;
}
/**
* Sets the {@link TableFormat} that will extract column data from each
* element. This has some very important consequences. Any cell selections
* will be lost - this is due to the fact that the TableFormats may have
* different numbers of columns, and JTable has no event to specify columns
* changing without rows.
*/
public void setTableFormat(TableFormat tableFormat) {
this.tableFormat = tableFormat;
tableModelEvent.setStructureChanged();
fireTableChanged(tableModelEvent);
}
/**
* Retrieves the value at the specified location from the table.
*
* This may be used by renderers to paint the cells of a row differently
* based on the entire value for that row.
*
* @see #getValueAt(int,int)
*/
public E getElementAt(int index) {
source.getReadWriteLock().readLock().lock();
try {
return source.get(index);
} finally {
source.getReadWriteLock().readLock().unlock();
}
}
/**
* For implementing the ListEventListener interface. This sends changes
* to the table which repaints the table cells. Because this class is
* backed by {@link GlazedListsSwing#swingThreadProxyList}, all natural
* calls to this method are guaranteed to occur on the Swing EDT.
*/
public void listChanged(ListEvent listChanges) {
handleListChange(listChanges);
}
/**
* Default implementation for converting a {@link ListEvent} to
* TableModelEvents. There will be one TableModelEvent per ListEvent block.
* Subclasses may choose to implement a different conversion.
*
* @param listChanges ListEvent to translate
*/
protected void handleListChange(ListEvent listChanges) {
// for all changes, one block at a time
while (listChanges.nextBlock()) {
// get the current change info
int startIndex = listChanges.getBlockStartIndex();
int endIndex = listChanges.getBlockEndIndex();
int changeType = listChanges.getType();
// create a table model event for this block
tableModelEvent.setValues(startIndex, endIndex, changeType);
fireTableChanged(tableModelEvent);
}
}
/**
* @return reusable TableModelEvent for broadcasting changes
*/
protected final MutableTableModelEvent getMutableTableModelEvent() {
return tableModelEvent;
}
/**
* Fetch the name for the specified column.
*/
public String getColumnName(int column) {
return tableFormat.getColumnName(column);
}
/**
* The number of rows equals the number of entries in the source event list.
*/
public int getRowCount() {
source.getReadWriteLock().readLock().lock();
try {
return source.size();
} finally {
source.getReadWriteLock().readLock().unlock();
}
}
/**
* Get the column count as specified by the table format.
*/
public int getColumnCount() {
return tableFormat.getColumnCount();
}
/**
* Gets the class of elements in the specified column. This behaviour can be
* customized by implementing the {@link AdvancedTableFormat} interface.
*/
public Class getColumnClass(int columnIndex) {
// See if the TableFormat is specifies a column class
if(tableFormat instanceof AdvancedTableFormat) {
return ((AdvancedTableFormat)tableFormat).getColumnClass(columnIndex);
// If not, use the default...
} else {
return super.getColumnClass(columnIndex);
}
}
/**
* Retrieves the value at the specified location of the table.
*/
public Object getValueAt(int row, int column) {
source.getReadWriteLock().readLock().lock();
try {
return tableFormat.getColumnValue(source.get(row), column);
} finally {
source.getReadWriteLock().readLock().unlock();
}
}
/**
* Delegates the question of whether the cell is editable or not to the
* backing TableFormat if it is a {@link WritableTableFormat}. Otherwise,
* the column is assumed to be uneditable.
*/
public boolean isCellEditable(int row, int column) {
if (!(tableFormat instanceof WritableTableFormat))
return false;
source.getReadWriteLock().readLock().lock();
try {
final E toEdit = source.get(row);
return ((WritableTableFormat) tableFormat).isEditable(toEdit, column);
} finally {
source.getReadWriteLock().readLock().unlock();
}
}
/**
* Attempts to update the object for the given row with the
* editedValue
. This requires the backing TableFormat
* be a {@link WritableTableFormat}. {@link WritableTableFormat#setColumnValue}
* is expected to contain the logic for updating the object at the given
* row
with the editedValue
which was in the
* given column
.
*/
public void setValueAt(Object editedValue, int row, int column) {
// ensure this is a writable table
if (!(tableFormat instanceof WritableTableFormat))
throw new UnsupportedOperationException("Unexpected setValueAt() on read-only table");
source.getReadWriteLock().writeLock().lock();
try {
// get the object being edited from the source list
final E baseObject = source.get(row);
// tell the table format to set the value based
final WritableTableFormat writableTableFormat = (WritableTableFormat) tableFormat;
final E updatedObject = writableTableFormat.setColumnValue(baseObject, editedValue, column);
// if the edit was discarded we have nothing to do
if (updatedObject != null) {
// check if updating the baseObject has caused it to be removed from this
// TableModel (FilterList) or moved to another location (SortedList)
final boolean baseObjectHasNotMoved = row < getRowCount() && source.get(row) == baseObject;
// if the row is still present in its original location, update it to induce a
// TableModelEvent that will redraw that row in the table
if (baseObjectHasNotMoved)
source.set(row, updatedObject);
}
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* Releases the resources consumed by this {@link EventTableModel} so that it
* may eventually be garbage collected.
*
* An {@link EventTableModel} will be garbage collected without a call to
* {@link #dispose()}, but not before its source {@link EventList} is garbage
* collected. By calling {@link #dispose()}, you allow the {@link EventTableModel}
* to be garbage collected before its source {@link EventList}. This is
* necessary for situations where an {@link EventTableModel} is short-lived but
* its source {@link EventList} is long-lived.
*
*
Warning: It is an error
* to call any method on an {@link EventTableModel} after it has been disposed.
* As such, this {@link EventTableModel} should be detached from its
* corresponding Component before it is disposed.
*/
public void dispose() {
source.removeListEventListener(this);
// if we created the swingThreadSource then we must also dispose it
if (swingThreadSource != null)
swingThreadSource.dispose();
// this encourages exceptions to be thrown if this model is incorrectly accessed again
swingThreadSource = null;
source = null;
}
}