package org.marketcetera.photon.internal.positions.ui.glazed;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;
import org.marketcetera.util.misc.ClassVersion;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.glazedlists.impl.adt.Barcode;
import ca.odell.glazedlists.swt.EventTableViewer;
import ca.odell.glazedlists.swt.GlazedListsSWT;
/* $License$ */
/**
* A view helper that displays an EventList in an SWT table.
*
* <p>This class is not thread safe. It must be used exclusively with the SWT
* event handler thread.
*
* Note this only supports SWT.VIRTUAL trees at this point.
*
* Derived from {@link EventTableViewer}.
*
* @see EventTableViewer
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
* @author <a href="mailto:will.horn@gmail.com">Will Horn</a>
*/
@ClassVersion("$Id: EventTreeViewer.java 16154 2012-07-14 16:34:05Z colin $")
public class EventTreeViewer<E> implements ListEventListener<E> {
/** the heavyweight tree */
private Tree tree;
/** the proxy that moves events to the SWT user interface thread */
protected TransformedList<E,E> swtThreadSource;
/** the actual EventList to which this EventTableViewer is listening */
protected EventList<E> source;
/** to determine Tree structure */
private EventTreeModel<E> treeModel;
/** to manipulate Trees in a generic way */
private TreeHandler<E> treeHandler;
/** Specifies how to render table headers and sort */
private TableFormat<? super E> tableFormat;
/** Specifies how to render column values represented by TreeItems. */
private TreeItemConfigurer<? super E> treeItemConfigurer;
/**
* Creates a new viewer for the given {@link Tree} that updates the tree
* contents in response to changes on the specified {@link EventList}. The
* {@link Tree} is formatted with the specified {@link TableFormat}.
*
* @param source the EventList that provides the row objects
* @param tree the Tree viewing the source objects
* @param tableFormat the object responsible for extracting column data
* from the row objects
* @throws IllegalArgumentException if any parameters are null
*/
@SuppressWarnings("unchecked")
public EventTreeViewer(EventList<E> source, Tree tree, TableFormat<? super E> tableFormat, EventTreeModel<E> treeModel) {
this(source, tree, tableFormat, treeModel, TreeItemConfigurer.DEFAULT);
}
/**
* Creates a new viewer for the given {@link Tree} that updates the tree
* contents in response to changes on the specified {@link EventList}. The
* {@link Tree} is formatted with the specified {@link TableFormat}.
*
* @param source the EventList that provides the row objects
* @param tree the Tree viewing the source objects
* @param tableFormat the object responsible for extracting column data
* from the row objects
* @param treeItemConfigurer responsible for configuring table items
* @throws IllegalArgumentException if any parameters are null
*/
public EventTreeViewer(EventList<E> source, Tree tree, TableFormat<? super E> tableFormat, EventTreeModel<E> treeModel,
TreeItemConfigurer<? super E> treeItemConfigurer) {
// check for valid arguments early
if (source == null)
throw new IllegalArgumentException("source list may not be null"); //$NON-NLS-1$
if (tree == null)
throw new IllegalArgumentException("Tree may not be null"); //$NON-NLS-1$
if (tableFormat == null)
throw new IllegalArgumentException("TableFormat may not be null"); //$NON-NLS-1$
if (treeModel == null)
throw new IllegalArgumentException("TreeModel may not be null"); //$NON-NLS-1$
if (treeItemConfigurer == null)
throw new IllegalArgumentException("TreeItemConfigurer may not be null"); //$NON-NLS-1$
// lock the source list for reading since we want to prevent writes
// from occurring until we fully initialize this EventTableViewer
source.getReadWriteLock().writeLock().lock();
try {
// insert a list to move ListEvents to the SWT event dispatch thread
final TransformedList<E,E> decorated = createSwtThreadProxyList(source, tree.getDisplay());
if (decorated != null && decorated != source)
this.source = swtThreadSource = decorated;
else
this.source = source;
this.tree = tree;
this.tableFormat = tableFormat;
this.treeModel = treeModel;
this.treeItemConfigurer = treeItemConfigurer;
// configure how the Table will be manipulated
if(isTreeVirtual()) {
treeHandler = new VirtualTreeHandler();
} else {
// only virtual supported now
throw new UnsupportedOperationException();
}
// setup the Table with initial values
initTree();
treeHandler.populateTree();
// prepare listeners
this.source.addListEventListener(this);
} finally {
source.getReadWriteLock().writeLock().unlock();
}
}
/**
* This method exists as a hook for subclasses that may have custom
* threading needs within their EventTreeViewers. By default, this method
* will wrap the given <code>source</code> in a SWTThreadProxyList if it
* is not already a SWTThreadProxyList. Subclasses may replace this logic
* and return either a custom ThreadProxyEventList of their choosing, or
* return <code>null</code> or the <code>source</code> unchanged in order
* to indicate that <strong>NO</strong> 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 <code>null</code> or the
* <code>source</code> unchanged to indicate that <strong>NO</strong>
* Thread-proxying is desired
*/
protected TransformedList<E,E> createSwtThreadProxyList(EventList<E> source, Display display) {
return GlazedListsSWT.isSWTThreadProxyList(source) ? null : GlazedListsSWT.swtThreadProxyList(source, display);
}
/**
* Gets whether the tree is virtual or not.
*/
private boolean isTreeVirtual() {
return ((tree.getStyle() & SWT.VIRTUAL) == SWT.VIRTUAL);
}
/**
* Builds the columns and headers for the {@link Tree}
*/
private void initTree() {
tree.setHeaderVisible(true);
final TreeColumnConfigurer configurer = getTableColumnConfigurer();
for(int c = 0; c < tableFormat.getColumnCount(); c++) {
TreeColumn column = new TreeColumn(tree, SWT.LEFT, c);
column.setText(tableFormat.getColumnName(c));
column.setWidth(80);
if (configurer != null) {
configurer.configure(column, c);
}
}
}
/**
* Sets all of the column values on a {@link TreeItem}.
*/
private void renderTreeItem(TreeItem item, E value, int row) {
for(int i = 0; i < tableFormat.getColumnCount(); i++) {
final Object cellValue = tableFormat.getColumnValue(value, i);
treeItemConfigurer.configure(item, value, cellValue, row, i);
}
item.setData(value);
EventList<E> children = treeModel.getChildren(value);
if (children != null) {
item.setItemCount(children.size());
}
}
/**
* Gets the {@link TableFormat}.
*/
public TableFormat<? super E> getTableFormat() {
return tableFormat;
}
/**
* Sets this {@link Table} to be formatted by a different
* {@link TableFormat}. This method is not yet implemented for SWT.
*/
public void setTableFormat(TableFormat<E> tableFormat) {
throw new UnsupportedOperationException();
}
/**
* Gets the {@link TreeItemConfigurer}.
*/
public TreeItemConfigurer<? super E> getTreeItemConfigurer() {
return treeItemConfigurer;
}
/**
* Sets a new {@link TreeItemConfigurer}. The cell values of existing,
* non-virtual tree items will be reconfigured with the specified configurer.
*/
public void setTreeItemConfigurer(TreeItemConfigurer<? super E> treeItemConfigurer) {
if (treeItemConfigurer == null)
throw new IllegalArgumentException("TreeItemConfigurer may not be null"); //$NON-NLS-1$
this.treeItemConfigurer = treeItemConfigurer;
// determine the index of the last, non-virtual table item
final int maxIndex = treeHandler.getLastIndex();
if (maxIndex < 0) return;
// Disable redraws so that the table is updated in bulk
tree.setRedraw(false);
source.getReadWriteLock().readLock().lock();
try {
// reprocess all table items between indexes 0 and maxIndex
for (int i = 0; i <= maxIndex; i++) {
final E rowValue = source.get(i);
renderTreeItem(tree.getItem(i), rowValue, i);
}
} finally {
source.getReadWriteLock().readLock().unlock();
}
// Re-enable redraws to update the table
tree.setRedraw(true);
}
/**
* Gets the {@link TreeColumnConfigurer} or <code>null</code> if not
* available.
*/
private TreeColumnConfigurer getTableColumnConfigurer() {
if (tableFormat instanceof TreeColumnConfigurer) {
return (TreeColumnConfigurer) tableFormat;
}
return null;
}
/**
* Gets the {@link Tree} that is being managed by this
* {@link EventTreeViewer}.
*/
public Tree getTree() {
return tree;
}
/**
* Get the source of this {@link EventTreeViewer}.
*/
public EventList<E> getSourceList() {
return source;
}
/**
* When the source list is changed, this forwards the change to the
* displayed {@link Tree}.
*/
public void listChanged(ListEvent<E> listChanges) {
// if the tree is no longer available, we don't want to do anything as
// it will result in a "Widget is disposed" exception
if (tree.isDisposed()) return;
// Disable redraws so that the table is updated in bulk
tree.setRedraw(false);
// Apply changes to the list
while (listChanges.next()) {
int changeIndex = listChanges.getIndex();
int changeType = listChanges.getType();
if (changeType == ListEvent.INSERT) {
treeHandler.addRow(changeIndex, source.get(changeIndex));
} else if (changeType == ListEvent.UPDATE) {
treeHandler.updateRow(changeIndex, source.get(changeIndex));
} else if (changeType == ListEvent.DELETE) {
treeHandler.removeRow(changeIndex);
}
}
// Re-enable redraws to update the tree
tree.setRedraw(true);
}
/**
* Releases the resources consumed by this {@link EventTreeViewer} so that it
* may eventually be garbage collected.
*
* <p>An {@link EventTreeViewer} 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 EventTreeViewer}
* to be garbage collected before its source {@link EventList}. This is
* necessary for situations where an {@link EventTreeViewer} is short-lived but
* its source {@link EventList} is long-lived.
*
* <p><strong><font color="#FF0000">Warning:</font></strong> It is an error
* to call any method on a {@link EventTreeViewer} after it has been disposed.
*/
public void dispose() {
treeHandler.dispose();
if (swtThreadSource != null)
swtThreadSource.dispose();
source.removeListEventListener(this);
// this encourages exceptions to be thrown if this model is incorrectly accessed again
swtThreadSource = null;
treeHandler = null;
source = null;
}
/**
* Defines how Trees will be manipulated.
*/
private interface TreeHandler<E> {
/**
* Populate the Tree with data.
*/
public void populateTree();
/**
* Add a row with the given value.
*/
public void addRow(int row, E value);
/**
* Update a row with the given value.
*/
public void updateRow(int row, E value);
/**
* Removes a row.
*/
public void removeRow(int row);
/**
* Disposes of this TreeHandler
*/
public void dispose();
/**
* Gets the last real, non-virtual row index. -1 means empty or
* completely virtual tree
*/
public int getLastIndex();
}
/**
* Allows manipulation of Virtual Trees and handles additional aspects
* like providing the SetData callback method and tracking which values
* are Virtual.
*/
private final class VirtualTreeHandler implements TreeHandler<E>, Listener {
/** to keep track of what's been requested */
private final Barcode requested = new Barcode();
/**
* Create a new VirtualTableHandler.
*/
public VirtualTreeHandler() {
requested.addWhite(0, source.size());
tree.addListener(SWT.SetData, this);
}
/**
* Populate the Table with initial data.
*/
public void populateTree() {
tree.setItemCount(source.size());
}
/**
* Adds a row with the given value.
*/
public void addRow(int row, E value) {
// Adding before the last non-Virtual value
if(row <= getLastIndex()) {
requested.addBlack(row, 1);
TreeItem item = new TreeItem(tree, 0, row);
renderTreeItem(item, value, row);
// Adding in the Virtual values at the end
} else {
requested.addWhite(requested.size(), 1);
tree.setItemCount(tree.getItemCount() + 1);
}
}
/**
* Updates a row with the given value.
*/
public void updateRow(int row, E value) {
// Only set a row if it is NOT Virtual
if(!isVirtual(row)) {
requested.setBlack(row, 1);
TreeItem item = tree.getItem(row);
renderTreeItem(item, value, row);
// not terribly efficient, basically refresh all children
item.clearAll(true);
}
}
/**
* Removes a row.
*/
public void removeRow(int row) {
// Sync the requested barcode to clear values that have been removed
requested.remove(row, 1);
tree.getItem(row).dispose();
}
/**
* Returns the highest index that has been requested or -1 if the
* Table is entirely Virtual.
*/
public int getLastIndex() {
// Everything is Virtual
if(requested.blackSize() == 0) return -1;
// Return the last index
else return requested.getIndex(requested.blackSize() - 1, Barcode.BLACK);
}
/**
* Returns whether a particular row is Virtual in the Table.
*/
private boolean isVirtual(int rowIndex) {
return requested.getBlackIndex(rowIndex) == -1;
}
/**
* Respond to requests for values to fill Virtual rows.
*/
@SuppressWarnings("unchecked")
public void handleEvent(Event e) {
// Get the TreeItem from the Tree
TreeItem item = (TreeItem)e.item;
// Set the value on the Virtual element
requested.setBlack(e.index, 1);
TreeItem parentItem = item.getParentItem();
EventList<E> list;
if (parentItem == null) {
// root level
list = source;
} else {
list = treeModel.getChildren((E) parentItem.getData());
}
E value = list.get(e.index);
renderTreeItem(item, value, e.index);
}
/**
* Allows this handler to clean up after itself.
*/
public void dispose() {
tree.removeListener(SWT.SetData, this);
}
}
}