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.
|
* <p>
|
* 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.
|
* <p>
|
* 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 <code>updatingListSelectionModel</code> 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 <code>updatingListSelectionModel</code> 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);
|
}
|
}
|
}
|