/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* ExplorerTree.java
* Creation date: Jan 17th 2003
* By: Ken Wong
*/
package org.openquark.gems.client.explorer;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.Autoscroll;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import java.awt.image.RescaleOp;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.plaf.TreeUI;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.openquark.cal.metadata.FunctionMetadata;
import org.openquark.gems.client.CodeGem;
import org.openquark.gems.client.CollectorGem;
import org.openquark.gems.client.FunctionalAgentGem;
import org.openquark.gems.client.Gem;
import org.openquark.gems.client.RecordFieldSelectionGem;
import org.openquark.gems.client.ReflectorGem;
import org.openquark.gems.client.valueentry.ValueEditor;
/**
* This class represents the JTree JComponent that is used in the TableTopExplorer.
* @author Ken Wong
*/
public class ExplorerTree extends JTree implements Autoscroll{
private static final long serialVersionUID = 1943630679610833258L;
/**
* The editor used to change the names of the code gems and collector gems
* @author Ken Wong
*/
private static class ExplorerCellEditor extends DefaultTreeCellEditor {
/**
* Default Constructor for this editor
* @param tree the explorer tree the editor is for
* @param renderer the cell renderer for the tree
* @param field the cell editor for the field we should edit
*/
ExplorerCellEditor(ExplorerTree tree, DefaultTreeCellRenderer renderer, ExplorerTreeCellEditor field) {
super (tree, renderer, field);
}
/**
* @see org.openquark.gems.client.explorer.ExplorerTreeCellEditor#getTreeCellEditorComponent(javax.swing.JTree, java.lang.Object, boolean, boolean, boolean, int)
*/
@Override
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
Component component = super.getTreeCellEditorComponent(tree, value, isSelected, expanded, leaf, row);
renderer.getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, true);
editingIcon = renderer.getIcon();
boolean valueEditor = ((DefaultTreeCellEditor.EditorContainer) component).getComponent(0) instanceof ValueEditor;
tree.setRowHeight(valueEditor ? -1 : 16);
return component;
}
}
/**
* Used to listen to mouse clicked events and when one is received on a focussable node the
* navigation helper is notified that the user wants to focus on the desired gem.
*/
private class ExplorerMouseEventListener extends MouseAdapter {
/**
* Clicking on a focussable node should trigger a navigation focus event on the node.
* @param e The mouse event that just happened
*/
@Override
public void mouseClicked(MouseEvent e) {
Point location = e.getPoint();
if (isFocusablePoint(location)) {
TreePath path = getPathForLocation(location.x, location.y);
Object node = path.getLastPathComponent();
if (node instanceof ExplorerGemNode) {
// Indicate that the navigation helper should change to focus on the gem
Gem gem = ((ExplorerGemNode)node).getGem();
navigationHelper.focusOn(gem);
}
}
}
}
/**
* This class uses the navigation helper to determine if a node is focusable. If it is then the
* cursor is switched to a hand to mimic the behaviour in browsers of mousing over a link.
*/
private class ExplorerMouseMotionListener extends MouseMotionAdapter {
@Override
public void mouseMoved(MouseEvent e) {
if (isFocusablePoint(e.getPoint())) {
// We can focus on the gem and its node should be rendered as a hyperlink so change
// to the hand cursor
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
// The mouse is not over a hyperlink so ensure that it is reset to the default cursor
setCursor(null);
}
}
}
/**
* Whenever something in the model changes we need to make sure the cursor is reset to the default
* cursor. If we don't then the cursor may be left as a hand even though the mouse is no longer
* over a hyperlink.
*/
private class ExplorerTreeModelListener implements TreeModelListener {
public void treeNodesChanged(TreeModelEvent e) {
setCursor(null);
}
public void treeNodesInserted(TreeModelEvent e) {
setCursor(null);
}
public void treeNodesRemoved(TreeModelEvent e) {
setCursor(null);
}
public void treeStructureChanged(TreeModelEvent e) {
setCursor(null);
}
}
/** The TableTopExplorerOwner of the TableTopExplorer using this tree. */
private TableTopExplorerOwner owner;
/** The current background image (null if none). */
private BufferedImage backgroundImage;
/**
* Controls whether the tree is drawn using the UIManager specified look and feel or a customized
* look and feel that uses alternating light and dark bands to provide visual separation.
*/
private boolean useBandedLookAndFeel = false;
/** The set of expanded paths that was saved when the tree state was saved. */
private Set<TreePath> savedExpandedPaths = new HashSet<TreePath>();
/** The saved selection path. */
private TreePath savedSelectionPath = null;
/**
* Navigation helper used to determine extra information about whether nodes can be focussed on. The
* navigation helper can be null in which case navigation functionality will be disabled.
*/
private final ExplorerNavigationHelper navigationHelper;
/**
* Constructor for ExplorerGemTree.
* @param explorerRootNode the root node to use for the tree
* @param owner the explorer owner
* @param tableTopExplorer the explorer this tree is for
*/
public ExplorerTree(ExplorerRootNode explorerRootNode, TableTopExplorerOwner owner, TableTopExplorer tableTopExplorer) {
this(explorerRootNode, owner, tableTopExplorer, null, null);
}
/**
* Constructor for ExplorerGemTree.
* @param explorerRootNode the root node to use for the tree
* @param owner the explorer owner
* @param tableTopExplorer the explorer this tree is for
* @param navigationHelper A helper that allows navigation between tree nodes
* @param cellRenderer A customized cell renderer. If this is null then the default renderer will be
* used
*/
public ExplorerTree(ExplorerRootNode explorerRootNode,
TableTopExplorerOwner owner,
TableTopExplorer tableTopExplorer,
ExplorerNavigationHelper navigationHelper,
DefaultTreeCellRenderer cellRenderer) {
super(explorerRootNode);
this.owner = owner;
this.navigationHelper = navigationHelper;
setEditable(true);
setRowHeight(-1);
setInvokesStopCellEditing(true);
setFocusCycleRoot(true);
// Create a default cell renderer if necessary
if (cellRenderer == null) {
cellRenderer = new ExplorerCellRenderer(owner);
}
setCellRenderer(cellRenderer);
setCellEditor(new ExplorerCellEditor(this, cellRenderer, new ExplorerTreeCellEditor(tableTopExplorer)));
TreeSelectionModel selectionModel = new DefaultTreeSelectionModel();
selectionModel.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
setSelectionModel(selectionModel);
ToolTipManager.sharedInstance().registerComponent(this);
// If we have a navigation helper then add a mouse listeners to change the cursor when it
// is over a hyperlink, focus on a node when clicked, and listen to model changes to ensure
// the cursor is reset correctly.
if (navigationHelper != null) {
addMouseListener(new ExplorerMouseEventListener());
addMouseMotionListener(new ExplorerMouseMotionListener());
getModel().addTreeModelListener(new ExplorerTreeModelListener());
}
}
/**
* @see javax.swing.JComponent#getToolTipText()
*/
@Override
public String getToolTipText(MouseEvent e) {
TreePath treePath = getPathForLocation(e.getX(), e.getY());
if (treePath == null) {
return null;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode) treePath.getLastPathComponent();
Object userObject = node.getUserObject();
if (userObject instanceof Gem.PartInput) {
return owner.getHTMLFormattedMetadata((Gem.PartInput) userObject);
} else if (userObject instanceof FunctionalAgentGem) {
return owner.getHTMLFormattedFunctionalAgentGemDescription((FunctionalAgentGem) userObject);
} else if (userObject instanceof CollectorGem) {
// Start with the open html tag
String toolTip = "<html>";
// If available add the metadata short description
FunctionMetadata metadata = ((CollectorGem)userObject).getDesignMetadata();
String shortDescription = metadata.getShortDescription();
if (shortDescription != null && shortDescription.length() > 0) {
toolTip += "<p>" + shortDescription + "</p>";
}
// Stick in the result type information
toolTip += ExplorerMessages.getString("ResultTypeToolTip", owner.getTypeString(((CollectorGem) userObject).getResultType()));
// End with the end html tag
toolTip += "</html>";
return toolTip;
} else if (userObject instanceof ReflectorGem) {
return "<html>" + ExplorerMessages.getString("OutputTypeToolTip", owner.getTypeString(((ReflectorGem)userObject).getOutputPart().getType())) + "</html>";
} else if (userObject instanceof CodeGem) {
return "<HTML><B>" + ((CodeGem) userObject).getUnqualifiedName() + "</B></HTML>";
} else if (userObject instanceof RecordFieldSelectionGem) {
return "<html>" + ExplorerMessages.getString("FieldToExtractToolTip", ((RecordFieldSelectionGem)userObject).getFieldName().toString() + "</html>");
}
return null;
}
/**
* @see java.awt.dnd.Autoscroll#autoscroll(Point)
*/
public void autoscroll(Point point) {
Rectangle visibleRect = getVisibleRect();
Insets insets = getAutoscrollInsets();
int offset = (point.y > (getHeight() - insets.bottom)) ? 20 : -20;
scrollRectToVisible(new Rectangle(visibleRect.x, visibleRect.y + offset, visibleRect.width, visibleRect.height));
}
/**
* @see java.awt.dnd.Autoscroll#getAutoscrollInsets()
*/
public Insets getAutoscrollInsets() {
Rectangle visibleRect = getVisibleRect();
int topScrollSection = visibleRect.y + 30;
int bottemScrollSection = getHeight() - (visibleRect.height + visibleRect.y - 30);
return new Insets (topScrollSection, 0, bottemScrollSection, 0);
}
/**
* @see javax.swing.JComponent#paintComponent(Graphics)
* Paint the Explorer
* @param g java.awt.Graphics
*/
@Override
public void paintComponent(java.awt.Graphics g) {
if (backgroundImage != null) {
// Paint a tiled image
Rectangle bounds = g.getClipBounds();
// Determine which tiled instances intersect with bounds and draw them
int imageWidth = backgroundImage.getWidth();
int imageHeight = backgroundImage.getHeight();
int offsetX = (bounds.x / imageWidth) * imageWidth;
int offsetY = (bounds.y / imageHeight) * imageHeight;
for (int yRegistration = offsetY; yRegistration < bounds.y + bounds.height; yRegistration += imageHeight) {
for (int xRegistration = offsetX; xRegistration < bounds.x + bounds.width; xRegistration += imageWidth) {
g.drawImage(backgroundImage, xRegistration, yRegistration, null);
}
}
}
// Just paint a simple background!
super.paintComponent(g);
}
/**
* Sets the background image of this tree.
* @param backgroundImage the background image to use, null for normal background.
*/
public void setBackgroundImage(BufferedImage backgroundImage) {
// we lighten the colours a bit to make it easier to read.
if (backgroundImage == null) {
this.backgroundImage = null;
setOpaque(true);
repaint();
return;
}
RescaleOp rescaleOp = new RescaleOp(1.1f, 35, null);
// Create an RGB buffered image
BufferedImage bimage = new BufferedImage(backgroundImage.getWidth(null), backgroundImage.getHeight(null), BufferedImage.TYPE_BYTE_GRAY);
// Copy non-RGB image to the RGB buffered image
Graphics2D g = bimage.createGraphics();
g.drawImage(backgroundImage, 0, 0, null);
// Copy non-RGB image to the RGB buffered image
this.backgroundImage = rescaleOp.filter(bimage, null);
setOpaque(false);
repaint();
}
/**
* @param useBandedLookAndFeel true if the banded look&feel should be used, false for normal LAF
*/
public void setBandedLookAndFeel(boolean useBandedLookAndFeel) {
this.useBandedLookAndFeel = useBandedLookAndFeel;
if (useBandedLookAndFeel) {
super.setUI(new BandedTreeUI());
} else {
super.setUI(UIManager.getUI(this));
}
// Inform the cell renderer since it will behave slightly differently for the banded look and feel
TreeCellRenderer cellRenderer = getCellRenderer();
if (cellRenderer instanceof ExplorerCellRenderer) {
((ExplorerCellRenderer)cellRenderer).setBandedLookAndFeel(useBandedLookAndFeel);
}
}
/**
* Overide this method so that we can explicitly use a special banded look and feel even if a different
* look and feel is requested.
* @param newUI
*/
@Override
public void setUI(TreeUI newUI) {
if (useBandedLookAndFeel) {
super.setUI(new BandedTreeUI());
} else {
super.setUI(newUI);
}
}
/**
* Returns the user object stored in the node underneath specified location.
* @param location the location for the node
* @param source the component in whose coordinate space the location is
* @return the user object or null if there is no node at the given location
*/
public Object getUserObjectAt(Point location, JComponent source) {
return getUserObjectAt(SwingUtilities.convertPoint(source, location, this));
}
/**
* Returns the user object stored in the node underneath specified location.
* @param location the location for the node
* @return the user object or null if there is no node at the given location
*/
public Object getUserObjectAt(Point location) {
TreePath path = getPathForLocation(location.x, location.y);
return path == null ? null : ((DefaultMutableTreeNode)path.getLastPathComponent()).getUserObject();
}
/**
* Determines if the point in question is over a focussable point in the tree. The node at this point
* will be rendered as a hyperlink if this is a focussable point so this method can be used to check
* if the cursor should be changed or to take action when the user clicks.
* @param location The point that will be checked. This point should be relative to the explorer tree.
* @return Returns true if the point is over a hyperlink rendered node and false if not.
*/
boolean isFocusablePoint(Point location) {
// As a safety precaution, bail early if we don't have a navigation helper
if (navigationHelper == null) {
return false;
}
// Get the tree path for the specified location
TreePath path = getPathForLocation(location.x, location.y);
if (path != null) {
// We don't want to consider the icon as part of the hyperlink
Rectangle pathBounds = getPathBounds(path);
Point relativePoint = new Point(location.x - pathBounds.x, location.y - pathBounds.y);
ExplorerCellRenderer renderer = (ExplorerCellRenderer)getCellRenderer();
if (!renderer.isPointOverIcon(relativePoint)) {
Object node = path.getLastPathComponent();
if (node instanceof ExplorerGemNode) {
// Check if the gem is focusable which means it will be rendered as a hyperlink
Gem gem = ((ExplorerGemNode)node).getGem();
if (navigationHelper.isFocusable(gem)) {
return true;
}
}
}
}
// Not a focusable point
return false;
}
/**
* Saves the current state of the tree: the selected node and the expanded nodes.
*/
void saveState () {
// Remember the currently expanded nodes.
Enumeration<TreePath> expandedPaths = getExpandedDescendants(new TreePath(getModel().getRoot()));
savedExpandedPaths = new HashSet<TreePath>();
while (expandedPaths != null && expandedPaths.hasMoreElements()) {
TreePath path = expandedPaths.nextElement();
savedExpandedPaths.add(path);
}
// Remember the selected node.
savedSelectionPath = getSelectionPath();
}
/**
* Restores the state that was last saved. Does nothing if no state was ever saved. The restored
* state might not exactly match the saved state if the tree model changed in such a way that a
* full restore is not possible (ie: nodes removed).
*/
void restoreSavedState () {
for (final TreePath treePath : savedExpandedPaths) {
this.expandPath(treePath);
}
if (savedSelectionPath != null) {
setSelectionPath(savedSelectionPath);
}
}
}
/**
* Provides a customized look and feel based that is identical to the BasicTreeUI except for alternating
* bands of colour to separate the top level of nodes in the tree. When the root node is hidden this
* provides separation between what appear to be the 'root' nodes of the tree.
*/
class BandedTreeUI extends BasicTreeUI {
/** Color for the light band */
private Color lightBand = new Color(220, 220, 220, 100);
/** Color for the dark band. */
private Color darkBand = new Color(180, 180, 180, 100);
/**
* @see javax.swing.plaf.basic.BasicTreeUI#paintRow(java.awt.Graphics, java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle, javax.swing.tree.TreePath, int, boolean, boolean, boolean)
*/
@Override
protected void paintRow(Graphics g,
Rectangle clipBounds,
Insets insets,
Rectangle bounds,
TreePath path,
int row,
boolean isExpanded,
boolean hasBeenExpanded,
boolean isLeaf) {
// Don't paint the renderer if editing this row.
if(editingComponent != null && editingRow == row) {
return;
}
Component component = null;
TreeNode currentNode = (TreeNode)path.getLastPathComponent();
TreeNode rootNode = (TreeNode)getModel().getRoot();
if (currentNode != rootNode) {
// Determine the colour for this particular node so that we alternate colour for
// top level gems
TreeNode topLevelNode = getTopLevelNode(rootNode, currentNode);
int index = getModel().getIndexOfChild(rootNode, topLevelNode);
if (index % 2 == 0) {
g.setColor(darkBand);
} else {
g.setColor(lightBand);
}
// Fill in the entire space available for the current row with the previously set colour
g.fillRect(clipBounds.x, bounds.y, clipBounds.width, bounds.height);
}
component = currentCellRenderer.getTreeCellRendererComponent (tree, path.getLastPathComponent(),
tree.isRowSelected(row), isExpanded, isLeaf, row,
tree.hasFocus());
rendererPane.paintComponent (g, component, tree, bounds.x, bounds.y,
bounds.width, bounds.height, true);
}
/**
* Method to retrieve the top level node for the specified node. The top level node is defined to be
* the ancestor that is one level below the root. In other words, with the root node hidden it will
* be the top node drawn in the tree. This method should not be called on the root gem or a
* NullPointerException will be thrown.
* @param rootNode the real root node
* @param currentNode the current node whose top level parent we are searching for
* @return TreeNode the top-level parent of the current node
*/
private TreeNode getTopLevelNode(TreeNode rootNode, TreeNode currentNode) {
if (currentNode == null) {
return null;
}
// Determine if this is the top level node, otherwise recurse up our parent again.
TreeNode parentNode = currentNode.getParent();
if (parentNode == rootNode) {
return currentNode;
} else {
return getTopLevelNode(rootNode, parentNode);
}
}
}