1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.hierarchyviewerlib.ui; 18 19 import com.android.ddmuilib.ImageLoader; 20 import com.android.hierarchyviewerlib.HierarchyViewerDirector; 21 import com.android.hierarchyviewerlib.device.ViewNode.ProfileRating; 22 import com.android.hierarchyviewerlib.models.TreeViewModel; 23 import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; 24 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; 25 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; 26 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; 27 28 import org.eclipse.swt.SWT; 29 import org.eclipse.swt.events.DisposeEvent; 30 import org.eclipse.swt.events.DisposeListener; 31 import org.eclipse.swt.events.KeyEvent; 32 import org.eclipse.swt.events.KeyListener; 33 import org.eclipse.swt.events.MouseEvent; 34 import org.eclipse.swt.events.MouseListener; 35 import org.eclipse.swt.events.MouseMoveListener; 36 import org.eclipse.swt.events.MouseWheelListener; 37 import org.eclipse.swt.events.PaintEvent; 38 import org.eclipse.swt.events.PaintListener; 39 import org.eclipse.swt.graphics.Color; 40 import org.eclipse.swt.graphics.Font; 41 import org.eclipse.swt.graphics.FontData; 42 import org.eclipse.swt.graphics.GC; 43 import org.eclipse.swt.graphics.Image; 44 import org.eclipse.swt.graphics.Path; 45 import org.eclipse.swt.graphics.RGB; 46 import org.eclipse.swt.graphics.Transform; 47 import org.eclipse.swt.widgets.Canvas; 48 import org.eclipse.swt.widgets.Composite; 49 import org.eclipse.swt.widgets.Display; 50 import org.eclipse.swt.widgets.Event; 51 import org.eclipse.swt.widgets.Listener; 52 53 import java.text.DecimalFormat; 54 55 public class TreeView extends Canvas implements ITreeChangeListener { 56 57 private TreeViewModel mModel; 58 59 private DrawableViewNode mTree; 60 61 private DrawableViewNode mSelectedNode; 62 63 private Rectangle mViewport; 64 65 private Transform mTransform; 66 67 private Transform mInverse; 68 69 private double mZoom; 70 71 private Point mLastPoint; 72 73 private boolean mAlreadySelectedOnMouseDown; 74 75 private boolean mDoubleClicked; 76 77 private boolean mNodeMoved; 78 79 private DrawableViewNode mDraggedNode; 80 81 public static final int LINE_PADDING = 10; 82 83 public static final float BEZIER_FRACTION = 0.35f; 84 85 private static Image sRedImage; 86 87 private static Image sYellowImage; 88 89 private static Image sGreenImage; 90 91 private static Image sNotSelectedImage; 92 93 private static Image sSelectedImage; 94 95 private static Image sFilteredImage; 96 97 private static Image sFilteredSelectedImage; 98 99 private static Font sSystemFont; 100 101 private Color mBoxColor; 102 103 private Color mTextBackgroundColor; 104 105 private Rectangle mSelectedRectangleLocation; 106 107 private Point mButtonCenter; 108 109 private static final int BUTTON_SIZE = 13; 110 111 private Image mScaledSelectedImage; 112 113 private boolean mButtonClicked; 114 115 private DrawableViewNode mLastDrawnSelectedViewNode; 116 117 // The profile-image box needs to be moved to, 118 // so add some dragging leeway. 119 private static final int DRAG_LEEWAY = 220; 120 121 // Profile-image box constants 122 private static final int RECT_WIDTH = 190; 123 124 private static final int RECT_HEIGHT = 224; 125 126 private static final int BUTTON_RIGHT_OFFSET = 5; 127 128 private static final int BUTTON_TOP_OFFSET = 5; 129 130 private static final int IMAGE_WIDTH = 125; 131 132 private static final int IMAGE_HEIGHT = 120; 133 134 private static final int IMAGE_OFFSET = 6; 135 136 private static final int IMAGE_ROUNDING = 8; 137 138 private static final int RECTANGLE_SIZE = 5; 139 140 private static final int TEXT_SIDE_OFFSET = 8; 141 142 private static final int TEXT_TOP_OFFSET = 4; 143 144 private static final int TEXT_SPACING = 2; 145 146 private static final int TEXT_ROUNDING = 20; 147 148 public TreeView(Composite parent) { 149 super(parent, SWT.NONE); 150 151 mModel = TreeViewModel.getModel(); 152 mModel.addTreeChangeListener(this); 153 154 addPaintListener(mPaintListener); 155 addMouseListener(mMouseListener); 156 addMouseMoveListener(mMouseMoveListener); 157 addMouseWheelListener(mMouseWheelListener); 158 addListener(SWT.Resize, mResizeListener); 159 addDisposeListener(mDisposeListener); 160 addKeyListener(mKeyListener); 161 162 loadResources(); 163 164 mTransform = new Transform(Display.getDefault()); 165 mInverse = new Transform(Display.getDefault()); 166 167 loadAllData(); 168 } 169 170 private void loadResources() { 171 ImageLoader loader = ImageLoader.getLoader(this.getClass()); 172 sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$ 173 sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$ 174 sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$ 175 sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ 176 sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$ 177 sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ 178 sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$ 179 mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225)); 180 mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82)); 181 if (mScaledSelectedImage != null) { 182 mScaledSelectedImage.dispose(); 183 } 184 sSystemFont = Display.getDefault().getSystemFont(); 185 } 186 187 private DisposeListener mDisposeListener = new DisposeListener() { 188 public void widgetDisposed(DisposeEvent e) { 189 mModel.removeTreeChangeListener(TreeView.this); 190 mTransform.dispose(); 191 mInverse.dispose(); 192 mBoxColor.dispose(); 193 mTextBackgroundColor.dispose(); 194 if (mTree != null) { 195 mModel.setViewport(null); 196 } 197 } 198 }; 199 200 private Listener mResizeListener = new Listener() { 201 public void handleEvent(Event e) { 202 synchronized (TreeView.this) { 203 if (mTree != null && mViewport != null) { 204 205 // Keep the center in the same place. 206 Point viewCenter = 207 new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height 208 / 2); 209 mViewport.width = getBounds().width / mZoom; 210 mViewport.height = getBounds().height / mZoom; 211 mViewport.x = viewCenter.x - mViewport.width / 2; 212 mViewport.y = viewCenter.y - mViewport.height / 2; 213 } 214 } 215 if (mViewport != null) { 216 mModel.setViewport(mViewport); 217 } 218 } 219 }; 220 221 private KeyListener mKeyListener = new KeyListener() { 222 223 public void keyPressed(KeyEvent e) { 224 boolean selectionChanged = false; 225 DrawableViewNode clickedNode = null; 226 synchronized (TreeView.this) { 227 if (mTree != null && mViewport != null && mSelectedNode != null) { 228 switch (e.keyCode) { 229 case SWT.ARROW_LEFT: 230 if (mSelectedNode.parent != null) { 231 mSelectedNode = mSelectedNode.parent; 232 selectionChanged = true; 233 } 234 break; 235 case SWT.ARROW_UP: 236 237 // On up and down, it is cool to go up and down only 238 // the leaf nodes. 239 // It goes well with the layout viewer 240 DrawableViewNode currentNode = mSelectedNode; 241 while (currentNode.parent != null && currentNode.viewNode.index == 0) { 242 currentNode = currentNode.parent; 243 } 244 if (currentNode.parent != null) { 245 selectionChanged = true; 246 currentNode = 247 currentNode.parent.children 248 .get(currentNode.viewNode.index - 1); 249 while (currentNode.children.size() != 0) { 250 currentNode = 251 currentNode.children 252 .get(currentNode.children.size() - 1); 253 } 254 } 255 if (selectionChanged) { 256 mSelectedNode = currentNode; 257 } 258 break; 259 case SWT.ARROW_DOWN: 260 currentNode = mSelectedNode; 261 while (currentNode.parent != null 262 && currentNode.viewNode.index + 1 == currentNode.parent.children 263 .size()) { 264 currentNode = currentNode.parent; 265 } 266 if (currentNode.parent != null) { 267 selectionChanged = true; 268 currentNode = 269 currentNode.parent.children 270 .get(currentNode.viewNode.index + 1); 271 while (currentNode.children.size() != 0) { 272 currentNode = currentNode.children.get(0); 273 } 274 } 275 if (selectionChanged) { 276 mSelectedNode = currentNode; 277 } 278 break; 279 case SWT.ARROW_RIGHT: 280 DrawableViewNode rightNode = null; 281 double mostOverlap = 0; 282 final int N = mSelectedNode.children.size(); 283 284 // We consider all the children and pick the one 285 // who's tree overlaps the most. 286 for (int i = 0; i < N; i++) { 287 DrawableViewNode child = mSelectedNode.children.get(i); 288 DrawableViewNode topMostChild = child; 289 while (topMostChild.children.size() != 0) { 290 topMostChild = topMostChild.children.get(0); 291 } 292 double overlap = 293 Math.min(DrawableViewNode.NODE_HEIGHT, Math.min( 294 mSelectedNode.top + DrawableViewNode.NODE_HEIGHT 295 - topMostChild.top, topMostChild.top 296 + child.treeHeight - mSelectedNode.top)); 297 if (overlap > mostOverlap) { 298 mostOverlap = overlap; 299 rightNode = child; 300 } 301 } 302 if (rightNode != null) { 303 mSelectedNode = rightNode; 304 selectionChanged = true; 305 } 306 break; 307 case SWT.CR: 308 clickedNode = mSelectedNode; 309 break; 310 } 311 } 312 } 313 if (selectionChanged) { 314 mModel.setSelection(mSelectedNode); 315 } 316 if (clickedNode != null) { 317 HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); 318 } 319 } 320 321 public void keyReleased(KeyEvent e) { 322 } 323 }; 324 325 private MouseListener mMouseListener = new MouseListener() { 326 327 public void mouseDoubleClick(MouseEvent e) { 328 DrawableViewNode clickedNode = null; 329 synchronized (TreeView.this) { 330 if (mTree != null && mViewport != null) { 331 Point pt = transformPoint(e.x, e.y); 332 clickedNode = mTree.getSelected(pt.x, pt.y); 333 } 334 } 335 if (clickedNode != null) { 336 HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); 337 mDoubleClicked = true; 338 } 339 } 340 341 public void mouseDown(MouseEvent e) { 342 boolean selectionChanged = false; 343 synchronized (TreeView.this) { 344 if (mTree != null && mViewport != null) { 345 Point pt = transformPoint(e.x, e.y); 346 347 // Ignore profiling rectangle, except for... 348 if (mSelectedRectangleLocation != null 349 && pt.x >= mSelectedRectangleLocation.x 350 && pt.x < mSelectedRectangleLocation.x 351 + mSelectedRectangleLocation.width 352 && pt.y >= mSelectedRectangleLocation.y 353 && pt.y < mSelectedRectangleLocation.y 354 + mSelectedRectangleLocation.height) { 355 356 // the small button! 357 if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x) 358 + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) { 359 mButtonClicked = true; 360 doRedraw(); 361 } 362 return; 363 } 364 mDraggedNode = mTree.getSelected(pt.x, pt.y); 365 366 // Update the selection. 367 if (mDraggedNode != null && mDraggedNode != mSelectedNode) { 368 mSelectedNode = mDraggedNode; 369 selectionChanged = true; 370 mAlreadySelectedOnMouseDown = false; 371 } else if (mDraggedNode != null) { 372 mAlreadySelectedOnMouseDown = true; 373 } 374 375 // Can't drag the root. 376 if (mDraggedNode == mTree) { 377 mDraggedNode = null; 378 } 379 380 if (mDraggedNode != null) { 381 mLastPoint = pt; 382 } else { 383 mLastPoint = new Point(e.x, e.y); 384 } 385 mNodeMoved = false; 386 mDoubleClicked = false; 387 } 388 } 389 if (selectionChanged) { 390 mModel.setSelection(mSelectedNode); 391 } 392 } 393 394 public void mouseUp(MouseEvent e) { 395 boolean redraw = false; 396 boolean redrawButton = false; 397 boolean viewportChanged = false; 398 boolean selectionChanged = false; 399 synchronized (TreeView.this) { 400 if (mTree != null && mViewport != null && mLastPoint != null) { 401 if (mDraggedNode == null) { 402 // The viewport moves. 403 handleMouseDrag(new Point(e.x, e.y)); 404 viewportChanged = true; 405 } else { 406 // The nodes move. 407 handleMouseDrag(transformPoint(e.x, e.y)); 408 } 409 410 // Deselect on the second click... 411 // This is in the mouse up, because mouse up happens after a 412 // double click event. 413 // During a double click, we don't want to deselect. 414 Point pt = transformPoint(e.x, e.y); 415 DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y); 416 if (mouseUpOn != null && mouseUpOn == mSelectedNode 417 && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) { 418 mSelectedNode = null; 419 selectionChanged = true; 420 } 421 mLastPoint = null; 422 mDraggedNode = null; 423 redraw = true; 424 } 425 426 // Just clicked the button here. 427 if (mButtonClicked) { 428 HierarchyViewerDirector.getDirector().showCapture(getShell(), 429 mSelectedNode.viewNode); 430 mButtonClicked = false; 431 redrawButton = true; 432 } 433 } 434 435 // Complicated. 436 if (viewportChanged) { 437 mModel.setViewport(mViewport); 438 } else if (redraw) { 439 mModel.removeTreeChangeListener(TreeView.this); 440 mModel.notifyViewportChanged(); 441 if (selectionChanged) { 442 mModel.setSelection(mSelectedNode); 443 } 444 mModel.addTreeChangeListener(TreeView.this); 445 doRedraw(); 446 } else if (redrawButton) { 447 doRedraw(); 448 } 449 } 450 451 }; 452 453 private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { 454 public void mouseMove(MouseEvent e) { 455 boolean redraw = false; 456 boolean viewportChanged = false; 457 synchronized (TreeView.this) { 458 if (mTree != null && mViewport != null && mLastPoint != null) { 459 if (mDraggedNode == null) { 460 handleMouseDrag(new Point(e.x, e.y)); 461 viewportChanged = true; 462 } else { 463 handleMouseDrag(transformPoint(e.x, e.y)); 464 } 465 redraw = true; 466 } 467 } 468 if (viewportChanged) { 469 mModel.setViewport(mViewport); 470 } else if (redraw) { 471 mModel.removeTreeChangeListener(TreeView.this); 472 mModel.notifyViewportChanged(); 473 mModel.addTreeChangeListener(TreeView.this); 474 doRedraw(); 475 } 476 } 477 }; 478 479 private void handleMouseDrag(Point pt) { 480 481 // Case 1: a node is dragged. DrawableViewNode knows how to handle this. 482 if (mDraggedNode != null) { 483 if (mLastPoint.y - pt.y != 0) { 484 mNodeMoved = true; 485 } 486 mDraggedNode.move(mLastPoint.y - pt.y); 487 mLastPoint = pt; 488 return; 489 } 490 491 // Case 2: the viewport is dragged. We have to make sure we respect the 492 // bounds - don't let the user drag way out... + some leeway for the 493 // profiling box. 494 double xDif = (mLastPoint.x - pt.x) / mZoom; 495 double yDif = (mLastPoint.y - pt.y) / mZoom; 496 497 double treeX = mTree.bounds.x - DRAG_LEEWAY; 498 double treeY = mTree.bounds.y - DRAG_LEEWAY; 499 double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY; 500 double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY; 501 502 if (mViewport.width > treeWidth) { 503 if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) { 504 mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width); 505 } else if (xDif > 0 && mViewport.x < treeX) { 506 mViewport.x = Math.min(mViewport.x + xDif, treeX); 507 } 508 } else { 509 if (xDif < 0 && mViewport.x > treeX) { 510 mViewport.x = Math.max(mViewport.x + xDif, treeX); 511 } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) { 512 mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width); 513 } 514 } 515 if (mViewport.height > treeHeight) { 516 if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) { 517 mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height); 518 } else if (yDif > 0 && mViewport.y < treeY) { 519 mViewport.y = Math.min(mViewport.y + yDif, treeY); 520 } 521 } else { 522 if (yDif < 0 && mViewport.y > treeY) { 523 mViewport.y = Math.max(mViewport.y + yDif, treeY); 524 } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) { 525 mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height); 526 } 527 } 528 mLastPoint = pt; 529 } 530 531 private Point transformPoint(double x, double y) { 532 float[] pt = { 533 (float) x, (float) y 534 }; 535 mInverse.transform(pt); 536 return new Point(pt[0], pt[1]); 537 } 538 539 private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { 540 public void mouseScrolled(MouseEvent e) { 541 Point zoomPoint = null; 542 synchronized (TreeView.this) { 543 if (mTree != null && mViewport != null) { 544 mZoom += Math.ceil(e.count / 3.0) * 0.1; 545 zoomPoint = transformPoint(e.x, e.y); 546 } 547 } 548 if (zoomPoint != null) { 549 mModel.zoomOnPoint(mZoom, zoomPoint); 550 } 551 } 552 }; 553 554 private PaintListener mPaintListener = new PaintListener() { 555 public void paintControl(PaintEvent e) { 556 synchronized (TreeView.this) { 557 e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); 558 e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); 559 if (mTree != null && mViewport != null) { 560 561 // Easy stuff! 562 e.gc.setTransform(mTransform); 563 e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); 564 Path connectionPath = new Path(Display.getDefault()); 565 paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath); 566 e.gc.drawPath(connectionPath); 567 connectionPath.dispose(); 568 569 // Draw the profiling box. 570 if (mSelectedNode != null) { 571 572 e.gc.setAlpha(200); 573 574 // Draw the little triangle 575 int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2; 576 int y = (int) mSelectedNode.top + 4; 577 e.gc.setBackground(mBoxColor); 578 e.gc.fillPolygon(new int[] { 579 x, y, x - 11, y - 11, x + 11, y - 11 580 }); 581 582 // Draw the rectangle and update the location. 583 y -= 10 + RECT_HEIGHT; 584 e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30, 585 30); 586 mSelectedRectangleLocation = 587 new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT); 588 589 e.gc.setAlpha(255); 590 591 // Draw the button 592 mButtonCenter = 593 new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2, 594 y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2); 595 596 if (mButtonClicked) { 597 e.gc 598 .setBackground(Display.getDefault().getSystemColor( 599 SWT.COLOR_BLACK)); 600 } else { 601 e.gc.setBackground(mTextBackgroundColor); 602 603 } 604 e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); 605 606 e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y 607 + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE); 608 609 e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET 610 + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y 611 + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2, 612 RECTANGLE_SIZE + 1, RECTANGLE_SIZE); 613 614 y += 15; 615 616 // If there is an image, draw it. 617 if (mSelectedNode.viewNode.image != null 618 && mSelectedNode.viewNode.image.getBounds().height != 1 619 && mSelectedNode.viewNode.image.getBounds().width != 1) { 620 621 // Scaling the image to the right size takes lots of 622 // time, so we want to do it only once. 623 624 // If the selection changed, get rid of the old 625 // image. 626 if (mLastDrawnSelectedViewNode != mSelectedNode) { 627 if (mScaledSelectedImage != null) { 628 mScaledSelectedImage.dispose(); 629 mScaledSelectedImage = null; 630 } 631 mLastDrawnSelectedViewNode = mSelectedNode; 632 } 633 634 if (mScaledSelectedImage == null) { 635 double ratio = 636 1.0 * mSelectedNode.viewNode.image.getBounds().width 637 / mSelectedNode.viewNode.image.getBounds().height; 638 int newWidth, newHeight; 639 if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) { 640 newWidth = 641 Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image 642 .getBounds().width); 643 newHeight = (int) (newWidth / ratio); 644 } else { 645 newHeight = 646 Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image 647 .getBounds().height); 648 newWidth = (int) (newHeight * ratio); 649 } 650 651 // Interesting note... We make the image twice 652 // the needed size so that there is better 653 // resolution under zoom. 654 newWidth = Math.max(newWidth * 2, 1); 655 newHeight = Math.max(newHeight * 2, 1); 656 mScaledSelectedImage = 657 new Image(Display.getDefault(), newWidth, newHeight); 658 GC gc = new GC(mScaledSelectedImage); 659 gc.setBackground(mTextBackgroundColor); 660 gc.fillRectangle(0, 0, newWidth, newHeight); 661 gc.drawImage(mSelectedNode.viewNode.image, 0, 0, 662 mSelectedNode.viewNode.image.getBounds().width, 663 mSelectedNode.viewNode.image.getBounds().height, 0, 0, 664 newWidth, newHeight); 665 gc.dispose(); 666 } 667 668 // Draw the background rectangle 669 e.gc.setBackground(mTextBackgroundColor); 670 e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4 671 - IMAGE_OFFSET, y 672 + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) 673 / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2 674 + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2 675 + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING); 676 677 // Under max zoom, we want the image to be 678 // untransformed. So, get back to the identity 679 // transform. 680 int imageX = x - mScaledSelectedImage.getBounds().width / 4; 681 int imageY = 682 y 683 + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) 684 / 2; 685 686 Transform untransformedTransform = new Transform(Display.getDefault()); 687 e.gc.setTransform(untransformedTransform); 688 float[] pt = new float[] { 689 imageX, imageY 690 }; 691 mTransform.transform(pt); 692 e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage 693 .getBounds().width, mScaledSelectedImage.getBounds().height, 694 (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage 695 .getBounds().width 696 * mZoom / 2), 697 (int) (mScaledSelectedImage.getBounds().height * mZoom / 2)); 698 untransformedTransform.dispose(); 699 e.gc.setTransform(mTransform); 700 } 701 702 // Text stuff 703 704 y += IMAGE_HEIGHT; 705 y += 10; 706 Font font = getFont(8, false); 707 e.gc.setFont(font); 708 709 String text = 710 mSelectedNode.viewNode.viewCount + " view" 711 + (mSelectedNode.viewNode.viewCount != 1 ? "s" : ""); 712 DecimalFormat formatter = new DecimalFormat("0.000"); 713 714 String measureText = 715 "Measure: " 716 + (mSelectedNode.viewNode.measureTime != -1 ? formatter 717 .format(mSelectedNode.viewNode.measureTime) 718 + " ms" : "n/a"); 719 String layoutText = 720 "Layout: " 721 + (mSelectedNode.viewNode.layoutTime != -1 ? formatter 722 .format(mSelectedNode.viewNode.layoutTime) 723 + " ms" : "n/a"); 724 String drawText = 725 "Draw: " 726 + (mSelectedNode.viewNode.drawTime != -1 ? formatter 727 .format(mSelectedNode.viewNode.drawTime) 728 + " ms" : "n/a"); 729 730 org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text); 731 org.eclipse.swt.graphics.Point measureExtent = 732 e.gc.stringExtent(measureText); 733 org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText); 734 org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText); 735 int boxWidth = 736 Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max( 737 layoutExtent.x, drawExtent.x))) 738 + 2 * TEXT_SIDE_OFFSET; 739 int boxHeight = 740 titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING 741 + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2 742 * TEXT_TOP_OFFSET; 743 744 e.gc.setBackground(mTextBackgroundColor); 745 e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight, 746 TEXT_ROUNDING, TEXT_ROUNDING); 747 748 e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); 749 750 y += TEXT_TOP_OFFSET; 751 752 e.gc.drawText(text, x - titleExtent.x / 2, y, true); 753 754 x -= boxWidth / 2; 755 x += TEXT_SIDE_OFFSET; 756 757 y += titleExtent.y + TEXT_SPACING; 758 759 e.gc.drawText(measureText, x, y, true); 760 761 y += measureExtent.y + TEXT_SPACING; 762 763 e.gc.drawText(layoutText, x, y, true); 764 765 y += layoutExtent.y + TEXT_SPACING; 766 767 e.gc.drawText(drawText, x, y, true); 768 769 font.dispose(); 770 } else { 771 mSelectedRectangleLocation = null; 772 mButtonCenter = null; 773 } 774 } 775 } 776 } 777 }; 778 779 private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node, 780 DrawableViewNode selectedNode, Path connectionPath) { 781 if (selectedNode == node && node.viewNode.filtered) { 782 gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); 783 } else if (selectedNode == node) { 784 gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); 785 } else if (node.viewNode.filtered) { 786 gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); 787 } else { 788 gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); 789 } 790 791 int fontHeight = gc.getFontMetrics().getHeight(); 792 793 // Draw the text... 794 int contentWidth = 795 DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; 796 String name = node.viewNode.name; 797 int dotIndex = name.lastIndexOf('.'); 798 if (dotIndex != -1) { 799 name = name.substring(dotIndex + 1); 800 } 801 double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; 802 double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING; 803 drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true); 804 805 y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; 806 807 drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight, 808 8, false); 809 810 y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; 811 if (!node.viewNode.id.equals("NO_ID")) { 812 drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8, 813 false); 814 } 815 816 if (node.viewNode.measureRating != ProfileRating.NONE) { 817 y = 818 node.top + DrawableViewNode.NODE_HEIGHT 819 - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING 820 - sRedImage.getBounds().height; 821 x += 822 (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2; 823 switch (node.viewNode.measureRating) { 824 case GREEN: 825 gc.drawImage(sGreenImage, (int) x, (int) y); 826 break; 827 case YELLOW: 828 gc.drawImage(sYellowImage, (int) x, (int) y); 829 break; 830 case RED: 831 gc.drawImage(sRedImage, (int) x, (int) y); 832 break; 833 } 834 835 x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; 836 switch (node.viewNode.layoutRating) { 837 case GREEN: 838 gc.drawImage(sGreenImage, (int) x, (int) y); 839 break; 840 case YELLOW: 841 gc.drawImage(sYellowImage, (int) x, (int) y); 842 break; 843 case RED: 844 gc.drawImage(sRedImage, (int) x, (int) y); 845 break; 846 } 847 848 x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; 849 switch (node.viewNode.drawRating) { 850 case GREEN: 851 gc.drawImage(sGreenImage, (int) x, (int) y); 852 break; 853 case YELLOW: 854 gc.drawImage(sYellowImage, (int) x, (int) y); 855 break; 856 case RED: 857 gc.drawImage(sRedImage, (int) x, (int) y); 858 break; 859 } 860 } 861 862 org.eclipse.swt.graphics.Point indexExtent = 863 gc.stringExtent(Integer.toString(node.viewNode.index)); 864 x = 865 node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING 866 - indexExtent.x; 867 y = 868 node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING 869 - indexExtent.y; 870 gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT); 871 872 int N = node.children.size(); 873 if (N == 0) { 874 return; 875 } 876 float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N; 877 for (int i = 0; i < N; i++) { 878 DrawableViewNode child = node.children.get(i); 879 paintRecursive(gc, transform, child, selectedNode, connectionPath); 880 float x1 = node.left + DrawableViewNode.NODE_WIDTH; 881 float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2; 882 float x2 = child.left; 883 float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; 884 float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; 885 float cy1 = y1; 886 float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; 887 float cy2 = y2; 888 connectionPath.moveTo(x1, y1); 889 connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); 890 } 891 } 892 893 private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y, 894 double width, double height, int fontSize, boolean bold) { 895 896 Font oldFont = gc.getFont(); 897 898 Font newFont = getFont(fontSize, bold); 899 gc.setFont(newFont); 900 901 org.eclipse.swt.graphics.Point extent = gc.stringExtent(text); 902 903 if (extent.x > width) { 904 // Oh no... we need to scale it. 905 double scale = width / extent.x; 906 float[] transformElements = new float[6]; 907 transform.getElements(transformElements); 908 transform.scale((float) scale, (float) scale); 909 gc.setTransform(transform); 910 911 x /= scale; 912 y /= scale; 913 y += (extent.y / scale - extent.y) / 2; 914 915 gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT); 916 917 transform.setElements(transformElements[0], transformElements[1], transformElements[2], 918 transformElements[3], transformElements[4], transformElements[5]); 919 gc.setTransform(transform); 920 } else { 921 gc.drawText(text, (int) (x + (width - extent.x) / 2), 922 (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT); 923 } 924 gc.setFont(oldFont); 925 newFont.dispose(); 926 927 } 928 929 public static Image paintToImage(DrawableViewNode tree) { 930 Image image = 931 new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math 932 .ceil(tree.bounds.height)); 933 934 Transform transform = new Transform(Display.getDefault()); 935 transform.identity(); 936 transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y); 937 Path connectionPath = new Path(Display.getDefault()); 938 GC gc = new GC(image); 939 940 // Can't use Display.getDefault().getSystemColor in a non-UI thread. 941 Color white = new Color(Display.getDefault(), 255, 255, 255); 942 Color black = new Color(Display.getDefault(), 0, 0, 0); 943 gc.setForeground(white); 944 gc.setBackground(black); 945 gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height); 946 gc.setTransform(transform); 947 paintRecursive(gc, transform, tree, null, connectionPath); 948 gc.drawPath(connectionPath); 949 gc.dispose(); 950 connectionPath.dispose(); 951 white.dispose(); 952 black.dispose(); 953 return image; 954 } 955 956 private static Font getFont(int size, boolean bold) { 957 FontData[] fontData = sSystemFont.getFontData(); 958 for (int i = 0; i < fontData.length; i++) { 959 fontData[i].setHeight(size); 960 if (bold) { 961 fontData[i].setStyle(SWT.BOLD); 962 } 963 } 964 return new Font(Display.getDefault(), fontData); 965 } 966 967 private void doRedraw() { 968 Display.getDefault().syncExec(new Runnable() { 969 public void run() { 970 redraw(); 971 } 972 }); 973 } 974 975 public void loadAllData() { 976 boolean newViewport = mViewport == null; 977 Display.getDefault().syncExec(new Runnable() { 978 public void run() { 979 synchronized (this) { 980 mTree = mModel.getTree(); 981 mSelectedNode = mModel.getSelection(); 982 mViewport = mModel.getViewport(); 983 mZoom = mModel.getZoom(); 984 if (mTree != null && mViewport == null) { 985 mViewport = 986 new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 987 - getBounds().height / 2, getBounds().width, 988 getBounds().height); 989 } else { 990 setTransform(); 991 } 992 } 993 } 994 }); 995 if (newViewport) { 996 mModel.setViewport(mViewport); 997 } 998 } 999 1000 // Fickle behaviour... When a new tree is loaded, the model doesn't know 1001 // about the viewport until it passes through here. 1002 public void treeChanged() { 1003 Display.getDefault().syncExec(new Runnable() { 1004 public void run() { 1005 synchronized (this) { 1006 mTree = mModel.getTree(); 1007 mSelectedNode = mModel.getSelection(); 1008 if (mTree == null) { 1009 mViewport = null; 1010 } else { 1011 mViewport = 1012 new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 1013 - getBounds().height / 2, getBounds().width, 1014 getBounds().height); 1015 } 1016 } 1017 } 1018 }); 1019 if (mViewport != null) { 1020 mModel.setViewport(mViewport); 1021 } else { 1022 doRedraw(); 1023 } 1024 } 1025 1026 private void setTransform() { 1027 if (mViewport != null && mTree != null) { 1028 // Set the transform. 1029 mTransform.identity(); 1030 mInverse.identity(); 1031 1032 mTransform.scale((float) mZoom, (float) mZoom); 1033 mInverse.scale((float) mZoom, (float) mZoom); 1034 mTransform.translate((float) -mViewport.x, (float) -mViewport.y); 1035 mInverse.translate((float) -mViewport.x, (float) -mViewport.y); 1036 mInverse.invert(); 1037 } 1038 } 1039 1040 // Note the syncExec and then synchronized... It avoids deadlock 1041 public void viewportChanged() { 1042 Display.getDefault().syncExec(new Runnable() { 1043 public void run() { 1044 synchronized (this) { 1045 mViewport = mModel.getViewport(); 1046 mZoom = mModel.getZoom(); 1047 setTransform(); 1048 } 1049 } 1050 }); 1051 doRedraw(); 1052 } 1053 1054 public void zoomChanged() { 1055 viewportChanged(); 1056 } 1057 1058 public void selectionChanged() { 1059 synchronized (this) { 1060 mSelectedNode = mModel.getSelection(); 1061 if (mSelectedNode != null && mSelectedNode.viewNode.image == null) { 1062 HierarchyViewerDirector.getDirector() 1063 .loadCaptureInBackground(mSelectedNode.viewNode); 1064 } 1065 } 1066 doRedraw(); 1067 } 1068 } 1069