/*
* 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.
*/
/*
* BrowserTreeCellRenderer.java
* Creation date: (2/1/01 11:21:29 AM)
* By: Luke Evans
*/
package org.openquark.gems.client.browser;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedString;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.tree.DefaultTreeCellRenderer;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous;
import org.openquark.cal.services.GemEntity;
import org.openquark.util.WildcardPatternMatcher;
/**
* The BrowserTreeCellRenderer sets the look of the BrowserTree cells based on their content.
* Creation date: (2/1/01 11:21:29 AM)
* @author Luke Evans
*/
public class BrowserTreeCellRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = -1966243017498574478L;
/** The padding between the name and type when displaying types for the gem tree node. */
private static final String NAME_TYPE_PAD = " ";
private static final Color NON_VISIBLE_COLOR = UIManager.getColor("TextField.inactiveForeground");
private static final Color CURRENT_MODULE_COLOR = Color.MAGENTA;
private static final Color TYPE_SIGNATURE_COLOR = Color.GRAY;
private static final Color SEARCH_HIGHLIGHT_COLOR = Color.MAGENTA.darker();
private static final ImageIcon constructorIcon;
private static final ImageIcon scIcon;
private static final ImageIcon workspaceIcon;
private static final ImageIcon drawerIcon;
private static final ImageIcon namespaceIcon;
private static final ImageIcon searchResultIcon;
static {
// Make icon objects
constructorIcon = new ImageIcon(Object.class.getResource("/Resources/Gem_Yellow.gif"));
scIcon = new ImageIcon(Object.class.getResource("/Resources/Gem_Red.gif"));
workspaceIcon = new ImageIcon(Object.class.getResource("/Resources/vault.gif"));
drawerIcon = new ImageIcon(Object.class.getResource("/Resources/drawer.png"));
namespaceIcon = new ImageIcon(Object.class.getResource("/Resources/nav_namespace.png"));
searchResultIcon = new ImageIcon(Object.class.getResource("/Resources/flashlight.gif"));
}
/** The BrowserTree we are rendering for. */
private BrowserTree browserTree;
/** The BrowserTreeModel of the tree we are rendering for. */
private BrowserTreeModel browserTreeModel;
/** The tree node we are rendering. */
private BrowserTreeNode treeNode;
/**
* Default BrowserTreeCellRenderer constructor
*/
public BrowserTreeCellRenderer() {
super();
}
/**
* {@inheritDoc}
* This is the default getTreeCellRendererComponent to add logic for selecting icons etc.
*/
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
// Remember the important stuff for later when we are painting
browserTree = (BrowserTree) tree;
browserTreeModel = (BrowserTreeModel) tree.getModel();
treeNode = (BrowserTreeNode) value;
// We want to do everything 'normal' except for a few tweaks
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
// The node's display text.
String displayString = treeNode.getDisplayedString();
if (leaf) {
// If the node is a leaf we know it must be a GemTreeNode
if (isDataConstructor(value)) {
setIcon(constructorIcon);
} else {
setIcon(scIcon);
}
// Paint non-visible entities in a different color
GemEntity gemEntity = (GemEntity) treeNode.getUserObject();
if (!browserTreeModel.isVisibleGem(gemEntity)) {
setForeground(NON_VISIBLE_COLOR);
}
// Update the text if we are supposed to show type signatures
if (browserTree.getDisplayTypeExpr()) {
ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(browserTreeModel.getPerspective().getWorkingModuleTypeInfo());
displayString += NAME_TYPE_PAD + gemEntity.getTypeExpr().toString(namingPolicy);
}
} else {
if (isWorkspaceRoot(value)) {
// Set the workspace icon
setIcon(workspaceIcon);
} else if (isDrawer(value)) {
GemDrawer gemDrawer = (GemDrawer)value;
ModuleName moduleName = gemDrawer.getModuleName();
// Set the drawer icon
if (gemDrawer.isNamespaceNode()) {
setIcon(namespaceIcon);
} else {
setIcon(drawerIcon);
}
// Draw non-visible modules in a different color and highlight the working module.
if (gemDrawer.isNamespaceNode()) {
if (!browserTreeModel.doesNamespaceContainVisibleModules(moduleName)) {
setForeground(NON_VISIBLE_COLOR);
}
} else if (!browserTreeModel.isVisibleModule(moduleName)) {
setForeground(NON_VISIBLE_COLOR);
} else if (browserTree.getHighlightCurrentModule() && browserTreeModel.isWorkingModule(moduleName)) {
setForeground(CURRENT_MODULE_COLOR);
}
} else if (isSearchResultParentNode(value)) {
// Set the search result icon
setIcon(searchResultIcon);
}
// If it fails the tests above, this must be one of those 'category' folders.
// Note: This will need an icon later...
}
// Set the node's displayed text.
setText(displayString);
return this;
}
/**
* Sets up the basic AttributedString to use for drawing the node names.
* Later this string is modified to draw in different colours, sizes, etc.
* @return AttributedString
*/
private AttributedString getAttributedString() {
AttributedString theString = new AttributedString(getText());
GemEntity gemEntity = (GemEntity) treeNode.getUserObject();
Color foreground = (selected) ? getTextSelectionColor() : getTextNonSelectionColor();
if (browserTreeModel.isVisibleGem(gemEntity)) {
theString.addAttribute(TextAttribute.FOREGROUND, foreground);
} else {
theString.addAttribute(TextAttribute.FOREGROUND, NON_VISIBLE_COLOR);
}
theString.addAttribute(TextAttribute.FONT, getFont());
return theString;
}
/**
* @return if the renderer should render any part of the node text with attributes different from those
* which will be specified in the graphics context (eg. custom fonts or colours), then the corresponding
* AttributedString will be returned with the desired attributes. Otherwise null.
*/
private AttributedString getStylizedName() {
AttributedString stylizedName = null;
if (isGemNode(treeNode) && browserTree.getDisplayTypeExpr()) {
// If required draw type signatures next to gem names in a different font.
stylizedName = getAttributedString();
GemEntity gemEntity = (GemEntity) treeNode.getUserObject();
ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(browserTreeModel.getPerspective().getWorkingModuleTypeInfo());
int nameLength = treeNode.getDisplayedString().length() + NAME_TYPE_PAD.length();
int typeLength = gemEntity.getTypeExpr().toString(namingPolicy).length();
// Make the type name appear in italic and in a smaller size
stylizedName.addAttribute(TextAttribute.FONT, getFont().deriveFont(Font.ITALIC), nameLength, nameLength + typeLength);
if (browserTreeModel.isVisibleGem(gemEntity)) {
// Visible entities get a different colour for type signature.
stylizedName.addAttribute(TextAttribute.FOREGROUND, TYPE_SIGNATURE_COLOR, nameLength, nameLength + typeLength);
}
}
if (isSearchResultNode(treeNode)) {
// For search result nodes we highlight the text that matches the search string.
// Note that a search result node is also a gem node.
if (stylizedName == null) {
stylizedName = getAttributedString();
}
String searchString = browserTreeModel.getSearchString();
if (searchString != null) {
Pattern searchPattern = Pattern.compile(WildcardPatternMatcher.wildcardPatternToRegExp(searchString), Pattern.CASE_INSENSITIVE);
Matcher matcher = searchPattern.matcher(getText());
// find() is a method on Matcher that returns true for as long as there are
// additional matches from the end of the last match to the end of the string.
// The Matcher itself is stateful and stores the location of the start and end of the last match,
// and a call find() is akin to Iterator.next() in that it alters these internal states.
while (matcher.find()) {
stylizedName.addAttribute(TextAttribute.FOREGROUND, SEARCH_HIGHLIGHT_COLOR, matcher.start(), matcher.end());
}
}
}
return stylizedName;
}
/**
* Overrides paint to draw the gem type signatures in a different font/color and to
* highlight gem search results in a different colour.
*/
@Override
public void paint(Graphics g) {
// Draw the normal representation first. We draw over it for our custom changes.
// TODO: This is slightly inefficient. However, even in the drawn-over case, we still need
// to call the super method to draw the icon.
super.paint(g);
AttributedString stylizedName = getStylizedName();
if (stylizedName != null) {
Graphics2D g2 = (Graphics2D)g;
Color background = (selected) ? getBackgroundSelectionColor() : getBackgroundNonSelectionColor();
// Update our border and background to match the new stylized text
TextLayout layout = new TextLayout(stylizedName.getIterator(), g2.getFontRenderContext());
int width = layout.getBounds().getBounds().width;
// Erase the previous background and text
g2.setColor(getBackground());
g2.fillRect(getLabelStart(), 0, getWidth(), getHeight());
// Draw the new background
g2.setColor(background);
g2.fillRect(getLabelStart(), 0, width + 2, getHeight());
// Draw the focus border
Color focusBorderColor = getBorderSelectionColor();
if (hasFocus && focusBorderColor != null) {
g2.setColor(focusBorderColor);
g2.drawRect(getLabelStart(), 0, width + 2, getHeight() - 1);
}
// Finally draw the actual text
g2.drawString(stylizedName.getIterator(), getLabelStart() + 1, getHeight() - 3);
}
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getPreferredSize() {
Dimension preferredSize = super.getPreferredSize();
AttributedString stylizedName = getStylizedName();
if (stylizedName != null) {
Graphics2D g2d = ((Graphics2D)getGraphics());
if (g2d != null) {
// Override to get the correct size when taking into account the fact that the attributed string
// size is different from the size when all text is rendered with the component's font.
FontRenderContext fontRenderContext = g2d.getFontRenderContext();
float unadjustedTextAdvance = (new TextLayout(getText(), getFont(), fontRenderContext)).getAdvance();
float adjustedTextAdvance = (new TextLayout(stylizedName.getIterator(), fontRenderContext)).getAdvance();
preferredSize.width += (int)(adjustedTextAdvance - unadjustedTextAdvance);
}
}
return preferredSize;
}
/**
* Returns the x-coordinate at which point the text portion of the tree node starts.
* @return int
*/
private int getLabelStart() {
Icon icon = getIcon();
if (icon != null) {
return icon.getIconWidth() + Math.max(0, getIconTextGap() - 1);
}
return 0;
}
/**
* Determine if this tree element is representing a data constructor.
* @return boolean true if a constructor
* @param value Object the tree element
*/
private boolean isDataConstructor(Object value) {
if (value instanceof BrowserTreeNode) {
BrowserTreeNode gem = (BrowserTreeNode)value;
GemEntity gemEntity = (GemEntity)gem.getUserObject();
return gemEntity.isDataConstructor();
}
return false;
}
/**
* Determine if this tree element is representing a drawer.
* @return boolean true if a drawer (any type of drawer)
* @param value Object the tree element
*/
private boolean isDrawer(Object value) {
return (value instanceof GemDrawer);
}
/**
* Determine if this tree element is representing the workspace.
* @return boolean true if it's the workspace.
* @param value Object the tree element
*/
private boolean isWorkspaceRoot(Object value) {
return (value instanceof WorkspaceRootTreeNode);
}
/**
* Determine if this tree element represents a gem node.
* @return boolean true if a gem node
* @param value Object the tree element
*/
private boolean isGemNode(Object value) {
return (value instanceof GemTreeNode);
}
/**
* Determine if this tree element represent a search result gem tree node.
* @return boolean true if a search results gem tree node
* @param value Object the tree element
*/
private boolean isSearchResultNode(Object value) {
return (value instanceof SearchResultGemTreeNode);
}
/**
* Determine if this tree element represents the search result parent node.
* @return boolean true if a search result parent node
* @param value Object the tree element
*/
private boolean isSearchResultParentNode(Object value) {
return (value instanceof SearchResultRootTreeNode);
}
}