/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.hierarchyviewerlib.ui;
import com.android.ddmuilib.ImageLoader;
import com.android.hierarchyviewerlib.HierarchyViewerDirector;
import com.android.hierarchyviewerlib.device.ViewNode.ProfileRating;
import com.android.hierarchyviewerlib.models.TreeViewModel;
import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Path;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import java.text.DecimalFormat;
public class TreeView extends Canvas implements ITreeChangeListener {
private TreeViewModel mModel;
private DrawableViewNode mTree;
private DrawableViewNode mSelectedNode;
private Rectangle mViewport;
private Transform mTransform;
private Transform mInverse;
private double mZoom;
private Point mLastPoint;
private boolean mAlreadySelectedOnMouseDown;
private boolean mDoubleClicked;
private boolean mNodeMoved;
private DrawableViewNode mDraggedNode;
public static final int LINE_PADDING = 10;
public static final float BEZIER_FRACTION = 0.35f;
private static Image sRedImage;
private static Image sYellowImage;
private static Image sGreenImage;
private static Image sNotSelectedImage;
private static Image sSelectedImage;
private static Image sFilteredImage;
private static Image sFilteredSelectedImage;
private static Font sSystemFont;
private Color mBoxColor;
private Color mTextBackgroundColor;
private Rectangle mSelectedRectangleLocation;
private Point mButtonCenter;
private static final int BUTTON_SIZE = 13;
private Image mScaledSelectedImage;
private boolean mButtonClicked;
private DrawableViewNode mLastDrawnSelectedViewNode;
// The profile-image box needs to be moved to,
// so add some dragging leeway.
private static final int DRAG_LEEWAY = 220;
// Profile-image box constants
private static final int RECT_WIDTH = 190;
private static final int RECT_HEIGHT = 224;
private static final int BUTTON_RIGHT_OFFSET = 5;
private static final int BUTTON_TOP_OFFSET = 5;
private static final int IMAGE_WIDTH = 125;
private static final int IMAGE_HEIGHT = 120;
private static final int IMAGE_OFFSET = 6;
private static final int IMAGE_ROUNDING = 8;
private static final int RECTANGLE_SIZE = 5;
private static final int TEXT_SIDE_OFFSET = 8;
private static final int TEXT_TOP_OFFSET = 4;
private static final int TEXT_SPACING = 2;
private static final int TEXT_ROUNDING = 20;
public TreeView(Composite parent) {
super(parent, SWT.NONE);
mModel = TreeViewModel.getModel();
mModel.addTreeChangeListener(this);
addPaintListener(mPaintListener);
addMouseListener(mMouseListener);
addMouseMoveListener(mMouseMoveListener);
addMouseWheelListener(mMouseWheelListener);
addListener(SWT.Resize, mResizeListener);
addDisposeListener(mDisposeListener);
addKeyListener(mKeyListener);
loadResources();
mTransform = new Transform(Display.getDefault());
mInverse = new Transform(Display.getDefault());
loadAllData();
}
private void loadResources() {
ImageLoader loader = ImageLoader.getLoader(this.getClass());
sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$
sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$
sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$
sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$
sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$
sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$
sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$
mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225));
mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82));
if (mScaledSelectedImage != null) {
mScaledSelectedImage.dispose();
}
sSystemFont = Display.getDefault().getSystemFont();
}
private DisposeListener mDisposeListener = new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
mModel.removeTreeChangeListener(TreeView.this);
mTransform.dispose();
mInverse.dispose();
mBoxColor.dispose();
mTextBackgroundColor.dispose();
if (mTree != null) {
mModel.setViewport(null);
}
}
};
private Listener mResizeListener = new Listener() {
public void handleEvent(Event e) {
synchronized (TreeView.this) {
if (mTree != null && mViewport != null) {
// Keep the center in the same place.
Point viewCenter =
new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height
/ 2);
mViewport.width = getBounds().width / mZoom;
mViewport.height = getBounds().height / mZoom;
mViewport.x = viewCenter.x - mViewport.width / 2;
mViewport.y = viewCenter.y - mViewport.height / 2;
}
}
if (mViewport != null) {
mModel.setViewport(mViewport);
}
}
};
private KeyListener mKeyListener = new KeyListener() {
public void keyPressed(KeyEvent e) {
boolean selectionChanged = false;
DrawableViewNode clickedNode = null;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null && mSelectedNode != null) {
switch (e.keyCode) {
case SWT.ARROW_LEFT:
if (mSelectedNode.parent != null) {
mSelectedNode = mSelectedNode.parent;
selectionChanged = true;
}
break;
case SWT.ARROW_UP:
// On up and down, it is cool to go up and down only
// the leaf nodes.
// It goes well with the layout viewer
DrawableViewNode currentNode = mSelectedNode;
while (currentNode.parent != null && currentNode.viewNode.index == 0) {
currentNode = currentNode.parent;
}
if (currentNode.parent != null) {
selectionChanged = true;
currentNode =
currentNode.parent.children
.get(currentNode.viewNode.index - 1);
while (currentNode.children.size() != 0) {
currentNode =
currentNode.children
.get(currentNode.children.size() - 1);
}
}
if (selectionChanged) {
mSelectedNode = currentNode;
}
break;
case SWT.ARROW_DOWN:
currentNode = mSelectedNode;
while (currentNode.parent != null
&& currentNode.viewNode.index + 1 == currentNode.parent.children
.size()) {
currentNode = currentNode.parent;
}
if (currentNode.parent != null) {
selectionChanged = true;
currentNode =
currentNode.parent.children
.get(currentNode.viewNode.index + 1);
while (currentNode.children.size() != 0) {
currentNode = currentNode.children.get(0);
}
}
if (selectionChanged) {
mSelectedNode = currentNode;
}
break;
case SWT.ARROW_RIGHT:
DrawableViewNode rightNode = null;
double mostOverlap = 0;
final int N = mSelectedNode.children.size();
// We consider all the children and pick the one
// who's tree overlaps the most.
for (int i = 0; i < N; i++) {
DrawableViewNode child = mSelectedNode.children.get(i);
DrawableViewNode topMostChild = child;
while (topMostChild.children.size() != 0) {
topMostChild = topMostChild.children.get(0);
}
double overlap =
Math.min(DrawableViewNode.NODE_HEIGHT, Math.min(
mSelectedNode.top + DrawableViewNode.NODE_HEIGHT
- topMostChild.top, topMostChild.top
+ child.treeHeight - mSelectedNode.top));
if (overlap > mostOverlap) {
mostOverlap = overlap;
rightNode = child;
}
}
if (rightNode != null) {
mSelectedNode = rightNode;
selectionChanged = true;
}
break;
case SWT.CR:
clickedNode = mSelectedNode;
break;
}
}
}
if (selectionChanged) {
mModel.setSelection(mSelectedNode);
}
if (clickedNode != null) {
HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
}
}
public void keyReleased(KeyEvent e) {
}
};
private MouseListener mMouseListener = new MouseListener() {
public void mouseDoubleClick(MouseEvent e) {
DrawableViewNode clickedNode = null;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null) {
Point pt = transformPoint(e.x, e.y);
clickedNode = mTree.getSelected(pt.x, pt.y);
}
}
if (clickedNode != null) {
HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
mDoubleClicked = true;
}
}
public void mouseDown(MouseEvent e) {
boolean selectionChanged = false;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null) {
Point pt = transformPoint(e.x, e.y);
// Ignore profiling rectangle, except for...
if (mSelectedRectangleLocation != null
&& pt.x >= mSelectedRectangleLocation.x
&& pt.x < mSelectedRectangleLocation.x
+ mSelectedRectangleLocation.width
&& pt.y >= mSelectedRectangleLocation.y
&& pt.y < mSelectedRectangleLocation.y
+ mSelectedRectangleLocation.height) {
// the small button!
if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x)
+ (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) {
mButtonClicked = true;
doRedraw();
}
return;
}
mDraggedNode = mTree.getSelected(pt.x, pt.y);
// Update the selection.
if (mDraggedNode != null && mDraggedNode != mSelectedNode) {
mSelectedNode = mDraggedNode;
selectionChanged = true;
mAlreadySelectedOnMouseDown = false;
} else if (mDraggedNode != null) {
mAlreadySelectedOnMouseDown = true;
}
// Can't drag the root.
if (mDraggedNode == mTree) {
mDraggedNode = null;
}
if (mDraggedNode != null) {
mLastPoint = pt;
} else {
mLastPoint = new Point(e.x, e.y);
}
mNodeMoved = false;
mDoubleClicked = false;
}
}
if (selectionChanged) {
mModel.setSelection(mSelectedNode);
}
}
public void mouseUp(MouseEvent e) {
boolean redraw = false;
boolean redrawButton = false;
boolean viewportChanged = false;
boolean selectionChanged = false;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null && mLastPoint != null) {
if (mDraggedNode == null) {
// The viewport moves.
handleMouseDrag(new Point(e.x, e.y));
viewportChanged = true;
} else {
// The nodes move.
handleMouseDrag(transformPoint(e.x, e.y));
}
// Deselect on the second click...
// This is in the mouse up, because mouse up happens after a
// double click event.
// During a double click, we don't want to deselect.
Point pt = transformPoint(e.x, e.y);
DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y);
if (mouseUpOn != null && mouseUpOn == mSelectedNode
&& mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) {
mSelectedNode = null;
selectionChanged = true;
}
mLastPoint = null;
mDraggedNode = null;
redraw = true;
}
// Just clicked the button here.
if (mButtonClicked) {
HierarchyViewerDirector.getDirector().showCapture(getShell(),
mSelectedNode.viewNode);
mButtonClicked = false;
redrawButton = true;
}
}
// Complicated.
if (viewportChanged) {
mModel.setViewport(mViewport);
} else if (redraw) {
mModel.removeTreeChangeListener(TreeView.this);
mModel.notifyViewportChanged();
if (selectionChanged) {
mModel.setSelection(mSelectedNode);
}
mModel.addTreeChangeListener(TreeView.this);
doRedraw();
} else if (redrawButton) {
doRedraw();
}
}
};
private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
public void mouseMove(MouseEvent e) {
boolean redraw = false;
boolean viewportChanged = false;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null && mLastPoint != null) {
if (mDraggedNode == null) {
handleMouseDrag(new Point(e.x, e.y));
viewportChanged = true;
} else {
handleMouseDrag(transformPoint(e.x, e.y));
}
redraw = true;
}
}
if (viewportChanged) {
mModel.setViewport(mViewport);
} else if (redraw) {
mModel.removeTreeChangeListener(TreeView.this);
mModel.notifyViewportChanged();
mModel.addTreeChangeListener(TreeView.this);
doRedraw();
}
}
};
private void handleMouseDrag(Point pt) {
// Case 1: a node is dragged. DrawableViewNode knows how to handle this.
if (mDraggedNode != null) {
if (mLastPoint.y - pt.y != 0) {
mNodeMoved = true;
}
mDraggedNode.move(mLastPoint.y - pt.y);
mLastPoint = pt;
return;
}
// Case 2: the viewport is dragged. We have to make sure we respect the
// bounds - don't let the user drag way out... + some leeway for the
// profiling box.
double xDif = (mLastPoint.x - pt.x) / mZoom;
double yDif = (mLastPoint.y - pt.y) / mZoom;
double treeX = mTree.bounds.x - DRAG_LEEWAY;
double treeY = mTree.bounds.y - DRAG_LEEWAY;
double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY;
double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY;
if (mViewport.width > treeWidth) {
if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) {
mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
} else if (xDif > 0 && mViewport.x < treeX) {
mViewport.x = Math.min(mViewport.x + xDif, treeX);
}
} else {
if (xDif < 0 && mViewport.x > treeX) {
mViewport.x = Math.max(mViewport.x + xDif, treeX);
} else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) {
mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
}
}
if (mViewport.height > treeHeight) {
if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) {
mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
} else if (yDif > 0 && mViewport.y < treeY) {
mViewport.y = Math.min(mViewport.y + yDif, treeY);
}
} else {
if (yDif < 0 && mViewport.y > treeY) {
mViewport.y = Math.max(mViewport.y + yDif, treeY);
} else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) {
mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
}
}
mLastPoint = pt;
}
private Point transformPoint(double x, double y) {
float[] pt = {
(float) x, (float) y
};
mInverse.transform(pt);
return new Point(pt[0], pt[1]);
}
private MouseWheelListener mMouseWheelListener = new MouseWheelListener() {
public void mouseScrolled(MouseEvent e) {
Point zoomPoint = null;
synchronized (TreeView.this) {
if (mTree != null && mViewport != null) {
mZoom += Math.ceil(e.count / 3.0) * 0.1;
zoomPoint = transformPoint(e.x, e.y);
}
}
if (zoomPoint != null) {
mModel.zoomOnPoint(mZoom, zoomPoint);
}
}
};
private PaintListener mPaintListener = new PaintListener() {
public void paintControl(PaintEvent e) {
synchronized (TreeView.this) {
e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
if (mTree != null && mViewport != null) {
// Easy stuff!
e.gc.setTransform(mTransform);
e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
Path connectionPath = new Path(Display.getDefault());
paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath);
e.gc.drawPath(connectionPath);
connectionPath.dispose();
// Draw the profiling box.
if (mSelectedNode != null) {
e.gc.setAlpha(200);
// Draw the little triangle
int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2;
int y = (int) mSelectedNode.top + 4;
e.gc.setBackground(mBoxColor);
e.gc.fillPolygon(new int[] {
x, y, x - 11, y - 11, x + 11, y - 11
});
// Draw the rectangle and update the location.
y -= 10 + RECT_HEIGHT;
e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30,
30);
mSelectedRectangleLocation =
new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT);
e.gc.setAlpha(255);
// Draw the button
mButtonCenter =
new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2,
y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2);
if (mButtonClicked) {
e.gc
.setBackground(Display.getDefault().getSystemColor(
SWT.COLOR_BLACK));
} else {
e.gc.setBackground(mTextBackgroundColor);
}
e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y
+ BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE);
e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET
+ (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y
+ BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2,
RECTANGLE_SIZE + 1, RECTANGLE_SIZE);
y += 15;
// If there is an image, draw it.
if (mSelectedNode.viewNode.image != null
&& mSelectedNode.viewNode.image.getBounds().height != 1
&& mSelectedNode.viewNode.image.getBounds().width != 1) {
// Scaling the image to the right size takes lots of
// time, so we want to do it only once.
// If the selection changed, get rid of the old
// image.
if (mLastDrawnSelectedViewNode != mSelectedNode) {
if (mScaledSelectedImage != null) {
mScaledSelectedImage.dispose();
mScaledSelectedImage = null;
}
mLastDrawnSelectedViewNode = mSelectedNode;
}
if (mScaledSelectedImage == null) {
double ratio =
1.0 * mSelectedNode.viewNode.image.getBounds().width
/ mSelectedNode.viewNode.image.getBounds().height;
int newWidth, newHeight;
if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) {
newWidth =
Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image
.getBounds().width);
newHeight = (int) (newWidth / ratio);
} else {
newHeight =
Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image
.getBounds().height);
newWidth = (int) (newHeight * ratio);
}
// Interesting note... We make the image twice
// the needed size so that there is better
// resolution under zoom.
newWidth = Math.max(newWidth * 2, 1);
newHeight = Math.max(newHeight * 2, 1);
mScaledSelectedImage =
new Image(Display.getDefault(), newWidth, newHeight);
GC gc = new GC(mScaledSelectedImage);
gc.setBackground(mTextBackgroundColor);
gc.fillRectangle(0, 0, newWidth, newHeight);
gc.drawImage(mSelectedNode.viewNode.image, 0, 0,
mSelectedNode.viewNode.image.getBounds().width,
mSelectedNode.viewNode.image.getBounds().height, 0, 0,
newWidth, newHeight);
gc.dispose();
}
// Draw the background rectangle
e.gc.setBackground(mTextBackgroundColor);
e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4
- IMAGE_OFFSET, y
+ (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
/ 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2
+ 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2
+ 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING);
// Under max zoom, we want the image to be
// untransformed. So, get back to the identity
// transform.
int imageX = x - mScaledSelectedImage.getBounds().width / 4;
int imageY =
y
+ (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
/ 2;
Transform untransformedTransform = new Transform(Display.getDefault());
e.gc.setTransform(untransformedTransform);
float[] pt = new float[] {
imageX, imageY
};
mTransform.transform(pt);
e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage
.getBounds().width, mScaledSelectedImage.getBounds().height,
(int) pt[0], (int) pt[1], (int) (mScaledSelectedImage
.getBounds().width
* mZoom / 2),
(int) (mScaledSelectedImage.getBounds().height * mZoom / 2));
untransformedTransform.dispose();
e.gc.setTransform(mTransform);
}
// Text stuff
y += IMAGE_HEIGHT;
y += 10;
Font font = getFont(8, false);
e.gc.setFont(font);
String text =
mSelectedNode.viewNode.viewCount + " view"
+ (mSelectedNode.viewNode.viewCount != 1 ? "s" : "");
DecimalFormat formatter = new DecimalFormat("0.000");
String measureText =
"Measure: "
+ (mSelectedNode.viewNode.measureTime != -1 ? formatter
.format(mSelectedNode.viewNode.measureTime)
+ " ms" : "n/a");
String layoutText =
"Layout: "
+ (mSelectedNode.viewNode.layoutTime != -1 ? formatter
.format(mSelectedNode.viewNode.layoutTime)
+ " ms" : "n/a");
String drawText =
"Draw: "
+ (mSelectedNode.viewNode.drawTime != -1 ? formatter
.format(mSelectedNode.viewNode.drawTime)
+ " ms" : "n/a");
org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text);
org.eclipse.swt.graphics.Point measureExtent =
e.gc.stringExtent(measureText);
org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText);
org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText);
int boxWidth =
Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max(
layoutExtent.x, drawExtent.x)))
+ 2 * TEXT_SIDE_OFFSET;
int boxHeight =
titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING
+ layoutExtent.y + TEXT_SPACING + drawExtent.y + 2
* TEXT_TOP_OFFSET;
e.gc.setBackground(mTextBackgroundColor);
e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight,
TEXT_ROUNDING, TEXT_ROUNDING);
e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
y += TEXT_TOP_OFFSET;
e.gc.drawText(text, x - titleExtent.x / 2, y, true);
x -= boxWidth / 2;
x += TEXT_SIDE_OFFSET;
y += titleExtent.y + TEXT_SPACING;
e.gc.drawText(measureText, x, y, true);
y += measureExtent.y + TEXT_SPACING;
e.gc.drawText(layoutText, x, y, true);
y += layoutExtent.y + TEXT_SPACING;
e.gc.drawText(drawText, x, y, true);
font.dispose();
} else {
mSelectedRectangleLocation = null;
mButtonCenter = null;
}
}
}
}
};
private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node,
DrawableViewNode selectedNode, Path connectionPath) {
if (selectedNode == node && node.viewNode.filtered) {
gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top));
} else if (selectedNode == node) {
gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top));
} else if (node.viewNode.filtered) {
gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top));
} else {
gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top));
}
int fontHeight = gc.getFontMetrics().getHeight();
// Draw the text...
int contentWidth =
DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
String name = node.viewNode.name;
int dotIndex = name.lastIndexOf('.');
if (dotIndex != -1) {
name = name.substring(dotIndex + 1);
}
double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING;
drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true);
y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight,
8, false);
y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
if (!node.viewNode.id.equals("NO_ID")) {
drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8,
false);
}
if (node.viewNode.measureRating != ProfileRating.NONE) {
y =
node.top + DrawableViewNode.NODE_HEIGHT
- DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING
- sRedImage.getBounds().height;
x +=
(contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2;
switch (node.viewNode.measureRating) {
case GREEN:
gc.drawImage(sGreenImage, (int) x, (int) y);
break;
case YELLOW:
gc.drawImage(sYellowImage, (int) x, (int) y);
break;
case RED:
gc.drawImage(sRedImage, (int) x, (int) y);
break;
}
x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
switch (node.viewNode.layoutRating) {
case GREEN:
gc.drawImage(sGreenImage, (int) x, (int) y);
break;
case YELLOW:
gc.drawImage(sYellowImage, (int) x, (int) y);
break;
case RED:
gc.drawImage(sRedImage, (int) x, (int) y);
break;
}
x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
switch (node.viewNode.drawRating) {
case GREEN:
gc.drawImage(sGreenImage, (int) x, (int) y);
break;
case YELLOW:
gc.drawImage(sYellowImage, (int) x, (int) y);
break;
case RED:
gc.drawImage(sRedImage, (int) x, (int) y);
break;
}
}
org.eclipse.swt.graphics.Point indexExtent =
gc.stringExtent(Integer.toString(node.viewNode.index));
x =
node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING
- indexExtent.x;
y =
node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING
- indexExtent.y;
gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT);
int N = node.children.size();
if (N == 0) {
return;
}
float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N;
for (int i = 0; i < N; i++) {
DrawableViewNode child = node.children.get(i);
paintRecursive(gc, transform, child, selectedNode, connectionPath);
float x1 = node.left + DrawableViewNode.NODE_WIDTH;
float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2;
float x2 = child.left;
float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
float cy1 = y1;
float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
float cy2 = y2;
connectionPath.moveTo(x1, y1);
connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
}
}
private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y,
double width, double height, int fontSize, boolean bold) {
Font oldFont = gc.getFont();
Font newFont = getFont(fontSize, bold);
gc.setFont(newFont);
org.eclipse.swt.graphics.Point extent = gc.stringExtent(text);
if (extent.x > width) {
// Oh no... we need to scale it.
double scale = width / extent.x;
float[] transformElements = new float[6];
transform.getElements(transformElements);
transform.scale((float) scale, (float) scale);
gc.setTransform(transform);
x /= scale;
y /= scale;
y += (extent.y / scale - extent.y) / 2;
gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT);
transform.setElements(transformElements[0], transformElements[1], transformElements[2],
transformElements[3], transformElements[4], transformElements[5]);
gc.setTransform(transform);
} else {
gc.drawText(text, (int) (x + (width - extent.x) / 2),
(int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT);
}
gc.setFont(oldFont);
newFont.dispose();
}
public static Image paintToImage(DrawableViewNode tree) {
Image image =
new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math
.ceil(tree.bounds.height));
Transform transform = new Transform(Display.getDefault());
transform.identity();
transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y);
Path connectionPath = new Path(Display.getDefault());
GC gc = new GC(image);
// Can't use Display.getDefault().getSystemColor in a non-UI thread.
Color white = new Color(Display.getDefault(), 255, 255, 255);
Color black = new Color(Display.getDefault(), 0, 0, 0);
gc.setForeground(white);
gc.setBackground(black);
gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height);
gc.setTransform(transform);
paintRecursive(gc, transform, tree, null, connectionPath);
gc.drawPath(connectionPath);
gc.dispose();
connectionPath.dispose();
white.dispose();
black.dispose();
return image;
}
private static Font getFont(int size, boolean bold) {
FontData[] fontData = sSystemFont.getFontData();
for (int i = 0; i < fontData.length; i++) {
fontData[i].setHeight(size);
if (bold) {
fontData[i].setStyle(SWT.BOLD);
}
}
return new Font(Display.getDefault(), fontData);
}
private void doRedraw() {
Display.getDefault().syncExec(new Runnable() {
public void run() {
redraw();
}
});
}
public void loadAllData() {
boolean newViewport = mViewport == null;
Display.getDefault().syncExec(new Runnable() {
public void run() {
synchronized (this) {
mTree = mModel.getTree();
mSelectedNode = mModel.getSelection();
mViewport = mModel.getViewport();
mZoom = mModel.getZoom();
if (mTree != null && mViewport == null) {
mViewport =
new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
- getBounds().height / 2, getBounds().width,
getBounds().height);
} else {
setTransform();
}
}
}
});
if (newViewport) {
mModel.setViewport(mViewport);
}
}
// Fickle behaviour... When a new tree is loaded, the model doesn't know
// about the viewport until it passes through here.
public void treeChanged() {
Display.getDefault().syncExec(new Runnable() {
public void run() {
synchronized (this) {
mTree = mModel.getTree();
mSelectedNode = mModel.getSelection();
if (mTree == null) {
mViewport = null;
} else {
mViewport =
new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
- getBounds().height / 2, getBounds().width,
getBounds().height);
}
}
}
});
if (mViewport != null) {
mModel.setViewport(mViewport);
} else {
doRedraw();
}
}
private void setTransform() {
if (mViewport != null && mTree != null) {
// Set the transform.
mTransform.identity();
mInverse.identity();
mTransform.scale((float) mZoom, (float) mZoom);
mInverse.scale((float) mZoom, (float) mZoom);
mTransform.translate((float) -mViewport.x, (float) -mViewport.y);
mInverse.translate((float) -mViewport.x, (float) -mViewport.y);
mInverse.invert();
}
}
// Note the syncExec and then synchronized... It avoids deadlock
public void viewportChanged() {
Display.getDefault().syncExec(new Runnable() {
public void run() {
synchronized (this) {
mViewport = mModel.getViewport();
mZoom = mModel.getZoom();
setTransform();
}
}
});
doRedraw();
}
public void zoomChanged() {
viewportChanged();
}
public void selectionChanged() {
synchronized (this) {
mSelectedNode = mModel.getSelection();
if (mSelectedNode != null && mSelectedNode.viewNode.image == null) {
HierarchyViewerDirector.getDirector()
.loadCaptureInBackground(mSelectedNode.viewNode);
}
}
doRedraw();
}
}