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 TreeView(Composite parent)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 loadResources()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 handleMouseDrag(Point pt)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 transformPoint(double x, double y)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 paintRecursive(GC gc, Transform transform, DrawableViewNode node, DrawableViewNode selectedNode, Path connectionPath)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 drawTextInArea(GC gc, Transform transform, String text, double x, double y, double width, double height, int fontSize, boolean bold)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 paintToImage(DrawableViewNode tree)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 getFont(int size, boolean bold)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 doRedraw()967 private void doRedraw() { 968 Display.getDefault().syncExec(new Runnable() { 969 public void run() { 970 redraw(); 971 } 972 }); 973 } 974 loadAllData()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. treeChanged()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 setTransform()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 viewportChanged()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 zoomChanged()1054 public void zoomChanged() { 1055 viewportChanged(); 1056 } 1057 selectionChanged()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