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