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 @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 handleMouseDrag(Point pt)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 transformPoint(double x, double y)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 paintRecursive(GC gc, Transform transform, DrawableViewNode node, DrawableViewNode selectedNode, Path connectionPath)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 drawTextInArea(GC gc, Transform transform, String text, double x, double y, double width, double height, int fontSize, boolean bold)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 paintToImage(DrawableViewNode tree)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 getFont(int size, boolean bold)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 doRedraw()977 private void doRedraw() { 978 Display.getDefault().syncExec(new Runnable() { 979 @Override 980 public void run() { 981 redraw(); 982 } 983 }); 984 } 985 loadAllData()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 treeChanged()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 setTransform()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 viewportChanged()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 zoomChanged()1071 public void zoomChanged() { 1072 viewportChanged(); 1073 } 1074 1075 @Override selectionChanged()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