/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.swing;
import ca.odell.glazedlists.TreeList;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.event.ListEvent;
import javax.swing.*;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.awt.event.*;
import java.awt.*;
/**
* This class {@link #install}s support for a single hierarchical column within
* a standard {@link JTable} that is backed by an {@link EventTableModel}. The
* {@link EventTableModel}, in turn <strong>must</strong> be backed by the same
* {@link TreeList} which is given as a parameter to {@link #install}.
*
* <p>Typical usage of {@link TreeTableSupport} resembles this:
* <br><br>
* <pre><code>
* // create an EventList of data
* EventList myEventList = ...
*
* // create a TreeList which uses a TreeFormat object to infer a hierarchy for each element in myEventList
* TreeList treeList = new TreeList(myEventList, new MyTreeFormat());
*
* // create a JTable that displays the contents of treeList in a tabular format
* EventTableModel myTableModel = new EventTableModel(treeList, new MyTableFormat());
* JTable myTable = new JTable(myTableModel);
*
* // make the 3rd table column a hierarchical column to create a TreeTable
* TreeTableSupport.install(myTable, treeList, 2);
* </code></pre>
*
* In order to achieve all of the treetable behaviours, the following occurs
* when {@link #install} is called:
*
* <ul>
* <li>a {@link TreeTableCellRenderer} will be installed on the hierarchical
* {@link TableColumn}. It wraps any {@link TableCellRenderer} previously
* installed on the {@link TableColumn}, or if none was present, a
* DefaultTableCellRenderer. The delegate renderer can be changed at any
* time using {@link #setDelegateRenderer}.</li>
*
* <li>a {@link TreeTableCellEditor} will be installed on the hierarchical
* {@link TableColumn}. It wraps any {@link TableCellEditor} previously
* installed on the {@link TableColumn}, or if none was present, a
* DefaultCellEditor. The delegate editor can be changed at any
* time using {@link #setDelegateEditor}.</li>
*
* <li>the UI Delegate's MouseListener will be decorated with extra
* functionality that detects clicks overtop of the expand/collapse icon
* and reacts accordingly by expanding/collapsing the hierarchy. In these
* cases the normal mechanism for handling clicks over the table is
* circumvented and thus cell edits and/or row selection changes are not
* honoured.</li>
* </ul>
*
* <font size="+1"><u>Customizing TreeTableSupport</u></font>
*
* <p>After installing the basic TreeTableSupport, its specific behaviour can
* be refined by setting properties on the instance of TreeTableSupport
* returned by the {@link #install} method. Below are descriptions of some of
* the available customizations.
*
* <p><i><u>Left & Right Arrow Keys</u></i>
*
* <p>Within a treetable there is ambiguity about the functions performed by
* the left and right arrow keys. Tables normally use them to advance cell
* focus to the left or right while trees commonly use them to expand or
* collapse hierarchies. What behaviour is desirable for any given treetable?
* By default, TreeTableSupport assumes the table-style behaviour but that
* can be adjusted to the tree-style behaviour calling
* {@link #setArrowKeyExpansionEnabled} with an argument of <tt>true</tt>.
* Generally speaking, tree-style behaviour is preferrable when the treetable
* is readonly.
*
* <p><i><u>Displaying Expander Buttons</u></i>
*
* <p>{@link TreeList} draws a distinction between three unique situations
* regarding the display of the expander button for a given tree node, N:
*
* <ul>
* <li>Does node N currently have children? If it does, the expander button
* <code>must</code> be displayed.
* <p>e.g.: A directory in a file system that contains files and/or subdirectories.</li>
*
* <li>If node N does not currently have children, is it capable of having
* children in the future? If it is not, then it is a true leaf node and
* the expander button <code>must not</code> be displayed.
* <p>e.g.: A file in a file system.</li>
*
* <li>If node N currently has no children but is capable of having children
* in the future, should it be rendered with an expander now? Both yes
* and no are valid choices and thus the decision is left to the API
* user. The preference for this situation can be controlled using
* {@link #setShowExpanderForEmptyParent(boolean)}.
* <br>e.g.: Empty directories in Mac OS X Finder are displayed with
* visible expanders while Windows Explorer does not display them.</li>
* </ul>
*
* @author James Lemieux
*/
public final class TreeTableSupport {
/**
* A special ListSelectionModel implementation that always reports itself
* as empty. It is useful for readonly tree tables that don't want to
* display a cell selection and instead want left/right arrow keys to
* expand/collapse the tree hierarchy.
*/
private static final ListSelectionModel NOOP_SELECTION_MODEL = new NoopColumnSelectionModel();
/**
* A ListEventListener that tests whether events are delivered to the
* underlying TreeList on the Swing Event Dispath Thread. If they are not,
* an {@link IllegalStateException} is thrown to indicate the error.
*/
private static final ListEventListener SWING_THREAD_CHECKER = new EventDispathThreadChecker();
/** The table to decorate with tree table behaviour. */
private final JTable table;
/** The TreeList that stores the state of the tree. */
private final TreeList treeList;
/** The model index of the table column that is the hierarchical column. */
private final int hierarchyColumnModelIndex;
/** Expands and collapses the tree structure based on key strokes (the space bar). */
private final KeyListener expandAndCollapseKeyListener = new ExpandAndCollapseKeyListener();
/** Expands and collapses the tree structure based on left and right arrow key strokes. */
private final KeyListener arrowKeyListener = new ArrowKeyListener();
/** Controls whether the left and right arrow keys perform simple table navigations or expand/collapse the table hierarchy. */
private boolean arrowKeyExpansionEnabled;
/**
* When {@link #arrowKeyExpansionEnabled} is enabled, we must replace the
* column selection model, so we remember the original selection model in
* order to restore it if necessary.
*/
private ListSelectionModel originalColumnSelectionModel;
/** Controls whether the expander is displayed for empty tree nodes that may eventually have children. */
private boolean showExpanderForEmptyParent;
/** The renderer installed on the hierarchical TableColumn. */
private TreeTableCellRenderer treeTableCellRenderer;
/** The editor installed on the hierarchical TableColumn. */
private TreeTableCellEditor treeTableCellEditor;
/** The orignal TableCellRenderer, if any, to be replaced when TreeTableSupport is {@link #uninstall() uninstalled}. */
private final TableCellRenderer originalRenderer;
/** The orignal TableCellEditor, if any, to be replaced when TreeTableSupport is {@link #uninstall() uninstalled}. */
private final TableCellEditor originalEditor;
/**
* This private constructor creates a TreeTableSupport object which adds
* tree table behaviours to a single column that is nominated as the
* "hierarchical column." Specifically, the rendering and editing of values
* in the hierarchical column will be augmented to display position within
* a hierarchy by indenting the "normal renderer and editor" according to
* the height within a tree and adding a collapse/expand button.
*
* @param table the table to convert into a tree table
* @param treeList The TreeList capable of answering hierarchical questions
* for the hierachical TableColumn's renderer and editor
* @param modelColumnIndex the model index of the hierarchical column
*/
private TreeTableSupport(JTable table, TreeList treeList, int modelColumnIndex) {
this.table = table;
this.treeList = treeList;
this.hierarchyColumnModelIndex = modelColumnIndex;
final int viewColumnIndex = table.convertColumnIndexToView(modelColumnIndex);
// ensure we can find the view column index of the hierarchical column
if (viewColumnIndex == -1)
throw new IllegalArgumentException("Unable to locate a view index for the given model index: " + modelColumnIndex);
// look up the hierarchical TableColumn
final TableColumn viewColumn = table.getColumnModel().getColumn(viewColumnIndex);
// wrap the existing TableCellRenderer with a TreeTableCellRenderer
this.originalRenderer = viewColumn.getCellRenderer();
this.treeTableCellRenderer = new TreeTableCellRenderer(originalRenderer, treeList);
viewColumn.setCellRenderer(treeTableCellRenderer);
// wrap the existing TableCellEditor with a TreeTableCellEditor
this.originalEditor = viewColumn.getCellEditor();
this.treeTableCellEditor = new TreeTableCellEditor(originalEditor, treeList);
viewColumn.setCellEditor(treeTableCellEditor);
// install some listeners that govern expand/collapse behaviour
this.table.addKeyListener(arrowKeyListener);
this.table.addKeyListener(expandAndCollapseKeyListener);
decorateUIDelegateMouseListener(this.table);
// verify that all elements on the TreeList arrive on the Swing EDT
this.treeList.addListEventListener(SWING_THREAD_CHECKER);
}
/**
* This method is a complete hack, but is necessary to achieve the
* desired behaviour when adding collapse/expand capabilities to a
* treetable.
*
* The problem is that when a mouse button is pressed, the MouseListener
* installed by the UI Delegate is executed <strong>first</strong> and
* changes the selected row to the row that was just clicked. This is
* undesirable from the standpoint of treetables. One should be able to
* expand or collapse tree nodes without adjusting the selection.
*
* To solve the problem, we rip out the MouseListener installed by
* the BasicTableUI and decorate it before reinstalling it. The decorated
* version of the MouseListener detects mouse clicks that actually toggle
* the collapsed/expanded state and ensures that we do not change the
* selected row in that case.
*/
private void decorateUIDelegateMouseListener(Component component) {
// replace the first MouseListener that appears to be installed by the UI Delegate
final MouseListener[] mouseListeners = component.getMouseListeners();
for (int i = mouseListeners.length-1; i >= 0; i--) {
if (mouseListeners[i].getClass().getName().indexOf("TableUI") != -1) {
component.removeMouseListener(mouseListeners[i]);
component.addMouseListener(new ExpandAndCollapseMouseListener(mouseListeners[i]));
break;
}
}
}
/**
* This method will undo the work performed by {@link #decorateUIDelegateMouseListener(Component)}.
*/
private void undecorateUIDelegateMouseListener(Component component) {
// replace all decorated MouseListeners with original UI Delegate MouseListener
final MouseListener[] mouseListeners = component.getMouseListeners();
for (int i = 0; i < mouseListeners.length; i++) {
if (mouseListeners[i] instanceof ExpandAndCollapseMouseListener) {
component.removeMouseListener(mouseListeners[i]);
component.addMouseListener(((ExpandAndCollapseMouseListener) mouseListeners[i]).getDelegate());
}
}
}
/**
* Installs support for a hierarchical table column into the
* <code>table</code> on the column with the given
* <code>hierarchyColumnModelIndex</code>. The data returned by the
* <code>TableModel</code> for the hierarchical column will remain
* unchanged. But, the renderer and editor will be wrapped with instances
* of {@link TreeTableCellRenderer} and {@link TreeTableCellEditor}
* which take care of displaying the hierarchical information (namely the
* whitespace indenting and expand/collapse button).
*
* <p>The following must be true in order to successfully install
* TreeTableSupport on a {@link JTable}:
*
* <ul>
* <li>the <code>hierarchyColumnModelIndex</code> must correspond to a valid
* view column index</li>
* </ul>
*
* @param table the table to convert into a tree table
* @param treeList The TreeList capable of answering hierarchical questions
* for the hierachical TableColumn's renderer and editor
* @param modelColumnIndex the model index of the hierarchical column
* @return an instance of the support class providing treetable behaviour
*
* @throws IllegalStateException if this method is called from any Thread
* other than the Swing Event Dispatch Thread
*/
public static TreeTableSupport install(JTable table, TreeList treeList, int modelColumnIndex) {
checkAccessThread();
return new TreeTableSupport(table, treeList, modelColumnIndex);
}
/**
* Removes treetable support from the {@link JTable} it was
* installed on. This method is useful when the {@link TreeList} of items
* that backs the JTable must outlive the JTable itself. Calling this method
* will return the hierarchical column to its original state before
* treetable support was installed, and it will be available for garbage
* collection independently of the {@link TreeList} of items.
*
* @throws IllegalStateException if this method is called from any Thread
* other than the Swing Event Dispatch Thread
*/
public void uninstall() {
checkAccessThread();
// stop verifying that all elements on the TreeList arrive on the Swing EDT
treeList.removeListEventListener(SWING_THREAD_CHECKER);
// disable arrow key expansion if it was enabled
setArrowKeyExpansionEnabled(false);
// stop trying to toggle tree nodes due to keystrokes and mouse clicks
table.removeKeyListener(arrowKeyListener);
table.removeKeyListener(expandAndCollapseKeyListener);
undecorateUIDelegateMouseListener(table);
// fetch information about the hierarchical column
final int viewColumnIndex = table.convertColumnIndexToView(hierarchyColumnModelIndex);
final TableColumn viewColumn = table.getColumnModel().getColumn(viewColumnIndex);
final TableCellRenderer renderer = viewColumn.getCellRenderer();
final TableCellEditor editor = viewColumn.getCellEditor();
// if the TreeTableCellRenderer is still installed, reinstall the original TableCellRenderer
if (renderer == treeTableCellRenderer)
viewColumn.setCellRenderer(originalRenderer);
// if the TreeTableCellEditor is still installed, reinstall the original TableCellEditor
if (editor == treeTableCellEditor)
viewColumn.setCellEditor(originalEditor);
treeTableCellRenderer.dispose();
treeTableCellEditor.dispose();
}
/**
* Sets whether the expander is displayed for nodes that do not contain
* children but are allowed to contain children, and thus may accumulate
* children in the future. If this property is <tt>true</tt> then empty
* nodes that may contain children in the future are displayed with a
* visible expander; otherwise they are displayed without the expander.
* A node signals that it may contain children in the future by returning
* <tt>true</tt> from {@link TreeList.Format#allowsChildren(Object)}.
*/
public void setShowExpanderForEmptyParent(boolean showExpanderForEmptyParent) {
checkAccessThread();
if (this.showExpanderForEmptyParent == showExpanderForEmptyParent)
return;
this.showExpanderForEmptyParent = showExpanderForEmptyParent;
// indicate the new property value to the renderer and editor
treeTableCellRenderer.setShowExpanderForEmptyParent(showExpanderForEmptyParent);
treeTableCellEditor.setShowExpanderForEmptyParent(showExpanderForEmptyParent);
// repaint the table so the display is updated
table.repaint();
}
/**
* Returns <tt>true</tt> if empty tree nodes that are allowed to have
* children are displayed with visible expanders while they are empty;
* <tt>false</tt> otherwise.
*/
public boolean getShowExpanderForEmptyParent() {
return showExpanderForEmptyParent;
}
/**
* If <code>arrowKeyExpansionEnabled</code> is <tt>true</tt> then two
* things are changed in tandem:
*
* <ul>
* <li> The left and right arrow keys will toggle the expansion state of
* hierarchy nodes in the tree rather than adjust cell focus in the
* table.</li>
*
* <li> The table column model's selection model is replaced with a no-op
* implementation so that column selection and column lead/anchor
* indexes are NOT tracked. Thus, column selection is disabled and
* there is no cell focus adjusted when the left and right arrow
* keys are used.</li>
* </ul>
*
* <p>If <code>arrowKeyExpansionEnabled</code> is <tt>false</tt> then the
* behaviour is reverted to default. That is, left and right arrow keys
* adjust cell focus in the table and not expansion, and the original
* column selection model is reinstalled.
*/
public void setArrowKeyExpansionEnabled(boolean arrowKeyExpansionEnabled) {
checkAccessThread();
// make sure we're actually changing the value
if (this.arrowKeyExpansionEnabled == arrowKeyExpansionEnabled)
return;
final TableColumnModel tableColumnModel = table.getColumnModel();
if (arrowKeyExpansionEnabled) {
// if arrow key expansion is turned on, install the noop column selection model
originalColumnSelectionModel = tableColumnModel.getSelectionModel();
tableColumnModel.setSelectionModel(NOOP_SELECTION_MODEL);
} else {
// if arrow key expansion is turned off, restore the original column selection model
if (tableColumnModel.getSelectionModel() == NOOP_SELECTION_MODEL)
tableColumnModel.setSelectionModel(originalColumnSelectionModel);
originalColumnSelectionModel = null;
}
this.arrowKeyExpansionEnabled = arrowKeyExpansionEnabled;
}
/**
* Returns <tt>true</tt> if the left and right arrow keys are currently
* used to control the tree hierarchy expansion; <tt>false</tt> if they are
* used to control the table's cell focus.
*/
public boolean getArrowKeyExpansionEnabled() {
return arrowKeyExpansionEnabled;
}
/**
* Use the given <code>treeTableCellRenderer</code> when rendering the
* hierarchy column of the tree table.
*/
public void setRenderer(TreeTableCellRenderer treeTableCellRenderer) {
checkAccessThread();
// ensure we can find the view column index of the hierarchical column
final int viewColumnIndex = table.convertColumnIndexToView(hierarchyColumnModelIndex);
if (viewColumnIndex == -1)
throw new IllegalArgumentException("Unable to locate a view index for the given model index: " + hierarchyColumnModelIndex);
// look up the hierarchical TableColumn
final TableColumn viewColumn = table.getColumnModel().getColumn(viewColumnIndex);
// dispose the old renderer
this.treeTableCellRenderer.dispose();
// install the new renderer
this.treeTableCellRenderer = treeTableCellRenderer;
treeTableCellRenderer.setShowExpanderForEmptyParent(showExpanderForEmptyParent);
viewColumn.setCellRenderer(treeTableCellRenderer);
}
/**
* Returns the <code>treeTableCellRenderer</code> used to render the
* hierarchy column of the tree table.
*/
public TreeTableCellRenderer getRenderer() {
return treeTableCellRenderer;
}
/**
* Use the given <code>renderer</code> as the new delegate renderer of the
* {@link TreeTableCellRenderer} which is responsible for rendering the
* data associated with each tree node in the hierarchy column.
*/
public void setDelegateRenderer(TableCellRenderer renderer) {
checkAccessThread();
treeTableCellRenderer.setDelegate(renderer);
}
/**
* Returns the <code>renderer</code> responsible for rendering the data
* associated with each tree node in the hierarchy column.
*/
public TableCellRenderer getDelegateRenderer() {
return treeTableCellRenderer.getDelegate();
}
/**
* Use the given <code>treeTableCellEditor</code> when editing the
* hierarchy column of the tree table.
*/
public void setEditor(TreeTableCellEditor treeTableCellEditor) {
checkAccessThread();
// ensure we can find the view column index of the hierarchical column
final int viewColumnIndex = table.convertColumnIndexToView(hierarchyColumnModelIndex);
if (viewColumnIndex == -1)
throw new IllegalArgumentException("Unable to locate a view index for the given model index: " + hierarchyColumnModelIndex);
// look up the hierarchical TableColumn
final TableColumn viewColumn = table.getColumnModel().getColumn(viewColumnIndex);
// dispose the old editor
this.treeTableCellEditor.dispose();
// install the new editor
this.treeTableCellEditor = treeTableCellEditor;
treeTableCellEditor.setShowExpanderForEmptyParent(showExpanderForEmptyParent);
viewColumn.setCellEditor(treeTableCellEditor);
}
/**
* Returns the <code>treeTableCellEditor</code> used to render the
* hierarchy column of the tree table.
*/
public TreeTableCellEditor getEditor() {
return treeTableCellEditor;
}
/**
* Use the given <code>editor</code> as the new delegate editor of the
* {@link TreeTableCellEditor} which is responsible for editing the data
* associated with each tree node in the hierarchy column.
*/
public void setDelegateEditor(TableCellEditor editor) {
checkAccessThread();
treeTableCellEditor.setDelegate(editor);
}
/**
* Returns the <code>editor</code> responsible for editing the data
* associated with each tree node in the hierarchy column.
*/
public TableCellEditor getDelegateEditor() {
return treeTableCellEditor.getDelegate();
}
/**
* A convenience method to ensure {@link TreeTableSupport} is being
* accessed from the Event Dispatch Thread.
*/
private static void checkAccessThread() {
if (!SwingUtilities.isEventDispatchThread())
throw new IllegalStateException("TreeTableSupport must be accessed from the Swing Event Dispatch Thread, but was called on Thread \"" + Thread.currentThread().getName() + "\"");
}
/**
* This listener watches for mouse clicks overtop of the expander button
* within a renderer in a hierarchical column and toggles the expansion
* of the tree node, if possible, without calling the delegate. In all
* other cases (where the mouse click does not toggle expansion), the
* delegate is allowed to handle the mouse click as normal.
*/
private class ExpandAndCollapseMouseListener implements MouseListener {
/** An optional MouseListener to which we delegate after performing our own processing. */
private final MouseListener delegate;
/**
* Decorate the given <code>delegate</code> with extra functionality.
*/
public ExpandAndCollapseMouseListener(MouseListener delegate) {
if (delegate == null)
throw new IllegalArgumentException("delegate may not be null");
this.delegate = delegate;
}
/**
* Return the {@link MouseListener} being decorated.
*/
public MouseListener getDelegate() {
return delegate;
}
public void mousePressed(MouseEvent me) {
// if the table isn't enabled, break early
if (!table.isEnabled())
return;
// we're going to check if the single click was overtop of the
// expander button and toggle the expansion of the row if it was
// extract information about the location of the click
final JTable table = (JTable) me.getSource();
final Point clickPoint = me.getPoint();
final int row = table.rowAtPoint(clickPoint);
final int column = table.columnAtPoint(clickPoint);
// ensure a valid cell has clicked
if (row == -1 || column == -1)
return;
// translate the clickPoint to be relative to the rendered component
final Rectangle cellRect = table.getCellRect(row, column, true);
clickPoint.translate(-cellRect.x, -cellRect.y);
// if a left-click occurred over the expand/collapse button
final TreeTableCellPanel renderedPanel = TreeTableUtilities.prepareRenderer(me);
if (SwingUtilities.isLeftMouseButton(me) && renderedPanel != null && renderedPanel.isPointOverExpanderButton(clickPoint)) {
treeList.getReadWriteLock().writeLock().lock();
try {
// expand/collapse the row if possible
if (treeList.getAllowsChildren(row))
TreeTableUtilities.toggleExpansion(table, treeList, row).run();
} finally {
treeList.getReadWriteLock().writeLock().unlock();
}
// return early and don't allow the delegate a chance to react to the mouse click
return;
}
delegate.mousePressed(me);
}
public void mouseClicked(MouseEvent me) { delegate.mouseClicked(me); }
public void mouseReleased(MouseEvent me) { delegate.mouseReleased(me); }
public void mouseEntered(MouseEvent me) { delegate.mouseEntered(me); }
public void mouseExited(MouseEvent me) { delegate.mouseExited(me); }
}
/**
* This listener watches for space bar presses when cell focus is within
* a hierarchical column and toggles the expansion of the tree node, if
* possible.
*/
private class ExpandAndCollapseKeyListener extends KeyAdapter {
/**
* A Runnable which restores the state of the selection model and the
* client property "JTable.autoStartsEdit". We save the Runnable when
* the backspace key is pressed and the focus is within the hierarchy
* column. We execute it when the key is released to resume normal
* handling of future keystrokes.
*/
private Runnable restoreStateRunnable;
public void keyPressed(KeyEvent e) {
// if the table isn't enabled, break early
if (!table.isEnabled())
return;
// if the table doesn't own the focus, break early
if (!table.isFocusOwner())
return;
// if the key pressed is not the space bar, break early
if (e.getKeyCode() != KeyEvent.VK_SPACE || e.getModifiers() != 0)
return;
final int row = table.getSelectionModel().getLeadSelectionIndex();
final int column = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
// ensure a valid row has focus
if (row == -1)
return;
// if a column is selected, ensure it is the hierarchy column
if (column != -1 && table.convertColumnIndexToModel(column) != hierarchyColumnModelIndex)
return;
treeList.getReadWriteLock().writeLock().lock();
try {
// if the row is expandable, toggle its expanded state
if (treeList.getAllowsChildren(row)) {
final Runnable r = TreeTableUtilities.toggleExpansion(table, treeList, row);
if (restoreStateRunnable == null)
restoreStateRunnable = r;
}
} finally {
treeList.getReadWriteLock().writeLock().unlock();
}
}
/**
* When the key is released, execute the Runnable which restores the
* state of the selection model and "JTable.autoStartsEdit" client property.
*/
public void keyReleased(KeyEvent e) {
if (restoreStateRunnable != null) {
restoreStateRunnable.run();
restoreStateRunnable = null;
}
}
}
/**
* This listener watches for ENTER key presses when cell focus is within
* a hierarchical column and toggles the expansion of the tree node, if
* possible.
*/
private class ArrowKeyListener extends KeyAdapter {
public void keyPressed(KeyEvent e) {
if (!arrowKeyExpansionEnabled)
return;
// if the table isn't enabled, break early
if (!table.isEnabled())
return;
// if column selection is allowed, we want normal table handling of
// the arrow key, not this special tree node toggling logic
if (table.getColumnSelectionAllowed())
return;
// if the key pressed is not the space bar, break early
final int c = e.getKeyCode();
final boolean isLeftArrowKey = c == KeyEvent.VK_LEFT;
final boolean isRightArrowKey = c == KeyEvent.VK_RIGHT;
if (!(isLeftArrowKey || isRightArrowKey) || e.getModifiers() != 0)
return;
final int row = table.getSelectionModel().getLeadSelectionIndex();
// ensure a valid cell has focus
if (row == -1)
return;
treeList.getReadWriteLock().writeLock().lock();
try {
// if the row is expandable, toggle its expanded state
if (treeList.getAllowsChildren(row)) {
final boolean expanded = treeList.isExpanded(row);
if ((expanded && isLeftArrowKey) || (!expanded && isRightArrowKey))
TreeTableUtilities.toggleExpansion(table, treeList, row).run();
}
} finally {
treeList.getReadWriteLock().writeLock().unlock();
}
}
}
/**
* A ListEventListener that exists only to ensure that the TreeList
* receives all ListEvents on the Swing Event Dispatch Thread, and not some
* other random Thread. This is important because the treetable renderer
* and editor read from the TreeList on the Swing EDT and must be able to
* guarantee that their reads are atomic.
*/
private static class EventDispathThreadChecker implements ListEventListener {
public void listChanged(ListEvent listChanges) {
if (!SwingUtilities.isEventDispatchThread())
throw new IllegalStateException("TreeTableSupport has detected that its underlying TreeList was changed on a Thread that is NOT the Swing Event Dispatch Thread. " +
"This can cause unreliable results including sporadic exceptions since the TreeList is read from the EDT. Two solutions exist for this problem:\n\n" +
"a) ensure each and every write to the EventList pipeline occurs on the Swing EDT\n" +
"b) wrap the source EventList of TreeList in a Swing Thread Proxy List using GlazedListsSwing.swingThreadProxyList(...) before passing it to the TreeList constructor");
}
}
/**
* This is an implementation of the ListSelectionModel interface that does
* nothing. Consequently it never reports a change to the selection. Most
* importantly it never reports lead or anchor indexes which implies that
* when this is used as a column selection model, there is never a cell
* selection, only row selections. This is desirable for readonly
* treetables.
*/
private static class NoopColumnSelectionModel implements ListSelectionModel {
public void setSelectionInterval(int index0, int index1) { }
public void addSelectionInterval(int index0, int index1) { }
public void removeSelectionInterval(int index0, int index1) { }
public int getMinSelectionIndex() { return -1; }
public int getMaxSelectionIndex() { return -1; }
public boolean isSelectedIndex(int index) { return false; }
public int getAnchorSelectionIndex() { return -1; }
public void setAnchorSelectionIndex(int index) { }
public int getLeadSelectionIndex() { return -1; }
public void setLeadSelectionIndex(int index) { }
public void clearSelection() { }
public boolean isSelectionEmpty() { return true; }
public void insertIndexInterval(int index, int length, boolean before) { }
public void removeIndexInterval(int index0, int index1) { }
public void setValueIsAdjusting(boolean valueIsAdjusting) { }
public boolean getValueIsAdjusting() { return false; }
public void setSelectionMode(int selectionMode) { }
public int getSelectionMode() { return ListSelectionModel.SINGLE_SELECTION; }
public void addListSelectionListener(ListSelectionListener x) { }
public void removeListSelectionListener(ListSelectionListener x) { }
}
}