blob: 12fd2a413b5c53f06da15977f425b14d01f69091 [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2007 The University of Manchester
*
* Modifications to the initial code base are copyright of their
* respective authors, or their employers as appropriate.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
******************************************************************************/
package net.sf.taverna.t2.partition.ui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.plaf.TreeUI;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
public abstract class TableTreeNodeRenderer implements TreeCellRenderer {
private static final long serialVersionUID = -7291631337751330696L;
// The difference in indentation between a node and its child nodes, there
// isn't an easy way to get this other than constructing a JTree and
// measuring it - you'd think it would be a property of TreeUI but
// apparently not.
private static int perNodeOffset = -1;
// Use this to rubber stamp the original node renderer in before rendering
// the table
private TreeCellRenderer nodeRenderer;
// Determines the space allocated to leaf nodes and their parents when
// applying the stamp defined by the nodeRenderer
private int nodeWidth;
// Number of pixels of space to leave between the node label and the table
// header or rows
private int labelToTablePad = 3;
// Number of pixels to leave around the label rendered into the table cells
private int cellPadding = 4;
// Drawing borders?
private boolean drawingBorders = true;
// The number of pixels by which the height of the header is reduced
// compared to the row height, this leaves a small border above the header
// and separates it from the last row of the table above, if any.
private int headerTopPad = 4;
// The proportion of colour : black or colour : white used to create the
// darker or lighter shades is blendFactor : 1
private int blendFactor = 2;
// Colour to use to draw the table borders when they're enabled
private Color borderColour = Color.black;
/**
* Set the colour to be used to draw the borders if they are displayed at
* all. Defaults to black.
*/
public void setBorderColour(Color borderColour) {
this.borderColour = borderColour;
}
/**
* The blend factor determines how strong the colour component is in the
* shadow and highlight colours used in the bevelled boxes, the ratio of
* black / white to colour is 1 : blendFactor
*
* @param blendFactor
*/
public void setBlendFactor(int blendFactor) {
this.blendFactor = blendFactor;
}
/**
* Set whether the renderer will draw borders around the table cells - if
* this is false the table still has the bevelled edges of the cell painters
* so will still look semi-bordered. Defaults to true if not otherwise set.
*
* @param drawingBorders
*/
public void setDrawBorders(boolean drawingBorders) {
this.drawingBorders = drawingBorders;
}
/**
* Override and implement to get the list of columns for a given partition
* node - currently assumes all partitions use the same column structure
* which I need to fix so it doesn't take a partition as argument (yet).
*
* @return an array of column specifications used to drive the renderer
*/
public abstract TableTreeNodeColumn[] getColumns();
/**
* Construct a new TableTreeNodeRenderer
*
* @param nodeRenderer
* The inner renderer used to render the node labels
* @param nodeWidth
* Width of the cell space into which the node label is rendered
* in the table header and row nodes
*/
public TableTreeNodeRenderer(TreeCellRenderer nodeRenderer, int nodeWidth) {
super();
this.nodeRenderer = nodeRenderer;
this.nodeWidth = nodeWidth;
}
/**
* Do the magic!
*/
public Component getTreeCellRendererComponent(final JTree tree,
final Object value, final boolean selected, final boolean expanded,
final boolean leaf, final int row, final boolean hasFocus) {
final Component nodeLabel = nodeRenderer.getTreeCellRendererComponent(
tree, value, false, expanded, leaf, row, false);
final int nodeLabelHeight = (int) nodeLabel.getPreferredSize()
.getHeight();
if (leaf) {
// Rendering the leaf nodes, therefore use the table rendering
// strategy
getPerNodeIndentation(tree, row);
return new JComponent() {
private static final long serialVersionUID = 4993815558563895266L;
@Override
public Dimension getPreferredSize() {
int width = nodeWidth + labelToTablePad;
for (TableTreeNodeColumn column : getColumns()) {
width += column.getColumnWidth();
}
return new Dimension(width, nodeLabelHeight);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform originalTransform = g2d.getTransform();
// Enable anti-aliasing for the curved lines
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// This method should paint a bevelled container for the
// original label but it doesn't really work terribly well
// as we can't ensure that the original label is actually
// honouring any opaque flags.
if (drawingBorders) {
paintRectangleWithBevel(g2d, nodeWidth
+ labelToTablePad, getHeight(), Color.white);
}
// Paint original node label
nodeLabel.setSize(new Dimension(
nodeWidth - cellPadding * 2, getHeight()
- (drawingBorders ? 2 : 1)));
g2d.translate(cellPadding, 0);
nodeLabel.paint(g2d);
g2d.translate(-cellPadding, 0);
if (drawingBorders) {
paintRectangleBorder(g2d, nodeWidth + labelToTablePad,
getHeight(), 0, 0, 1, 1, borderColour);
}
g2d.translate(nodeWidth + labelToTablePad, 0);
boolean first = true;
for (TableTreeNodeColumn column : getColumns()) {
Color fillColour = column.getColour().brighter();
Object parentNode = tree.getPathForRow(row)
.getParentPath().getLastPathComponent();
int indexInParent = tree.getModel().getIndexOfChild(
parentNode, value);
if ((indexInParent & 1) == 1) {
fillColour = new Color(
(fillColour.getRed() + column.getColour()
.getRed()) / 2, (fillColour
.getGreen() + column.getColour()
.getGreen()) / 2, (fillColour
.getBlue() + column.getColour()
.getBlue()) / 2);
}
// Paint background and bevel
paintRectangleWithBevel(g2d, column.getColumnWidth(),
getHeight(), fillColour);
// Paint cell component
Component cellComponent = column.getCellRenderer(value);
cellComponent.setSize(new Dimension(column
.getColumnWidth()
- cellPadding * 2, getHeight()));
g2d.translate(cellPadding, 0);
cellComponent.paint(g2d);
g2d.translate(-cellPadding, 0);
// Draw border
if (drawingBorders) {
paintRectangleBorder(g2d, column.getColumnWidth(),
getHeight(), 0, 1, 1, first ? 1 : 0,
borderColour);
}
first = false;
g2d.translate(column.getColumnWidth(), 0);
}
if (selected) {
g2d.setTransform(originalTransform);
g2d.translate(2, 0);
paintRectangleWithHighlightColour(g2d, getWidth()
- (drawingBorders ? 4 : 2), getHeight()
- (drawingBorders ? 2 : 0));
}
}
};
} else {
// If there are no child nodes, or there are child nodes but they
// aren't leaves then we render the cell as normal. If there are
// child nodes and the first one is a leaf (we assume this means
// they all are!) then we render the table header after the label.
if (!expanded) {
return getLabelWithHighlight(nodeLabel, selected);
}
// Expanded so do the model check...
TreeModel model = tree.getModel();
int childCount = model.getChildCount(value);
if (childCount == 0) {
return getLabelWithHighlight(nodeLabel, selected);
}
Object childNode = model.getChild(value, 0);
if (!model.isLeaf(childNode)) {
return getLabelWithHighlight(nodeLabel, selected);
}
getPerNodeIndentation(tree, row);
// Got to here so we need to render a table header.
return new JComponent() {
private static final long serialVersionUID = -4923965850510357216L;
@Override
public Dimension getPreferredSize() {
int width = nodeWidth + labelToTablePad + perNodeOffset;
for (TableTreeNodeColumn column : getColumns()) {
width += column.getColumnWidth();
}
return new Dimension(width, nodeLabelHeight);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform originalTransform = g2d.getTransform();
// Enable anti-aliasing for the curved lines
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Paint original node label
nodeLabel.setSize(new Dimension(nodeWidth + perNodeOffset,
getHeight()));
nodeLabel.paint(g2d);
// Draw line under label to act as line above table row
// below
if (drawingBorders) {
GeneralPath path = new GeneralPath();
path.moveTo(perNodeOffset, getHeight() - 1);
path.lineTo(
perNodeOffset + nodeWidth + labelToTablePad,
getHeight() - 1);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
g2d.setPaint(borderColour);
g2d.draw(path);
}
// Move painting origin to the start of the header row
g2d.translate(nodeWidth + perNodeOffset + labelToTablePad,
0);
// Paint columns
boolean first = true;
for (TableTreeNodeColumn column : getColumns()) {
// Paint header cell background with bevel
g2d.translate(0, headerTopPad);
paintRectangleWithBevel(g2d, column.getColumnWidth(),
getHeight() - headerTopPad, column.getColour());
// Paint header label
JLabel columnLabel = new JLabel(column.getShortName());
columnLabel.setSize(new Dimension(column
.getColumnWidth()
- cellPadding * 2, getHeight() - headerTopPad));
g2d.translate(cellPadding, 0);
columnLabel.paint(g2d);
g2d.translate(-cellPadding, 0);
// Paint header borders
if (drawingBorders) {
paintRectangleBorder(g2d, column.getColumnWidth(),
getHeight() - headerTopPad, 1, 1, 1,
first ? 1 : 0, borderColour);
}
g2d.translate(0, -headerTopPad);
first = false;
g2d.translate(column.getColumnWidth(), 0);
}
if (selected) {
g2d.setTransform(originalTransform);
g2d.translate(1, headerTopPad);
paintRectangleWithHighlightColour(g2d, perNodeOffset
+ nodeWidth + labelToTablePad
- (drawingBorders ? 2 : 0), getHeight()
- (headerTopPad + 2));
}
}
};
}
}
private static Component getLabelWithHighlight(final Component c,
boolean selected) {
if (!selected) {
return c;
} else {
return new JComponent() {
private static final long serialVersionUID = -9175635475959046704L;
@Override
public Dimension getPreferredSize() {
return c.getPreferredSize();
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
c.setSize(new Dimension(getWidth(), getHeight()));
c.paint(g2d);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.translate(1, 1);
paintRectangleWithHighlightColour(g2d, getWidth() - 2,
getHeight() - 2);
}
};
}
}
private static void paintRectangleBorder(Graphics2D g2d, int width,
int height, int north, int east, int south, int west, Color c) {
Paint oldPaint = g2d.getPaint();
Stroke oldStroke = g2d.getStroke();
g2d.setPaint(c);
GeneralPath path;
if (north > 0) {
g2d.setStroke(new BasicStroke(north, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
path = new GeneralPath();
path.moveTo(0, north - 1);
path.lineTo(width - 1, north - 1);
g2d.draw(path);
}
if (east > 0) {
g2d.setStroke(new BasicStroke(east, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
path = new GeneralPath();
path.moveTo(width - east, 0);
path.lineTo(width - east, height - 1);
g2d.draw(path);
}
if (south > 0) {
g2d.setStroke(new BasicStroke(south, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
path = new GeneralPath();
path.moveTo(0, height - south);
path.lineTo(width - 1, height - south);
g2d.draw(path);
}
if (west > 0) {
g2d.setStroke(new BasicStroke(west, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
path = new GeneralPath();
path.moveTo(west - 1, 0);
path.lineTo(west - 1, height - 1);
g2d.draw(path);
}
g2d.setPaint(oldPaint);
g2d.setStroke(oldStroke);
}
/**
* Paint a rectangle with the border colour set from the UIManager
* 'textHighlight' property and filled with the same colour at alpha 50/255.
* Paints from 0,0 to width-1,height-1 into the specified Graphics2D,
* preserving the existing paint and stroke properties on exit.
*/
private static void paintRectangleWithHighlightColour(Graphics2D g2d,
int width, int height) {
GeneralPath path = new GeneralPath();
path.moveTo(0, 0);
path.lineTo(width - 1, 0);
path.lineTo(width - 1, height - 1);
path.lineTo(0, height - 1);
path.closePath();
Paint oldPaint = g2d.getPaint();
Stroke oldStroke = g2d.getStroke();
Color c = UIManager.getColor("textHighlight");
g2d.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
g2d.setPaint(c);
g2d.draw(path);
Color alpha = new Color(c.getRed(), c.getGreen(), c.getBlue(), 50);
g2d.setPaint(alpha);
g2d.fill(path);
g2d.setPaint(oldPaint);
g2d.setStroke(oldStroke);
}
/**
* Paint a bevelled rectangle into the specified Graphics2D with shape from
* 0,0 to width-1,height-1 using the specified colour as a base and
* preserving colour and stroke in the Graphics2D
*/
private void paintRectangleWithBevel(Graphics2D g2d, int width, int height,
Color c) {
if (drawingBorders) {
width = width - 1;
height = height - 1;
}
GeneralPath path = new GeneralPath();
path.moveTo(0, 0);
path.lineTo(width - 1, 0);
path.lineTo(width - 1, height - 1);
path.lineTo(0, height - 1);
path.closePath();
Paint oldPaint = g2d.getPaint();
Stroke oldStroke = g2d.getStroke();
g2d.setPaint(c);
g2d.fill(path);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER));
// Draw highlight (Northeast)
path = new GeneralPath();
path.moveTo(0, 0);
path.lineTo(width - 1, 0);
path.lineTo(width - 1, height - 1);
Color highlightColour = new Color((c.getRed() * blendFactor + 255)
/ (blendFactor + 1), (c.getGreen() * blendFactor + 255)
/ (blendFactor + 1), (c.getBlue() * blendFactor + 255)
/ (blendFactor + 1));
g2d.setPaint(highlightColour);
g2d.draw(path);
// Draw shadow (Southwest)
path = new GeneralPath();
path.moveTo(0, 0);
path.lineTo(0, height - 1);
path.lineTo(width - 1, height - 1);
Color shadowColour = new Color((c.getRed() * blendFactor)
/ (blendFactor + 1), (c.getGreen() * blendFactor)
/ (blendFactor + 1), (c.getBlue() * blendFactor)
/ (blendFactor + 1));
g2d.setPaint(shadowColour);
g2d.draw(path);
g2d.setPaint(oldPaint);
g2d.setStroke(oldStroke);
}
/**
* The TreeUI which was used to determine the per node indentation in the
* JTree for which this is a renderer. If this hasn't been set yet then this
* is null.
*/
private static TreeUI cachedTreeUI = null;
/**
* Use the current TreeUI to determine the indentation per-node in the tree,
* this only works when the treeRow passed in is not the root as it has to
* traverse up to the parent to do anything sensible. Cached and associated
* with the TreeUI so in theory if the look and feel changes the UI changes
* and this is re-generated within the renderer code.
*
* @param tree
* @param treeRow
* @return
*/
private static int getPerNodeIndentation(JTree tree, int treeRow) {
if (perNodeOffset > 0 && tree.getUI() == cachedTreeUI) {
return perNodeOffset;
}
TreeUI uiModel = tree.getUI();
cachedTreeUI = uiModel;
TreePath path = tree.getPathForRow(treeRow);
Rectangle nodeBounds = uiModel.getPathBounds(tree, path);
Rectangle parentNodeBounds = uiModel.getPathBounds(tree, path
.getParentPath());
perNodeOffset = (int) nodeBounds.getMinX()
- (int) parentNodeBounds.getMinX();
return perNodeOffset;
}
}