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