• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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