package com.vci.client.ui.treeTable; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.util.EventObject; import javax.swing.AbstractCellEditor; import javax.swing.JTable; import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.LookAndFeel; import javax.swing.UIManager; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeSelectionModel; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import com.vci.client.ui.table.HorizontalAlignTableCellRender; public class JTreeTable extends JTable { /** * */ private static final long serialVersionUID = -3912783890684226553L; protected TreeTableCellRenderer renderer; public JTreeTable(InterfaceTreeTableModel treeTableModel) { super(); // Create the tree. It will be used as a renderer and editor. renderer = new TreeTableCellRenderer(treeTableModel); super.setModel(new TreeTableModelAdapter(treeTableModel, renderer)); // Force the JTable and JTree to share their row selection models. ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper(); renderer.setSelectionModel(selectionWrapper); setSelectionModel(selectionWrapper.getListSelectionModel()); // Install the tree editor renderer and editor. setDefaultRenderer(InterfaceTreeTableModel.class, renderer); setDefaultEditor(InterfaceTreeTableModel.class, new TreeTableCellEditor()); // No grid. setShowGrid(true); // No intercell spacing setIntercellSpacing(new Dimension(0, 0)); // And update the height of the trees row to match that of // the table. if (renderer.getRowHeight() < 1) { // Metal looks better like this. setRowHeight(18); } setCellAlignToCenter(this); } /** * Overridden to message super and forward the method to the tree. Since the * tree is not actually in the component hieachy it will never receive this * unless we forward it in this manner. */ public void updateUI() { super.updateUI(); if (renderer != null) { renderer.updateUI(); } // Use the tree's default foreground and background colors in the // table. LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font"); } /* * Workaround for BasicTableUI anomaly. Make sure the UI never tries to paint * the editor. The UI currently uses different techniques to paint the renderers * and editors and overriding setBounds() below is not the right thing to do for * an editor. Returning -1 for the editing row in this case, ensures the editor * is never painted. */ public int getEditingRow() { return (getColumnClass(editingColumn) == InterfaceTreeTableModel.class) ? -1 : editingRow; } /** * Overridden to pass the new rowHeight to the tree. */ public void setRowHeight(int rowHeight) { super.setRowHeight(rowHeight); if (renderer != null && renderer.getRowHeight() != rowHeight) { renderer.setRowHeight(getRowHeight()); } } /** * Returns the tree that is being shared between the model. */ public JTree getTree() { return renderer; } public class TreeTableCellRenderer extends JTree implements TableCellRenderer { /** * */ private static final long serialVersionUID = -5786712212850260154L; protected int visibleRow; public TreeTableCellRenderer(TreeModel model) { super(model); } /** * updateUI is overridden to set the colors of the Tree's renderer to match that * of the table. */ public void updateUI() { super.updateUI(); // Make the tree's cell renderer use the table's cell selection // colors. TreeCellRenderer tcr = getCellRenderer(); if (tcr instanceof DefaultTreeCellRenderer) { DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr); // For 1.1 uncomment this, 1.2 has a bug that will cause an // exception to be thrown if the border selection color is // null. // dtcr.setBorderSelectionColor(null); dtcr.setTextSelectionColor(UIManager.getColor("Table.selectionForeground")); dtcr.setBackgroundSelectionColor(UIManager.getColor("Table.selectionBackground")); } } /** * Sets the row height of the tree, and forwards the row height to the table. */ public void setRowHeight(int rowHeight) { if (rowHeight > 0) { super.setRowHeight(rowHeight); if (JTreeTable.this != null && JTreeTable.this.getRowHeight() != rowHeight) { JTreeTable.this.setRowHeight(getRowHeight()); } } } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (isSelected) { setBackground(table.getSelectionBackground()); } else { setBackground(table.getBackground()); } visibleRow = row; return this; } /** * This is overridden to set the height to match that of the JTable. */ public void setBounds(int x, int y, int w, int h) { super.setBounds(x, 0, w, JTreeTable.this.getHeight()); } /** * Sublcassed to translate the graphics such that the last visible row will be * drawn at 0,0. */ public void paint(Graphics g) { g.translate(0, -visibleRow * getRowHeight()); super.paint(g); if (!JTreeTable.this.isCellSelected(visibleRow, 0)) { Rectangle rcBounds = this.getBounds(); Rectangle rc = this.getRowBounds(visibleRow); Color c = g.getColor(); g.setColor(JTreeTable.this.gridColor); g.drawLine(0, rc.y + rc.height - 1, rcBounds.width, rc.y + rc.height - 1); g.drawLine(rcBounds.width - 1, rc.y, rcBounds.width - 1, rc.y + rc.height - 1); g.setColor(c); } } } /** * TreeTableCellEditor implementation. Component returned is the JTree. */ public class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor { public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { return renderer; } /** * Overridden to return false, and if the event is a mouse event it is forwarded * to the tree. *
* The behavior for this is debatable, and should really be offered as a * property. By returning false, all keyboard actions are implemented in terms * of the table. By returning true, the tree would get a chance to do something * with the keyboard events. For the most part this is ok. But for certain keys, * such as left/right, the tree will expand/collapse where as the table focus * should really move to a different column. Page up/down should also be * implemented in terms of the table. By returning false this also has the added * benefit that clicking outside of the bounds of the tree node, but still in * the tree column will select the row, whereas if this returned true that * wouldn't be the case. *
* By returning false we are also enforcing the policy that the tree will never
* be editable (at least by a key sequence).
*/
public boolean isCellEditable(EventObject e) {
if (e instanceof MouseEvent) {
for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
if (getColumnClass(counter) == InterfaceTreeTableModel.class) {
MouseEvent me = (MouseEvent) e;
MouseEvent newME = new MouseEvent(renderer, me.getID(), me.getWhen(), me.getModifiers(),
me.getX() - getCellRect(0, counter, true).x, me.getY(), me.getClickCount(),
me.isPopupTrigger());
renderer.dispatchEvent(newME);
break;
}
}
}
return false;
}
public Object getCellEditorValue() {
return null;
}
}
class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
/** Set to true when we are updating the ListSelectionModel. */
protected boolean updatingListSelectionModel;
public ListToTreeSelectionModelWrapper() {
super();
getListSelectionModel().addListSelectionListener(createListSelectionListener());
}
/**
* Returns the list selection model. ListToTreeSelectionModelWrapper listens for
* changes to this model and updates the selected paths accordingly.
*/
ListSelectionModel getListSelectionModel() {
return listSelectionModel;
}
/**
* This is overridden to set updatingListSelectionModel
and message
* super. This is the only place DefaultTreeSelectionModel alters the
* ListSelectionModel.
*/
public void resetRowSelection() {
if (!updatingListSelectionModel) {
updatingListSelectionModel = true;
try {
super.resetRowSelection();
} finally {
updatingListSelectionModel = false;
}
}
// Notice how we don't message super if
// updatingListSelectionModel is true. If
// updatingListSelectionModel is true, it implies the
// ListSelectionModel has already been updated and the
// paths are the only thing that needs to be updated.
}
/**
* Creates and returns an instance of ListSelectionHandler.
*/
protected ListSelectionListener createListSelectionListener() {
return new ListSelectionHandler();
}
/**
* If updatingListSelectionModel
is false, this will reset the
* selected paths from the selected rows in the list selection model.
*/
protected void updateSelectedPathsFromSelectedRows() {
if (!updatingListSelectionModel) {
updatingListSelectionModel = true;
try {
// This is way expensive, ListSelectionModel needs an
// enumerator for iterating.
int min = listSelectionModel.getMinSelectionIndex();
int max = listSelectionModel.getMaxSelectionIndex();
clearSelection();
if (min != -1 && max != -1) {
for (int counter = min; counter <= max; counter++) {
if (listSelectionModel.isSelectedIndex(counter)) {
TreePath selPath = renderer.getPathForRow(counter);
if (selPath != null) {
addSelectionPath(selPath);
}
}
}
}
} finally {
updatingListSelectionModel = false;
}
}
}
/**
* Class responsible for calling updateSelectedPathsFromSelectedRows when the
* selection of the list changse.
*/
class ListSelectionHandler implements ListSelectionListener {
public void valueChanged(ListSelectionEvent e) {
updateSelectedPathsFromSelectedRows();
}
}
}
private void setCellAlignToCenter(JTable table) {
HorizontalAlignTableCellRender cellRenderer = new HorizontalAlignTableCellRender(
DefaultTableCellRenderer.CENTER);
for (int i = 1; i < table.getColumnCount(); i++) {
this.getColumnModel().getColumn(i).setCellRenderer(cellRenderer);
}
}
}