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
17package com.android.hierarchyviewerlib.ui;
18
19import com.android.ddmuilib.ImageLoader;
20import com.android.hierarchyviewerlib.HierarchyViewerDirector;
21import com.android.hierarchyviewerlib.device.ViewNode.ProfileRating;
22import com.android.hierarchyviewerlib.models.TreeViewModel;
23import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
24import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
25import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
26import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
27
28import org.eclipse.swt.SWT;
29import org.eclipse.swt.events.DisposeEvent;
30import org.eclipse.swt.events.DisposeListener;
31import org.eclipse.swt.events.KeyEvent;
32import org.eclipse.swt.events.KeyListener;
33import org.eclipse.swt.events.MouseEvent;
34import org.eclipse.swt.events.MouseListener;
35import org.eclipse.swt.events.MouseMoveListener;
36import org.eclipse.swt.events.MouseWheelListener;
37import org.eclipse.swt.events.PaintEvent;
38import org.eclipse.swt.events.PaintListener;
39import org.eclipse.swt.graphics.Color;
40import org.eclipse.swt.graphics.Font;
41import org.eclipse.swt.graphics.FontData;
42import org.eclipse.swt.graphics.GC;
43import org.eclipse.swt.graphics.Image;
44import org.eclipse.swt.graphics.Path;
45import org.eclipse.swt.graphics.RGB;
46import org.eclipse.swt.graphics.Transform;
47import org.eclipse.swt.widgets.Canvas;
48import org.eclipse.swt.widgets.Composite;
49import org.eclipse.swt.widgets.Display;
50import org.eclipse.swt.widgets.Event;
51import org.eclipse.swt.widgets.Listener;
52
53import java.text.DecimalFormat;
54
55public 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