src.gov.nasa.worldwind.util.tree.BasicTreeLayout Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.util.tree;
import com.jogamp.opengl.util.awt.TextRenderer;
import com.jogamp.opengl.util.texture.TextureCoords;
import gov.nasa.worldwind.WWObjectImpl;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.event.SelectEvent;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.util.*;
import javax.media.opengl.*;
import java.awt.*;
import java.awt.geom.*;
import java.beans.*;
import java.util.*;
import java.util.List;
/**
* Layout that draws a {@link Tree} similar to a file browser tree.
*
* @author pabercrombie
* @version $Id: BasicTreeLayout.java 1171 2013-02-11 21:45:02Z dcollins $
*/
public class BasicTreeLayout extends WWObjectImpl implements TreeLayout, Scrollable, PreRenderable
{
/** Tree that is drawn by this layout. */
protected Tree tree;
/** Frame that contains the tree. */
protected ScrollFrame frame;
/** Attributes to use when the tree is not highlighted. */
protected TreeAttributes normalAttributes = new BasicTreeAttributes();
/** Attributes to use when the frame is highlighted. */
protected TreeAttributes highlightAttributes = new BasicTreeAttributes();
/** Active attributes, either normal or highlight. */
protected TreeAttributes activeAttributes = new BasicTreeAttributes();
/** The attributes used if attributes are not specified. */
protected static final TreeAttributes defaultAttributes;
/** Indicates whether or not the tree is highlighted. */
protected boolean highlighted;
/** Support for setting up and restoring picking state, and resolving the picked object. */
protected PickSupport pickSupport = new PickSupport();
/**
* This field is set by {@link #makeVisible(TreePath)}, and read by {@link #scrollToNode(gov.nasa.worldwind.render.DrawContext)}
* during rendering.
*/
protected TreeNode scrollToNode;
/** Cache the rendered size of the tree and recompute when the tree changes. */
protected Dimension size;
/** Indicates that the tree size needs to be computed. */
protected boolean mustRecomputeSize = true;
/** Indicates that the tree layout needs to be computed. */
protected boolean mustRecomputeLayout = true;
/** Indicates that node description text must be drawn. */
protected boolean showDescription = true;
/** Indicates that a triangle must be drawn to indicate if a group node is expanded or collapsed. */
protected boolean drawNodeStateSymbol = true;
/** Indicates that a checkbox must be drawn for each node to indicate if the node is selected or not. */
protected boolean drawSelectedSymbol = true;
/** Indicates whether or not the description text will be wrapped to fit the frame. */
protected boolean wrapText = true;
/**
* Maximum number of lines of wrapped description text to draw. If the description exceeds this it will be cut off
* at this number of lines, with a trailing "...".
*/
protected int maxWrappedLines = 2;
/** Cache of computed text bounds. */
protected BoundedHashMap textCache = new BoundedHashMap();
/** Cache of computed node layout data. */
protected BoundedHashMap layoutCache = new BoundedHashMap();
/** Cache of node layouts. This list is populated when the tree layout is computed. */
protected java.util.List treeNodes = new ArrayList();
/**
* A little extra space is added to the tree dimensions to give the tree a little bit of separation from the
* scrollable frame. This value determines the amount of padding, in pixels.
*/
protected int padding = 10;
// Computed each frame
protected long frameNumber = -1L;
protected long attributesFrameNumber = -1L;
/** Location of the lower left corner of the tree, in GL coordinates. */
protected Point screenLocation;
/**
* Time at which the rendered tree last changed. Used to indicate when the ScrollFrame needs to refresh the rendered
* representation
*/
protected long updateTime;
/** Frame size when the tree layout was last computed. */
protected Dimension previousFrameSize;
/** Frame size when the tree size was last computed. */
protected Dimension previousSizeBounds;
/** The height of one line of text in the active font. */
protected int lineHeight;
/** Number of nodes in the tree, used to set a bound on the text cache. */
protected int nodeCount;
/** Indentation in pixels applied to each new level of the tree. */
protected int indent;
static
{
defaultAttributes = new BasicTreeAttributes();
}
/**
* Create a layout for a tree.
*
* @param tree Tree to create layout for.
*/
public BasicTreeLayout(Tree tree)
{
this(tree, null);
}
/**
* Create a layout for a tree, at a screen location.
*
* @param tree Tree to create layout for.
* @param x X coordinate of the upper left corner of the tree frame.
* @param y Y coordinate of the upper left corner of the tree frame, measured from the top of the screen.
*/
public BasicTreeLayout(Tree tree, int x, int y)
{
this(tree, new Offset((double) x, (double) y, AVKey.PIXELS, AVKey.INSET_PIXELS));
}
/**
* Create a layout for a tree, at a screen location.
*
* @param tree Tree to create layout for.
* @param screenLocation The location of the upper left corner of the tree frame. The offset is interpreted relative
* to the lower left corner of the screen.
*/
public BasicTreeLayout(Tree tree, Offset screenLocation)
{
this.tree = tree;
this.frame = this.createFrame();
this.frame.setContents(this);
// Listen for property changes in the frame. These will be forwarded to the layout listeners. This is necessary
// to pass AVKey.REPAINT events up the layer.
this.frame.addPropertyChangeListener(this);
// Add listener for tree events so that we can recompute the tree size when things change. Because TreeLayout
// is a WWObject, it sends property change events to its listeners. Since Tree is likely to listen for property
// change events on TreeLayout, we add an anonymous listener to avoid an infinite cycle of property change
// events between TreeLayout and Tree.
this.tree.addPropertyChangeListener(new PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent propertyChangeEvent)
{
// Ignore events originated by this TreeLayout, and repaint events. There is no need to recompute the
// tree layout just because a repaint was triggered.
if (propertyChangeEvent.getSource() != BasicTreeLayout.this
&& !AVKey.REPAINT.equals(propertyChangeEvent.getPropertyName()))
{
BasicTreeLayout.this.invalidate();
}
}
});
if (screenLocation != null)
this.setScreenLocation(screenLocation);
}
/**
* Indicates whether or not the layout wraps the node description to multiple lines. Note that the node title is
* never wrapped, only the description.
*
* @return {@code true} if the description will be wrapped to fit the frame.
*/
public boolean isWrapText()
{
return this.wrapText;
}
/**
* Specifies whether or not the layout wraps the node description to multiple lines. Note that the node title is
* never wrapped, only the description.
*
* @param wrapText {@code true} if the description text must be wrapped to fit the frame.
*/
public void setWrapText(boolean wrapText)
{
this.wrapText = wrapText;
}
/**
* Get the size of the symbol that indicates that a node is expanded or collapsed.
*
* @return The size of the node state symbol.
*/
protected Dimension getNodeStateSymbolSize()
{
return new Dimension(12, 12);
}
/**
* Get the size of the symbol that indicates that a node is selected or not selected.
*
* @return The size of the node selection symbol.
*/
protected Dimension getSelectedSymbolSize()
{
return new Dimension(12, 12);
}
/**
* Should the node renderer include node descriptions?
*
* @return True if the renderer should renderer node descriptions.
*/
public boolean isShowDescription()
{
return this.showDescription;
}
/**
* Set the renderer to renderer node descriptions (additional text rendered under the node title).
*
* @param showDescription True if the description should be rendered. False if only the icon and title should be
* rendered.
*/
public void setShowDescription(boolean showDescription)
{
this.showDescription = showDescription;
}
/**
* Will the renderer draw a symbol to indicate that the node is selected? The default symbol is a checkbox.
*
* @return True if the node selected symbol (a checkbox by default) will be drawn.
*/
public boolean isDrawSelectedSymbol()
{
return this.drawSelectedSymbol;
}
/**
* Set whether or not the renderer will draw a symbol to indicate that the node is selected. The default symbol is a
* checkbox.
*
* @param drawSelectedSymbol True if the node selected symbol (a checkbox by default) will be drawn.
*/
public void setDrawSelectedSymbol(boolean drawSelectedSymbol)
{
this.drawSelectedSymbol = drawSelectedSymbol;
}
/**
* Will the renderer draw a symbol to indicate that the node is expanded or collapsed (applies only to non-leaf
* nodes). The default symbol is a triangle pointing to the right, for collapsed nodes, or down for expanded nodes.
*
* @return True if the node state symbol (default is a triangle pointing either to the right or down) will be
* drawn.
*/
public boolean isDrawNodeStateSymbol()
{
return this.drawNodeStateSymbol;
}
/**
* Specifies the maximum number of lines of text wrapped description text to draw. If the description exceeds this
* number of lines it will be cut off with a trailing "...".
*
* @return Maximum number of lines of description text that will be drawn.
*/
public int getMaxWrappedLines()
{
return this.maxWrappedLines;
}
/**
* Indicates the maximum number of lines of text wrapped description text to draw. If the description exceeds this
* number of lines it will be cut off with a trailing "...".
*
* @param maxLines Maximum number of lines of description text that will be drawn.
*/
public void setMaxWrappedLines(int maxLines)
{
if (maxLines != this.maxWrappedLines)
{
this.maxWrappedLines = maxLines;
// Need to re-wrap the text because the number of lines changes.
this.invalidate();
this.invalidateWrappedText();
}
}
/**
* Set whether or not the renderer will draw a symbol to indicate that the node is expanded or collapsed (applies
* only to non-leaf nodes). The default symbol is a triangle pointing to the right, for collapsed nodes, or down for
* expanded nodes.
*
* @param drawNodeStateSymbol True if the node state symbol (default is a triangle pointing either to the right or
* down) will be drawn.
*/
public void setDrawNodeStateSymbol(boolean drawNodeStateSymbol)
{
this.drawNodeStateSymbol = drawNodeStateSymbol;
}
/** {@inheritDoc} */
public long getUpdateTime()
{
return this.updateTime;
}
/**
* Create the frame that the tree will be rendered inside.
*
* @return A new frame.
*/
protected ScrollFrame createFrame()
{
return new ScrollFrame();
}
/**
* Get the size of the entire tree, including the part that is not visible in the scroll pane.
*
* @param dc Draw context.
* @param frameSize Size of the frame the tree will be rendered into. May be {@code null}.
*
* @return Size of the rendered tree.
*/
public Dimension getSize(DrawContext dc, Dimension frameSize)
{
this.updateAttributes(dc);
// Computing the size of rendered text is expensive, so only recompute the tree size when necessary.
if (this.mustRecomputeSize(frameSize))
{
TreeModel model = this.tree.getModel();
TreeNode root = model.getRoot();
this.size = new Dimension();
this.nodeCount = 0;
this.computeSize(this.tree, root, dc, frameSize, this.size, 0, 1);
// Limit the caches to the number of the nodes that are visible in the tree.
this.layoutCache.setCapacity(this.nodeCount);
this.textCache.setCapacity(this.nodeCount * 2); // Each node can have two strings (title and description)
// Add a little padding to the dimension so that no text gets clipped off by the scrollable frame
this.size.height += this.padding;
this.mustRecomputeSize = false;
this.previousSizeBounds = frameSize;
this.markUpdated();
}
return this.size;
}
/**
* Compute the size of a tree. This method invokes itself recursively to calculate the size of the tree, taking into
* account which nodes are expanded and which are not. This computed size will be stored in the {@code size}
* parameter.
*
* @param tree Tree that contains the root node.
* @param root Root node of the subtree to find the size of. This does not need to be the root node of the
* tree.
* @param dc Draw context.
* @param frameSize Size of the frame into which the tree will render.
* @param size Size object to modify. This method will change the width and height fields of {@code size} to
* hold the new size of the tree.
* @param x Horizontal coordinate of the start of this node. This parameter must be zero. This method calls
* itself recursively and changes the {@code x} parameter to reflect the indentation level of
* different levels of the tree.
* @param level Level of this node. Tree root node is level 1, children of the root are level 2, etc.
*/
protected void computeSize(Tree tree, TreeNode root, DrawContext dc, Dimension frameSize, Dimension size, int x,
int level)
{
this.nodeCount++;
TreeAttributes attributes = this.getActiveAttributes();
Dimension thisSize = this.getNodeSize(dc, frameSize, x, root, attributes);
int indent = 0;
if (this.mustDisplayNode(root, level))
{
int thisWidth = thisSize.width + x;
if (thisWidth > size.width)
size.width = thisWidth;
size.height += thisSize.height;
size.height += attributes.getRowSpacing();
indent = this.indent;
}
if (tree.isNodeExpanded(root))
{
for (TreeNode child : root.getChildren())
{
this.computeSize(tree, child, dc, frameSize, size, x + indent, level + 1);
}
}
}
/** Force the layout to recompute the size of the tree. */
public void invalidate()
{
this.markUpdated();
this.mustRecomputeSize = true;
this.mustRecomputeLayout = true;
}
/** Set the {@link #updateTime} to the current system time, marking the Scrollable contents as updated. */
protected void markUpdated()
{
this.updateTime = System.currentTimeMillis();
}
/**
* Determine if a node needs to be displayed. This method examines only one node at a time. It does not take into
* account that the node's parent may be in the collapsed state, in which the children are not rendered.
*
* @param node Node to test.
* @param level Level of the node in the tree. The root node is level 1, its children are level 2, etc.
*
* @return True if the node must be displayed.
*/
protected boolean mustDisplayNode(TreeNode node, int level)
{
return node.isVisible() && (level > 1 || this.getActiveAttributes().isRootVisible());
}
/** {@inheritDoc} */
public void preRender(DrawContext dc)
{
// Adjust scroll position if an application has requested that the layout scroll to make a node visible.
this.scrollToNode(dc);
this.frame.preRender(dc);
}
/** {@inheritDoc} */
public void render(DrawContext dc)
{
this.frame.render(dc);
}
/**
* Scroll the frame to make a the node set in {@link #scrollToNode} node visible. Does nothing if {@link
* #scrollToNode} is null.
*
* @param dc Draw context.
*/
protected synchronized void scrollToNode(DrawContext dc)
{
if (this.scrollToNode != null)
{
// Update the frame bounds to make sure that the frame's scroll model includes the full extent of the tree
ScrollFrame frame = this.getFrame();
frame.updateBounds(dc);
Point drawPoint = new Point(0, 0);
Rectangle bounds = this.findNodeBounds(this.scrollToNode, this.tree.getModel().getRoot(), dc,
frame.getBounds(dc).getSize(), drawPoint, 1);
// Calculate a scroll position that will bring the node to the top of the visible area. Subtract the row spacing
// to avoid clipping off the top of the node.
int scroll = (int) Math.abs(bounds.getMaxY()) - this.getActiveAttributes().getRowSpacing();
this.frame.getScrollBar(AVKey.VERTICAL).setValue(scroll);
this.scrollToNode = null;
}
}
/** {@inheritDoc} */
public void renderScrollable(DrawContext dc, Point location, Dimension frameSize, Rectangle clipBounds)
{
TreeModel model = this.tree.getModel();
TreeNode root = model.getRoot();
this.screenLocation = location;
this.updateAttributes(dc);
if (this.frameNumber != dc.getFrameTimeStamp())
{
if (this.mustRecomputeTreeLayout(frameSize))
{
this.treeNodes.clear();
Point drawPoint = new Point(0, this.size.height);
this.computeTreeLayout(root, dc, frameSize, drawPoint, 1, treeNodes);
this.previousFrameSize = frameSize;
this.mustRecomputeLayout = false;
}
this.frameNumber = dc.getFrameTimeStamp();
}
try
{
if (dc.isPickingMode())
{
this.pickSupport.clearPickList();
this.pickSupport.beginPicking(dc);
}
this.renderNodes(dc, location, treeNodes, clipBounds);
}
finally
{
if (dc.isPickingMode())
{
this.pickSupport.endPicking(dc);
this.pickSupport.resolvePick(dc, dc.getPickPoint(), dc.getCurrentLayer());
}
}
}
/**
* Indicates whether or not the tree layout needs to be recomputed.
*
* @param frameSize Size of the frame that holds the tree.
*
* @return {@code true} if the layout needs to be recomputed, otherwise {@code false}.
*/
protected boolean mustRecomputeTreeLayout(Dimension frameSize)
{
return this.mustRecomputeLayout || this.previousFrameSize == null
|| this.previousFrameSize.width != frameSize.width;
}
/**
* Indicates whether or not the tree size needs to be recomputed.
*
* @param frameSize Size of the frame that holds the tree. Size may be null if the frame size is not known.
*
* @return {@code true} if the size needs to be recomputed, otherwise {@code false}.
*/
protected boolean mustRecomputeSize(Dimension frameSize)
{
return this.mustRecomputeSize
|| (this.previousSizeBounds == null && frameSize != null)
|| (frameSize != null && this.previousSizeBounds.width != frameSize.width);
}
/**
* Update the active attributes for the current frame, and compute other properties that are based on the active
* attributes. This method only computes attributes once for each frame. Subsequent calls in the same frame will not
* recompute the attributes.
*
* @param dc Current draw context.
*/
protected void updateAttributes(DrawContext dc)
{
if (dc.getFrameTimeStamp() != this.attributesFrameNumber)
{
this.determineActiveAttributes();
this.indent = this.computeIndentation();
this.lineHeight = this.computeMaxTextHeight(dc);
this.attributesFrameNumber = dc.getFrameTimeStamp();
}
}
/**
* Compute the indentation, in pixels, applied to each new level of the tree.
*
* @return indention (in pixels) to apply to each new level in the tree.
*/
protected int computeIndentation()
{
int iconWidth = this.getActiveAttributes().getIconSize().width;
int iconSpacing = this.getActiveAttributes().getIconSpace();
int checkboxWidth = this.getSelectedSymbolSize().width;
// Compute the indentation to make the checkbox of the child level line up the icon of the parent level
return checkboxWidth + iconSpacing + ((iconWidth - checkboxWidth) / 2);
}
/**
* Determine the maximum height of a line of text using the active font.
*
* @param dc Current draw context.
*
* @return The maximum height of a line of text.
*/
protected int computeMaxTextHeight(DrawContext dc)
{
TreeAttributes attributes = this.getActiveAttributes();
// Use underscore + capital E with acute accent as max height
Rectangle2D bounds = this.getTextBounds(dc, "_\u00c9", attributes.getFont());
double lineHeight = Math.abs(bounds.getY());
return (int) Math.max(lineHeight, attributes.getIconSize().height);
}
/**
* Render a list of tree nodes.
*
* @param dc Current draw context.
* @param drawPoint Point in GL coordinates (origin bottom left corner of the screen) that locates the bottom left
* corner of the tree.
* @param nodes Nodes to draw.
* @param clipBounds Pixels outside of this rectangle will be discarded. Any nodes that do not intersect this
* rectangle will not be drawn.
*/
protected void renderNodes(DrawContext dc, Point drawPoint, Iterable nodes, Rectangle clipBounds)
{
// Collect the nodes that are actually visible in the scroll area in a list.
List visibleNodes = new ArrayList();
for (NodeLayout layout : nodes)
{
layout.reset(drawPoint);
if (this.intersectsFrustum(dc, layout, clipBounds))
visibleNodes.add(layout);
// // Draw a box around the node bounds. Useful for debugging node layout
//
// GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
// gl.glBegin(GL2.GL_LINE_LOOP);
// gl.glVertex2d(layout.screenBounds.getMinX(), layout.screenBounds.getMinY());
// gl.glVertex2d(layout.screenBounds.getMaxX(), layout.screenBounds.getMinY());
// gl.glVertex2d(layout.screenBounds.getMaxX(), layout.screenBounds.getMaxY());
// gl.glVertex2d(layout.screenBounds.getMinX(), layout.screenBounds.getMaxY());
// gl.glEnd();
}
if (this.isDrawNodeStateSymbol())
this.drawTriangles(dc, visibleNodes);
if (this.isDrawSelectedSymbol())
this.drawCheckboxes(dc, visibleNodes);
// If not picking, draw text and icons. Otherwise just draw pickable rectangles tagged with the node. Unlike
// the toggle and select controls, selecting the node does not mean anything to the tree, but it may mean
// something to an application controller.
if (!dc.isPickingMode())
{
this.drawIcons(dc, visibleNodes);
this.drawText(dc, visibleNodes);
if (this.isShowDescription())
this.drawDescriptionText(dc, visibleNodes);
}
else
{
this.pickTextAndIcon(dc, visibleNodes);
}
}
/**
* Determines whether a node intersects the view frustum.
*
* @param dc the current draw context.
* @param layout node to test intersection of.
* @param scrollBounds bounds of the area currently visible in the scroll frame.
*
* @return {@code true} If the frame intersects the frustum, otherwise {@code false}.
*/
protected boolean intersectsFrustum(DrawContext dc, NodeLayout layout, Rectangle scrollBounds)
{
//noinspection SimplifiableIfStatement
if (!scrollBounds.intersects(layout.screenBounds))
return false;
return !dc.isPickingMode() || dc.getPickFrustums().intersectsAny(layout.pickScreenBounds);
}
/**
* Draw pick rectangles over the icon and text areas the visible nodes.
*
* @param dc Current draw context.
* @param nodes Visible nodes.
*/
protected void pickTextAndIcon(DrawContext dc, Iterable nodes)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
try
{
gl.glBegin(GL2.GL_QUADS);
for (NodeLayout layout : nodes)
{
Color color = dc.getUniquePickColor();
PickedObject pickedObject = new PickedObject(color.getRGB(), layout.node);
pickedObject.setValue(AVKey.HOT_SPOT, this.getFrame());
this.pickSupport.addPickableObject(pickedObject);
gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
float minX = (float) layout.drawPoint.x;
float minY = (float) layout.drawPoint.y;
float maxX = (float) layout.screenBounds.getMaxX();
float maxY = (float) layout.screenBounds.getMaxY();
gl.glVertex2f(minX, maxY);
gl.glVertex2f(maxX, maxY);
gl.glVertex2f(maxX, minY);
gl.glVertex2f(minX, minY);
}
}
finally
{
gl.glEnd(); // Quads
}
}
/**
* Draw the main line of text for a list of tree nodes.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawText(DrawContext dc, Iterable nodes)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
TreeAttributes attributes = this.getActiveAttributes();
Color color = attributes.getColor();
float[] colorRGB = color.getRGBColorComponents(null);
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(),
attributes.getFont(), true, false, false);
gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL);
try
{
textRenderer.begin3DRendering();
textRenderer.setColor(colorRGB[0], colorRGB[1], colorRGB[2], 1);
for (NodeLayout layout : nodes)
{
String text = this.getText(layout.node);
Rectangle2D textBounds = this.getTextBounds(dc, text, attributes.getFont());
// Calculate height of text from baseline to top of text. Note that this does not include descenders
// below the baseline.
int textHeight = (int) Math.abs(textBounds.getY());
int vertAdjust = layout.bounds.height - textHeight - (this.lineHeight - textHeight) / 2;
textRenderer.draw(text, layout.drawPoint.x, layout.drawPoint.y + vertAdjust);
}
}
finally
{
textRenderer.end3DRendering();
}
}
/**
* Draw the description text for tree nodes. The description text is drawn under the main line of text.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawDescriptionText(DrawContext dc, Iterable nodes)
{
TreeAttributes attributes = this.getActiveAttributes();
Color color = attributes.getColor();
float[] colorRGB = color.getRGBColorComponents(null);
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(),
attributes.getDescriptionFont(), true, false, false);
MultiLineTextRenderer mltr = new MultiLineTextRenderer(textRenderer);
try
{
textRenderer.begin3DRendering();
textRenderer.setColor(colorRGB[0], colorRGB[1], colorRGB[2], 1);
for (NodeLayout layout : nodes)
{
String description = layout.node.getDescription();
if (description != null)
{
String wrappedText = this.computeWrappedText(dc, layout.node, attributes.getDescriptionFont(),
(int) (layout.screenBounds.getMaxX() - layout.drawPoint.x));
int vertAdjust = layout.bounds.height - this.lineHeight;
mltr.draw(wrappedText, layout.drawPoint.x, layout.drawPoint.y + vertAdjust);
}
}
}
finally
{
textRenderer.end3DRendering();
}
}
/**
* Draw icons for a tree nodes.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawIcons(DrawContext dc, Iterable nodes)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
try
{
gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL);
gl.glEnable(GL.GL_TEXTURE_2D);
TreeAttributes attributes = this.getActiveAttributes();
Dimension iconSize = attributes.getIconSize();
gl.glColor4d(1d, 1d, 1d, 1);
WWTexture activeTexture = null;
for (NodeLayout layout : nodes)
{
WWTexture texture = layout.node.getTexture();
if (texture == null)
continue;
// Check to see if this node's icon is the same as the previous node. If so, there's no need to rebind
// the texture.
boolean textureBound;
// noinspection SimplifiableIfStatement
if ((activeTexture != null) && (texture.getImageSource() == activeTexture.getImageSource()))
{
textureBound = true;
}
else
{
textureBound = texture.bind(dc);
if (textureBound)
activeTexture = texture;
}
if (textureBound)
{
// If the total node height is greater than the image height, vertically center the image
int vertAdjustment = 0;
if (iconSize.height < layout.bounds.height)
{
vertAdjustment = layout.bounds.height - iconSize.height
- (this.lineHeight - iconSize.height) / 2;
}
try
{
gl.glPushMatrix();
TextureCoords texCoords = activeTexture.getTexCoords();
gl.glTranslated(layout.drawPoint.x, layout.drawPoint.y + vertAdjustment, 1.0);
gl.glScaled((double) iconSize.width, (double) iconSize.width, 1d);
dc.drawUnitQuad(texCoords);
}
finally
{
gl.glPopMatrix();
}
layout.drawPoint.x += attributes.getIconSize().width + attributes.getIconSpace();
}
}
}
finally
{
gl.glDisable(GL.GL_TEXTURE_2D);
gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
}
}
/**
* Draw check boxes. Each box includes a check mark is the node is selected, or is filled with a gradient if the
* node is partially selected.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawCheckboxes(DrawContext dc, Iterable nodes)
{
// The check boxes are drawn in three passes:
// 1) Draw filled background for partially selected nodes
// 2) Draw check marks for selected nodes
// 3) Draw checkbox outlines
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
Dimension symbolSize;
if (!dc.isPickingMode())
{
this.drawFilledCheckboxes(dc, nodes); // Draw filled boxes for partially selected nodes
this.drawCheckmarks(dc, nodes); // Draw check marks for selected nodes
symbolSize = this.getSelectedSymbolSize();
}
else
{
// Make the pickable area of the checkbox a little bigger than the actual box so that it is easier to hit.
symbolSize = new Dimension(this.getSelectedSymbolSize().width + this.getActiveAttributes().getIconSpace(),
this.lineHeight + this.getActiveAttributes().getRowSpacing());
}
// In picking mode all of the boxes can be drawn as filled quads. Otherwise, each box is drawn as a
// separate line loop
if (dc.isPickingMode())
{
gl.glBegin(GL2.GL_QUADS);
}
try
{
for (NodeLayout layout : nodes)
{
int vertAdjust = layout.bounds.height - symbolSize.height
- (this.lineHeight - symbolSize.height) / 2;
int x = layout.drawPoint.x;
int y = layout.drawPoint.y + vertAdjust;
int width = symbolSize.width;
if (!dc.isPickingMode())
{
// Draw a hollow box uses a line loop
gl.glBegin(GL2.GL_LINE_LOOP);
try
{
gl.glVertex2f(x + width, y + symbolSize.height + 0.5f);
gl.glVertex2f(x, y + symbolSize.height + 0.5f);
gl.glVertex2f(x, y);
gl.glVertex2f(x + width, y + 0.5f);
}
finally
{
gl.glEnd();
}
}
// Otherwise draw a filled quad
else
{
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
this.pickSupport.addPickableObject(colorCode, this.createSelectControl(layout.node));
gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
// If the node does not have a triangle to the left of the checkbox, make the checkbox pickable
// area stretch all the way to the frame on the left hand side, since this is otherwise dead space.
if (layout.node.isLeaf() || !this.isDrawNodeStateSymbol())
{
width = x - this.screenLocation.x + symbolSize.width;
x = this.screenLocation.x;
}
gl.glVertex2f(x + width, y + symbolSize.height);
gl.glVertex2f(x, y + symbolSize.height);
gl.glVertex2f(x, y);
gl.glVertex2f(x + width, y);
}
layout.drawPoint.x += symbolSize.width + this.getActiveAttributes().getIconSpace();
}
}
finally
{
if (dc.isPickingMode())
{
gl.glEnd(); // Quads
}
}
}
/**
* Draw squares filled with a gradient for partially selected checkboxes.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawFilledCheckboxes(DrawContext dc, Iterable nodes)
{
Dimension selectedSymbolSize = this.getSelectedSymbolSize();
TreeAttributes attributes = this.getActiveAttributes();
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
Color[] colors = attributes.getCheckBoxColor();
try
{
gl.glLineWidth(1f);
gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL);
// Fill box with a diagonal gradient
gl.glBegin(GL2.GL_QUADS);
for (NodeLayout layout : nodes)
{
int vertAdjust = layout.bounds.height - selectedSymbolSize.height
- (this.lineHeight - selectedSymbolSize.height) / 2;
int x = layout.drawPoint.x;
int y = layout.drawPoint.y + vertAdjust;
String selected = layout.node.isTreeSelected();
boolean filled = TreeNode.PARTIALLY_SELECTED.equals(selected);
if (filled)
{
OGLUtil.applyColor(gl, colors[0], 1, false);
gl.glVertex2f(x + selectedSymbolSize.width, y + selectedSymbolSize.height);
gl.glVertex2f(x, y + selectedSymbolSize.height);
gl.glVertex2f(x, y);
OGLUtil.applyColor(gl, colors[1], 1, false);
gl.glVertex2f(x + selectedSymbolSize.width, y);
}
}
}
finally
{
gl.glEnd(); // Quads
}
}
/**
* Draw checkmark symbols in the selected checkboxes.
*
* @param dc Current draw context.
* @param nodes List of visible nodes.
*/
protected void drawCheckmarks(DrawContext dc, Iterable nodes)
{
Dimension selectedSymbolSize = this.getSelectedSymbolSize();
TreeAttributes attributes = this.getActiveAttributes();
Color color = attributes.getColor();
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
// Draw checkmarks for selected nodes
OGLUtil.applyColor(gl, color, 1, false);
try
{
gl.glEnable(GL.GL_LINE_SMOOTH);
gl.glBegin(GL2.GL_LINES);
for (NodeLayout layout : nodes)
{
int vertAdjust = layout.bounds.height - selectedSymbolSize.height
- (this.lineHeight - selectedSymbolSize.height) / 2;
String selected = layout.node.isTreeSelected();
boolean checked = TreeNode.SELECTED.equals(selected);
if (checked)
{
int x = layout.drawPoint.x;
int y = layout.drawPoint.y + vertAdjust;
gl.glVertex2f(x + selectedSymbolSize.width * 0.3f - 1, y + selectedSymbolSize.height * 0.6f);
gl.glVertex2f(x + selectedSymbolSize.width * 0.3f - 1, y + selectedSymbolSize.height * 0.2f + 1);
gl.glVertex2f(x + selectedSymbolSize.width * 0.3f - 1, y + selectedSymbolSize.height * 0.2f + 1);
gl.glVertex2f(x + selectedSymbolSize.width * 0.8f - 1, y + selectedSymbolSize.height * 0.8f);
}
}
}
finally
{
gl.glEnd(); // Lines
gl.glDisable(GL.GL_LINE_SMOOTH);
}
}
/**
* Draw triangles to indicate that the nodes are expanded or collapsed.
*
* @param dc Current draw context.
* @param nodes Visible nodes.
*/
protected void drawTriangles(DrawContext dc, Iterable nodes)
{
GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
Dimension symbolSize = this.getNodeStateSymbolSize();
int halfHeight = symbolSize.height / 2;
int halfWidth = symbolSize.width / 2;
int iconSpace = this.getActiveAttributes().getIconSpace();
int pickWidth = symbolSize.width + iconSpace;
if (!dc.isPickingMode())
{
TreeAttributes attributes = this.getActiveAttributes();
Color color = attributes.getColor();
gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL);
gl.glLineWidth(1f);
OGLUtil.applyColor(gl, color, 1, false);
gl.glBegin(GL2.GL_TRIANGLES);
}
else
{
gl.glBegin(GL2.GL_QUADS); // Draw pick areas as rectangles, not triangles
}
try
{
for (NodeLayout layout : nodes)
{
// If the node is not a leaf, draw a symbol to indicate if it is expanded or collapsed
if (!layout.node.isLeaf())
{
int x = layout.drawPoint.x;
int y = layout.drawPoint.y;
if (!dc.isPickingMode())
{
x += halfWidth;
y += halfHeight;
if (this.tree.isNodeExpanded(layout.node))
{
int vertAdjust = layout.bounds.height - halfWidth - (this.lineHeight - halfWidth) / 2;
y += vertAdjust;
// Draw triangle pointing down
gl.glVertex2i(x - halfHeight, y);
gl.glVertex2i(x, -halfWidth + y);
gl.glVertex2i(x + halfHeight, y);
}
else
{
int vertAdjust = layout.bounds.height - symbolSize.height
- (this.lineHeight - symbolSize.height) / 2;
y += vertAdjust;
// Draw triangle pointing right
gl.glVertex2f(x, -halfHeight + y - 0.5f);
gl.glVertex2f(x + halfWidth, y);
gl.glVertex2f(x, halfHeight + y - 0.5f);
}
}
else
{
Color color = dc.getUniquePickColor();
int colorCode = color.getRGB();
this.pickSupport.addPickableObject(colorCode,
this.createTogglePathControl(this.tree, layout.node));
gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
x = this.screenLocation.x;
int width = (layout.drawPoint.x + pickWidth) - x;
y = (int) layout.screenBounds.getMaxY() - this.lineHeight;
gl.glVertex2f(x, y);
gl.glVertex2f(x, y + this.lineHeight);
gl.glVertex2f(x + width, y + this.lineHeight);
gl.glVertex2f(x + width, y);
}
}
if (this.isDrawNodeStateSymbol())
layout.drawPoint.x += this.getNodeStateSymbolSize().width
+ this.getActiveAttributes().getIconSpace();
}
}
finally
{
gl.glEnd(); // Triangles if drawing, quads if picking
}
}
/**
* Determine the tree layout. This method determines which nodes are visible, and where they will be drawn.
*
* @param root Root node of the subtree to render.
* @param dc Draw context.
* @param frameSize Size of the frame into which the tree will render.
* @param location Location at which to draw the node. The location specifies the upper left corner of the
* subtree.
* @param level The level of this node in the tree. The root node is at level 1, its child nodes are at level 2,
* etc.
* @param nodes List to collect nodes that are currently visible. This method adds nodes to this list.
*/
protected void computeTreeLayout(TreeNode root, DrawContext dc, Dimension frameSize, Point location, int level,
java.util.List nodes)
{
TreeAttributes attributes = this.getActiveAttributes();
int oldX = location.x;
if (this.mustDisplayNode(root, level))
{
Dimension size = this.getNodeSize(dc, frameSize, location.x, root, attributes);
// Adjust y to the bottom of the node area
int y = location.y - (size.height + this.getActiveAttributes().getRowSpacing());
Rectangle nodeBounds = new Rectangle(location.x, y, size.width, size.height);
NodeLayout layout = this.layoutCache.get(root);
if (layout == null)
layout = new NodeLayout(root);
layout.bounds = nodeBounds;
// Compute pick bounds for the node that include the row spacing above and below the node, and the full
// width of the frame.
int rowSpacing = attributes.getRowSpacing();
layout.pickBounds = new Rectangle(0, nodeBounds.y - rowSpacing, frameSize.width,
nodeBounds.height + rowSpacing * 2);
nodes.add(layout);
location.x += this.indent;
location.y -= (size.height + this.getActiveAttributes().getRowSpacing());
}
// Draw child nodes if the root node is expanded.
if (this.tree.isNodeExpanded(root))
{
for (TreeNode child : root.getChildren())
{
this.computeTreeLayout(child, dc, frameSize, location, level + 1, nodes);
}
}
location.x = oldX; // Restore previous indent level
}
/**
* Find the bounds of a node in the tree.
*
* @param needle The node to find.
* @param haystack Root node of the subtree to search.
* @param dc Draw context.
* @param frameSize Size of the frame into which the tree is rendered.
* @param location Point in OpenGL screen coordinates (origin lower left corner) that defines the upper left corner
* of the subtree.
* @param level Level of this subtree in the tree. The root node is level 1, its children are level 2, etc.
*
* @return Bounds of the node {@code needle}.
*/
protected Rectangle findNodeBounds(TreeNode needle, TreeNode haystack, DrawContext dc, Dimension frameSize,
Point location, int level)
{
TreeAttributes attributes = this.getActiveAttributes();
int oldX = location.x;
if (level > 1 || attributes.isRootVisible())
{
Dimension size = this.getNodeSize(dc, frameSize, location.x, haystack, attributes);
// Adjust y to the bottom of the node area
location.y -= (size.height + this.getActiveAttributes().getRowSpacing());
Rectangle nodeBounds = new Rectangle(location.x, location.y, size.width, size.height);
if (haystack.getPath().equals(needle.getPath()))
return nodeBounds;
location.x += level * this.indent;
}
// Draw child nodes if the root node is expanded
if (this.tree.isNodeExpanded(haystack))
{
for (TreeNode child : haystack.getChildren())
{
Rectangle bounds = this.findNodeBounds(needle, child, dc, frameSize, location, level + 1);
if (bounds != null)
return bounds;
}
}
location.x = oldX; // Restore previous indent level
return null;
}
/** {@inheritDoc} */
public synchronized void makeVisible(TreePath path)
{
TreeNode node = this.tree.getNode(path);
if (node == null)
return;
TreeNode parent = node.getParent();
while (parent != null)
{
this.tree.expandPath(parent.getPath());
parent = parent.getParent();
}
// Set the scrollToNode field. This field will be read during rendering, and the frame will be
// scrolled appropriately.
this.scrollToNode = node;
}
/**
* Get the location of the upper left corner of the tree, measured in screen coordinates with the origin at the
* upper left corner of the screen.
*
* @return Screen location, measured in pixels from the upper left corner of the screen.
*/
public Offset getScreenLocation()
{
return this.frame.getScreenLocation();
}
/**
* Set the location of the upper left corner of the tree, measured in screen coordinates with the origin at the
* upper left corner of the screen.
*
* @param screenLocation New screen location.
*/
public void setScreenLocation(Offset screenLocation)
{
frame.setScreenLocation(screenLocation);
}
/** {@inheritDoc} */
public TreeAttributes getAttributes()
{
return this.normalAttributes;
}
/** {@inheritDoc} */
public void setAttributes(TreeAttributes attributes)
{
if (attributes == null)
{
String msg = Logging.getMessage("nullValue.AttributesIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.normalAttributes = attributes;
}
/**
* Get the attributes to apply when the tree is highlighted.
*
* @return Attributes to use when tree is highlighted.
*/
public TreeAttributes getHighlightAttributes()
{
return this.highlightAttributes;
}
/**
* Set the attributes to use when the tree is highlighted.
*
* @param attributes New highlight attributes.
*/
public void setHighlightAttributes(TreeAttributes attributes)
{
if (attributes == null)
{
String msg = Logging.getMessage("nullValue.AttributesIsNull");
Logging.logger().severe(msg);
throw new IllegalArgumentException(msg);
}
this.highlightAttributes = attributes;
}
/**
* Get the active attributes, based on the highlight state.
*
* @return Highlight attributes if the tree is highlighted. Otherwise, the normal attributes.
*/
protected TreeAttributes getActiveAttributes()
{
return this.activeAttributes;
}
/** Determines which attributes -- normal, highlight or default -- to use each frame. */
protected void determineActiveAttributes()
{
TreeAttributes newAttributes = defaultAttributes;
if (this.isHighlighted())
{
if (this.getHighlightAttributes() != null)
newAttributes = this.getHighlightAttributes();
else
{
// If no highlight attributes have been specified we will use the normal attributes.
if (this.getAttributes() != null)
newAttributes = this.getAttributes();
else
newAttributes = defaultAttributes;
}
}
else if (this.getAttributes() != null)
{
newAttributes = this.getAttributes();
}
// If the attributes have changed since the last frame, change the update time since the tree needs to repaint
if (!newAttributes.equals(this.activeAttributes))
{
this.markUpdated();
}
this.activeAttributes.copy(newAttributes);
}
/**
* Is the tree highlighted? The tree is highlighted when the mouse is within the bounds of the containing frame.
*
* @return True if the tree is highlighted.
*/
public boolean isHighlighted()
{
return this.highlighted;
}
/**
* Set the tree layout to highlighted or not highlighted.
*
* @param highlighted True if the tree should be highlighted.
*/
public void setHighlighted(boolean highlighted)
{
this.highlighted = highlighted;
}
/**
* Get the frame that surrounds the tree.
*
* @return The frame that the tree is drawn on.
*/
public ScrollFrame getFrame()
{
return this.frame;
}
/**
* Compute the size of a node.
*
* @param dc Current draw context.
* @param frameSize Size of the frame into which the tree is rendered.
* @param x Offset in pixels from the left side of the screen to the left most part of the node.
* @param node Node for which to compute bounds.
* @param attributes Attributes to use for bounds calculation.
*
* @return The dimensions of the node.
*/
public Dimension getNodeSize(DrawContext dc, Dimension frameSize, int x, TreeNode node, TreeAttributes attributes)
{
Dimension size = new Dimension();
// Find bounds of the node icon.
if (node.hasImage())
{
Dimension iconSize = attributes.getIconSize();
if (iconSize.height > size.height)
size.height = iconSize.height;
size.width += (iconSize.width + attributes.getIconSpace());
}
// Add width of the check box and toggle control, if present
if (this.isDrawSelectedSymbol())
size.width += (this.getSelectedSymbolSize().width + attributes.getIconSpace());
if (this.isDrawNodeStateSymbol())
size.width += (this.getNodeStateSymbolSize().width + attributes.getIconSpace());
int textIndent = size.width;
int textWidth;
// Find the bounds of the main line of text.
Rectangle2D textBounds = this.getTextBounds(dc, this.getText(node), attributes.getFont());
textWidth = (int) textBounds.getWidth();
size.height = (int) Math.max(size.height, textBounds.getHeight());
// Find the bounds of the description string, which may be wrapped to multiple lines.
String description = this.getDescriptionText(node);
if (description != null)
{
Rectangle2D descriptionBounds;
// Compute bounds based on wrapped text, if text is set to wrap
if (this.isWrapText() && frameSize != null) // Can't wrap text without frame bounds
{
// Estimate the bounds of the wrapped text. Wrapping text is expensive, so we wait until the node
// is rendered to actually wrap the text. All we need to know here is how many lines we will have.
int textAreaWidth = frameSize.width - x - textIndent;
int numLines = this.estimateWrappedTextLines(dc, description, attributes.getDescriptionFont(),
textAreaWidth);
// If the text needs to wrap, then use the text area width as the width of the text since this is the
// edge that the text wraps to. Otherwise, the text will display on one line, so compute the bounds of the
// unwrapped text.
int width;
if (numLines == 1)
{
descriptionBounds = this.getMultilineTextBounds(dc, description, attributes.getDescriptionFont());
width = (int) Math.min(textAreaWidth, descriptionBounds.getWidth());
}
else
{
width = textAreaWidth;
}
descriptionBounds = new Rectangle(width, numLines * this.lineHeight);
NodeLayout layout = this.layoutCache.get(node);
if (layout == null)
{
layout = new NodeLayout(node);
this.layoutCache.put(node, layout);
}
layout.numLines = numLines;
}
else
{
descriptionBounds = this.getMultilineTextBounds(dc, description, attributes.getDescriptionFont());
}
size.height += (int) Math.abs(descriptionBounds.getHeight());
size.width += Math.max(textWidth, descriptionBounds.getWidth());
}
else
{
size.width += textWidth;
}
return size;
}
protected int estimateWrappedTextLines(DrawContext dc, String text, Font font, int frameWidth)
{
boolean containsWhitespace = (text.contains(" ") || text.contains("\t"));
// If there's no whitespace in the string, then the text can't wrap, it must be one line
if (!containsWhitespace)
{
return 1;
}
else
{
// Compute the bounds of the first 50 characters of the string, and use this to estimate the length of the
// full string. Computing the length of a very long description string can be very expensive, and all we're
// really trying to figure out is whether the line will need to wrap or not.
int numChars = Math.min(text.length(), 50);
Rectangle2D estTextBounds = this.getTextBounds(dc, text.substring(0, numChars), font);
double avgCharWidth = estTextBounds.getWidth() / numChars;
double textWidth = (int) avgCharWidth * text.length();
return (int) Math.min(Math.ceil(textWidth / frameWidth), this.getMaxWrappedLines());
}
}
/**
* Get the wrapped description text for a node. The wrapped text will be cached in the {@link #layoutCache}.
*
* @param dc Current draw context.
* @param node Node for which to get wrapped text.
* @param font Font to use for the description.
* @param width Width to which to wrap text.
*
* @return The wrapped text as a String. The string will contain newline characters to delimit the lines of wrapped
* text.
*/
protected String computeWrappedText(DrawContext dc, TreeNode node, Font font, int width)
{
NodeLayout layout = this.layoutCache.get(node);
if (layout == null)
{
layout = new NodeLayout(node);
this.layoutCache.put(node, layout);
}
String description = node.getDescription();
if ((layout.wrappedText == null || layout.textWrapWidth != width) && description != null)
{
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font);
MultiLineTextRenderer mltr = new MultiLineTextRenderer(textRenderer);
// Compute the maximum text height from the number of lines. Multiply by 1.5 to ensure that the height is
// enough to render maxLines, but not enough to render maxLines + 1.
int maxHeight = (int) (this.lineHeight * layout.numLines + this.lineHeight * 0.5);
layout.wrappedText = mltr.wrap(description, width, maxHeight);
layout.textWrapWidth = width;
}
return layout.wrappedText;
}
/** Invalidate the computed wrapped text, forcing the text wrap to be recomputed. */
protected void invalidateWrappedText()
{
for (Map.Entry entry : this.layoutCache.entrySet())
{
entry.getValue().wrappedText = null;
}
}
/**
* Create a pickable object to represent a toggle control in the tree. The toggle control will expand or collapse a
* node in response to user input.
*
* @param tree Tree that contains the node.
* @param node The node to expand or collapse.
*
* @return A {@link TreeHotSpot} that will be added as a pickable object to the screen area occupied by the toggle
* control.
*/
protected HotSpot createTogglePathControl(final Tree tree, final TreeNode node)
{
return new TreeHotSpot(this.getFrame())
{
@Override
public void selected(SelectEvent event)
{
if (event == null || this.isConsumed(event))
return;
if (event.isLeftClick() || event.isLeftDoubleClick())
{
tree.togglePath(node.getPath());
event.consume();
}
else
{
super.selected(event);
}
}
};
}
/**
* Create a pickable object to represent selection control in the tree. The selection control will select or
* deselect a node in response to user input. The returned HotSpot
calls {@link
* #toggleNodeSelection(TreeNode)}
upon a left-click select event.
*
* @param node The node to expand or collapse.
*
* @return A {@link TreeHotSpot} that will be added as a pickable object to the screen area occupied by the toggle
* control.
*/
protected HotSpot createSelectControl(final TreeNode node)
{
return new TreeHotSpot(this.getFrame())
{
@Override
public void selected(SelectEvent event)
{
if (event == null || this.isConsumed(event))
return;
if (event.isLeftClick() || event.isLeftDoubleClick())
{
toggleNodeSelection(node);
event.consume();
}
else
{
super.selected(event);
}
}
};
}
/**
* Get the bounds of a text string. This method consults the text bound cache. If the bounds of the input string are
* not already cached, they will be computed and added to the cache.
*
* @param dc Draw context.
* @param text Text to get bounds of.
* @param font Font applied to the text.
*
* @return A rectangle that describes the node bounds. See com.jogamp.opengl.util.awt.TextRenderer.getBounds for
* information on how this rectangle should be interpreted.
*/
protected Rectangle2D getTextBounds(DrawContext dc, String text, Font font)
{
TextCacheKey cacheKey = new TextCacheKey(text, font);
Rectangle2D bounds = this.textCache.get(cacheKey);
if (bounds == null)
{
TextRenderer textRenderer = OGLTextRenderer.getOrCreateTextRenderer(dc.getTextRendererCache(), font);
bounds = textRenderer.getBounds(text);
this.textCache.put(cacheKey, bounds);
}
return bounds;
}
/**
* Get the bounds of a multi-line text string. Each newline character in the input string (\n) indicates the start
* of a new line.
*
* @param dc Current draw context.
* @param text Text to find bounds of.
* @param font Font applied to the text.
*
* @return A rectangle that describes the node bounds. See com.jogamp.opengl.util.awt.TextRenderer.getBounds for
* information on how this rectangle should be interpreted.
*/
protected Rectangle2D getMultilineTextBounds(DrawContext dc, String text, Font font)
{
int width = 0;
int maxLineHeight = 0;
String[] lines = text.split("\n");
for (String line : lines)
{
Rectangle2D lineBounds = this.getTextBounds(dc, line, font);
width = (int) Math.max(lineBounds.getWidth(), width);
maxLineHeight = (int) Math.max(lineBounds.getMaxY(), lineHeight);
}
// Compute final height using maxLineHeight and number of lines
return new Rectangle(lines.length, lineHeight, width, lines.length * maxLineHeight);
}
/**
* Toggles the selection state of the specified node
. In order to provide an intuitive tree selection
* model to the application, this changes the selection state of the node
's ancestors and descendants
* as follows:
*
* - The branch beneath the node it also set to the node's new selection state. Toggling an interior node's
* selection state causes that entire branch to toggle.
- The node's ancestors are set to match the node's
* new selection state. If the new state is
false
, this stops at the first ancestor with another branch
* that has a selected node. When an interior or leaf node is toggled, the path to that node is also toggled, except
* when doing so would clear a selected path to another interior or leaf node.
*
*
* @param node the TreeNode
who's selection state should be toggled.
*/
protected void toggleNodeSelection(TreeNode node)
{
boolean selected = !node.isSelected();
node.setSelected(selected);
// Change the selection state of the node's descendants to match. Toggling an interior node's selection state
// causes that entire branch to toggle.
if (!node.isLeaf())
this.setDescendantsSelected(node, selected);
// Change the selection state of the node's ancestors to match. If the node's new selection state is true, then
// mark its ancestors as selected. When an interior or leaf node is selected, the path to that node is also
// selected. If the node's new selection state is false, then mark its ancestors as not selected, stopping at
// the first ancestor with a selected child. This avoids clearing a selected path to another interior or leaf
// node.
TreeNode parent = node.getParent();
while (parent != null)
{
boolean prevSelected = parent.isSelected();
parent.setSelected(selected);
if (!selected && !TreeNode.NOT_SELECTED.equals(parent.isTreeSelected()))
{
parent.setSelected(prevSelected);
break;
}
parent = parent.getParent();
}
}
/**
* Sets the selection state of the branch beneath the specified node
.
*
* @param node the TreeNode
who descendants selection should be set.
* @param selected true
to mark the descendants and selected, otherwise false
.
*/
protected void setDescendantsSelected(TreeNode node, boolean selected)
{
for (TreeNode child : node.getChildren())
{
child.setSelected(selected);
if (!child.isLeaf())
this.setDescendantsSelected(child, selected);
}
}
/**
* Get the text for a node.
*
* @param node Node to get text for.
*
* @return Text for node.
*/
protected String getText(TreeNode node)
{
return node.getText();
}
/**
* Get the description text for a node.
*
* @param node Node to get text for.
*
* @return Description text for {@code node}. May return null if there is no description.
*/
protected String getDescriptionText(TreeNode node)
{
return node.getDescription();
}
/** Cache key for cache text bound cache. */
protected static class TextCacheKey
{
/** Text string. */
protected String text;
/** Font used to compute bounds. */
protected Font font;
/** Hash code. */
protected int hash = 0;
/**
* Create a cache key for a string rendered in a font.
*
* @param text String for which to cache bounds.
* @param font Font of the rendered string.
*/
public TextCacheKey(String text, Font font)
{
if (text == null)
{
String message = Logging.getMessage("nullValue.StringIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (font == null)
{
String message = Logging.getMessage("nullValue.FontIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.text = text;
this.font = font;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || this.getClass() != o.getClass())
return false;
TextCacheKey cacheKey = (TextCacheKey) o;
return this.text.equals(cacheKey.text) && this.font.equals(cacheKey.font);
}
@Override
public int hashCode()
{
if (this.hash == 0)
{
int result;
result = this.text.hashCode();
result = 31 * result + this.font.hashCode();
this.hash = result;
}
return this.hash;
}
}
/** Class to hold information about how a tree node is laid out. */
protected static class NodeLayout
{
/** Node that this layout applies to. */
protected TreeNode node;
/** Node bounds, relative to the bottom left corner of the tree. */
protected Rectangle bounds;
protected Rectangle pickBounds;
/**
* Node bounds relative to the bottom left corner of the viewport. This field is set by {@link
* #reset(java.awt.Point)}.
*/
protected Rectangle screenBounds;
protected Rectangle pickScreenBounds;
/** Wrapped version of the node description text. Computed once and then cached here. */
protected String wrappedText;
/** The width used to wrap the description text. */
protected int textWrapWidth;
/** Number of lines of wrapped description text in this layout. */
protected int numLines;
/**
* Point at which the next component should be drawn. Nodes are drawn left to right, and the draw point is
* updated as parts of the node are rendered. For example, the toggle triangle is drawn first at the draw point,
* and then the draw point is moved to the right by the width of the triangle, so the next component will draw
* at the correct point. The draw point is reset to the lower left corner of the node bounds before each render
* cycle.
*/
protected Point drawPoint;
/**
* Create a new node layout.
*
* @param node Node that is being laid out.
*/
protected NodeLayout(TreeNode node)
{
if (node == null)
{
String message = Logging.getMessage("nullValue.TreeNodeIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.node = node;
this.drawPoint = new Point();
}
/**
* Reset the draw point to the lower left corner of the node bounds.
*
* @param treePoint location of the lower left corner of the tree, measured in GL coordinates (origin lower left
* corner of the screen).
*/
protected void reset(Point treePoint)
{
this.drawPoint.x = this.bounds.x + treePoint.x;
this.drawPoint.y = this.bounds.y + treePoint.y;
this.screenBounds = new Rectangle(this.drawPoint.x, this.drawPoint.y, this.bounds.width,
this.bounds.height);
int pickX = this.pickBounds.x + treePoint.x;
int pickY = this.pickBounds.y + treePoint.y;
this.pickScreenBounds = new Rectangle(pickX, pickY, this.pickBounds.width, this.pickBounds.height);
}
}
}