• 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         @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