Tweaking and moving.

Change-Id: I9906268a91c53c7b9e938b9c969cedeae2a4303a
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/Android.mk b/hierarchyviewer2/libs/hierarchyviewerlib/src/Android.mk
index 1be1a29..922e337 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/Android.mk
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/Android.mk
@@ -16,8 +16,13 @@
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_RESOURCE_DIRS := resources
 
-LOCAL_JAVA_LIBRARIES := ddmlib
+LOCAL_JAVA_LIBRARIES := ddmlib \
+    ddmuilib \
+    hierarchyviewerlib \
+    swt \
+    org.eclipse.jface_3.4.2.M20090107-0800
 
 LOCAL_MODULE := hierarchyviewerlib
 
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/HierarchyViewerDirector.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/HierarchyViewerDirector.java
index 65cb24f..48a45dd 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/HierarchyViewerDirector.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/HierarchyViewerDirector.java
@@ -28,6 +28,16 @@
 import com.android.hierarchyviewerlib.device.WindowUpdater;
 import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo;
 import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
+import com.android.hierarchyviewerlib.ui.CaptureDisplay;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
 
 import java.io.IOException;
 
@@ -175,19 +185,12 @@
                 public void run() {
                     if (ComponentRegistry.getDeviceSelectionModel().getFocusedWindow(device) != -1
                             && device == ComponentRegistry.getPixelPerfectModel().getDevice()) {
-                        try {
-                            ViewNode viewNode =
-                                    DeviceBridge.loadWindowData(Window.getFocusedWindow(device));
-                            RawImage screenshot = device.getScreenshot();
-                            ComponentRegistry.getPixelPerfectModel().setFocusData(screenshot,
+                        ViewNode viewNode =
+                                DeviceBridge.loadWindowData(Window.getFocusedWindow(device));
+                        Image screenshotImage = getScreenshotImage(device);
+                        if (screenshotImage != null) {
+                            ComponentRegistry.getPixelPerfectModel().setFocusData(screenshotImage,
                                     viewNode);
-                        } catch (IOException e) {
-                            Log.e(TAG, "Unable to load screenshot from device " + device);
-                        } catch (TimeoutException e) {
-                            Log.e(TAG, "Timeout loading screenshot from device " + device);
-                        } catch (AdbCommandRejectedException e) {
-                            Log.e(TAG, "Adb rejected command to load screenshot from device "
-                                    + device);
                         }
                     }
                     synchronized (HierarchyViewerDirector.this) {
@@ -202,22 +205,48 @@
     public void loadPixelPerfectData(final IDevice device) {
         executeInBackground(new Runnable() {
             public void run() {
-                try {
-                    RawImage screenshot = device.getScreenshot();
+                Image screenshotImage = getScreenshotImage(device);
+                if (screenshotImage != null) {
                     ViewNode viewNode =
                             DeviceBridge.loadWindowData(Window.getFocusedWindow(device));
-                    ComponentRegistry.getPixelPerfectModel().setData(device, screenshot, viewNode);
-                } catch (IOException e) {
-                    Log.e(TAG, "Unable to load screenshot from device " + device);
-                } catch (TimeoutException e) {
-                    Log.e(TAG, "Timeout loading screenshot from device " + device);
-                } catch (AdbCommandRejectedException e) {
-                    Log.e(TAG, "Adb rejected command to load screenshot from device " + device);
+                    ComponentRegistry.getPixelPerfectModel().setData(device, screenshotImage,
+                            viewNode);
                 }
             }
         });
     }
 
+    private Image getScreenshotImage(IDevice device) {
+        try {
+            final RawImage screenshot = device.getScreenshot();
+            if (screenshot == null) {
+                return null;
+            }
+            class ImageContainer {
+                public Image image;
+            }
+            final ImageContainer imageContainer = new ImageContainer();
+            Display.getDefault().syncExec(new Runnable() {
+                public void run() {
+                    ImageData imageData =
+                            new ImageData(screenshot.width, screenshot.height, screenshot.bpp,
+                                    new PaletteData(screenshot.getRedMask(), screenshot
+                                            .getGreenMask(), screenshot.getBlueMask()), 1,
+                                    screenshot.data);
+                    imageContainer.image = new Image(Display.getDefault(), imageData);
+                }
+            });
+            return imageContainer.image;
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to load screenshot from device " + device);
+        } catch (TimeoutException e) {
+            Log.e(TAG, "Timeout loading screenshot from device " + device);
+        } catch (AdbCommandRejectedException e) {
+            Log.e(TAG, "Adb rejected command to load screenshot from device " + device);
+        }
+        return null;
+    }
+
     public void loadViewTreeData(final Window window) {
         executeInBackground(new Runnable() {
             public void run() {
@@ -230,4 +259,47 @@
             }
         });
     }
+
+    public void loadOverlay(final Shell shell) {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
+                fileDialog.setFilterExtensions(new String[] {
+                    "*.jpg;*.jpeg;*.png;*.gif;*.bmp"
+                });
+                fileDialog.setFilterNames(new String[] {
+                    "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)"
+                });
+                String fileName = fileDialog.open();
+                if (fileName != null) {
+                    try {
+                        Image image = new Image(Display.getDefault(), fileName);
+                        ComponentRegistry.getPixelPerfectModel().setOverlayImage(image);
+                    } catch (SWTException e) {
+                        Log.e(TAG, "Unable to load image from " + fileName);
+                    }
+                }
+            }
+        });
+    }
+
+    public void showCapture(final Shell shell, final ViewNode viewNode) {
+        executeInBackground(new Runnable() {
+            public void run() {
+                final Image image = DeviceBridge.loadCapture(viewNode.window, viewNode);
+                if (image != null) {
+                    viewNode.image = image;
+
+                    // Force the layout viewer to redraw.
+                    ComponentRegistry.getTreeViewModel().notifySelectionChanged();
+
+                    Display.getDefault().asyncExec(new Runnable() {
+                        public void run() {
+                            CaptureDisplay.show(shell, viewNode, image);
+                        }
+                    });
+                }
+            }
+        });
+    }
 }
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceBridge.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceBridge.java
index af3f9f1..23c6f07 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceBridge.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceBridge.java
@@ -24,6 +24,9 @@
 import com.android.ddmlib.ShellCommandUnresponsiveException;
 import com.android.ddmlib.TimeoutException;
 
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -413,7 +416,7 @@
                     currentNode = currentNode.parent;
                     currentDepth--;
                 }
-                currentNode = new ViewNode(currentNode, line.substring(depth));
+                currentNode = new ViewNode(window, currentNode, line.substring(depth));
                 currentDepth = depth;
             }
             if (currentNode == null) {
@@ -447,7 +450,11 @@
             if (protocol < 3) {
                 return loadProfileData(viewNode, in);
             } else {
-                return loadProfileDataRecursive(viewNode, in);
+                boolean ret = loadProfileDataRecursive(viewNode, in);
+                if (ret) {
+                    viewNode.setProfileRatings();
+                }
+                return ret;
             }
         } catch (IOException e) {
             Log.e(TAG, "Unable to load profiling data for window " + window.getTitle()
@@ -485,4 +492,22 @@
         }
         return true;
     }
+
+    public static Image loadCapture(Window window, ViewNode viewNode) {
+        DeviceConnection connection = null;
+        try {
+            connection = new DeviceConnection(window.getDevice());
+            connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString());
+            return new Image(Display.getDefault(), connection.getSocket().getInputStream());
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to capture data for node " + viewNode + " in window "
+                    + window.getTitle() + " on device " + window.getDevice());
+        } finally {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        return null;
+    }
+
 }
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceConnection.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceConnection.java
index 581f76b..18b9619 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceConnection.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/DeviceConnection.java
@@ -24,6 +24,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.net.InetSocketAddress;
+import java.net.Socket;
 import java.nio.channels.SocketChannel;
 
 /**
@@ -61,6 +62,10 @@
         return out;
     }
 
+    public Socket getSocket() {
+        return socketChannel.socket();
+    }
+
     public void sendCommand(String command) throws IOException {
         BufferedWriter out = getOutputStream();
         out.write(command);
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/ViewNode.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/ViewNode.java
index 2dd9b61..2872952 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/ViewNode.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/device/ViewNode.java
@@ -16,6 +16,8 @@
 
 package com.android.hierarchyviewerlib.device;
 
+import org.eclipse.swt.graphics.Image;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -26,6 +28,15 @@
 import java.util.TreeSet;
 
 public class ViewNode {
+
+    public static enum ProfileRating {
+        RED, YELLOW, GREEN, NONE
+    };
+
+    private static final double RED_THRESHOLD = 0.8;
+
+    private static final double YELLOW_THRESHOLD = 0.5;
+
     public static final String MISCELLANIOUS = "miscellaneous";
 
     public String id;
@@ -86,9 +97,22 @@
 
     public double drawTime;
 
+    public ProfileRating measureRating = ProfileRating.NONE;
+
+    public ProfileRating layoutRating = ProfileRating.NONE;
+
+    public ProfileRating drawRating = ProfileRating.NONE;
+
     public Set<String> categories = new TreeSet<String>();
 
-    public ViewNode(ViewNode parent, String data) {
+    public Window window;
+
+    public Image image;
+
+    public int imageReferences = 1;
+
+    public ViewNode(Window window, ViewNode parent, String data) {
+        this.window = window;
         this.parent = parent;
         index = this.parent == null ? 0 : this.parent.children.size();
         if (this.parent != null) {
@@ -106,6 +130,25 @@
         drawTime = -1;
     }
 
+    public void dispose() {
+        final int N = children.size();
+        for(int i = 0; i<N; i++) {
+            children.get(i).dispose();
+        }
+        dereferenceImage();
+    }
+
+    public void referenceImage() {
+        imageReferences++;
+    }
+
+    public void dereferenceImage() {
+        imageReferences--;
+        if (image != null && imageReferences == 0) {
+            image.dispose();
+        }
+    }
+
     private void loadProperties(String data) {
         int start = 0;
         boolean stop;
@@ -141,9 +184,9 @@
         top = namedProperties.containsKey("mTop") ?
                 getInt("mTop", 0) : getInt("layout:mTop", 0);
         width = namedProperties.containsKey("getWidth()") ?
-                getInt("getWidth()", 0) : getInt("measurement:getWidth()", 0);
+                getInt("getWidth()", 0) : getInt("layout:getWidth()", 0);
         height = namedProperties.containsKey("getHeight()") ?
-                getInt("getHeight()", 0) : getInt("measurement:getHeight()", 0);
+                getInt("getHeight()", 0) : getInt("layout:getHeight()", 0);
         scrollX = namedProperties.containsKey("mScrollX") ?
                 getInt("mScrollX", 0) : getInt("scrolling:mScrollX", 0);
         scrollY = namedProperties.containsKey("mScrollY") ?
@@ -158,19 +201,19 @@
                 getInt("mPaddingBottom", 0) : getInt("padding:mPaddingBottom", 0);
         marginLeft = namedProperties.containsKey("layout_leftMargin") ?
                 getInt("layout_leftMargin", Integer.MIN_VALUE) :
-                getInt("layout:leftMargin", Integer.MIN_VALUE);
+                getInt("layout:layout_leftMargin", Integer.MIN_VALUE);
         marginRight = namedProperties.containsKey("layout_rightMargin") ?
                 getInt("layout_rightMargin", Integer.MIN_VALUE) :
-                getInt("layout:rightMargin", Integer.MIN_VALUE);
+                getInt("layout:layout_rightMargin", Integer.MIN_VALUE);
         marginTop = namedProperties.containsKey("layout_topMargin") ?
                 getInt("layout_topMargin", Integer.MIN_VALUE) :
-                getInt("layout:topMargin", Integer.MIN_VALUE);
+                getInt("layout:layout_topMargin", Integer.MIN_VALUE);
         marginBottom = namedProperties.containsKey("layout_bottomMargin") ?
                 getInt("layout_bottomMargin", Integer.MIN_VALUE) :
-                getInt("layout:bottomMargin", Integer.MIN_VALUE);
+                getInt("layout:layout_bottomMargin", Integer.MIN_VALUE);
         baseline = namedProperties.containsKey("getBaseline()") ?
                 getInt("getBaseline()", 0) :
-                getInt("measurement:getBaseline()", 0);
+                getInt("layout:getBaseline()", 0);
         willNotDraw = namedProperties.containsKey("willNotDraw()") ?
                 getBoolean("willNotDraw()", false) :
                 getBoolean("drawing:willNotDraw()", false);
@@ -193,6 +236,48 @@
         }
     }
 
+    public void setProfileRatings() {
+        final int N = children.size();
+        if (N > 1) {
+            double totalMeasure = 0;
+            double totalLayout = 0;
+            double totalDraw = 0;
+            for (int i = 0; i < N; i++) {
+                ViewNode child = children.get(i);
+                totalMeasure += child.measureTime;
+                totalLayout += child.layoutTime;
+                totalDraw += child.drawTime;
+            }
+            for (int i = 0; i < N; i++) {
+                ViewNode child = children.get(i);
+                if (child.measureTime / totalMeasure >= RED_THRESHOLD) {
+                    child.measureRating = ProfileRating.RED;
+                } else if (child.measureTime / totalMeasure >= YELLOW_THRESHOLD) {
+                    child.measureRating = ProfileRating.YELLOW;
+                } else {
+                    child.measureRating = ProfileRating.GREEN;
+                }
+                if (child.layoutTime / totalLayout >= RED_THRESHOLD) {
+                    child.layoutRating = ProfileRating.RED;
+                } else if (child.layoutTime / totalLayout >= YELLOW_THRESHOLD) {
+                    child.layoutRating = ProfileRating.YELLOW;
+                } else {
+                    child.layoutRating = ProfileRating.GREEN;
+                }
+                if (child.drawTime / totalDraw >= RED_THRESHOLD) {
+                    child.drawRating = ProfileRating.RED;
+                } else if (child.drawTime / totalDraw >= YELLOW_THRESHOLD) {
+                    child.drawRating = ProfileRating.YELLOW;
+                } else {
+                    child.drawRating = ProfileRating.GREEN;
+                }
+            }
+        }
+        for (int i = 0; i < N; i++) {
+            children.get(i).setProfileRatings();
+        }
+    }
+
     private boolean getBoolean(String name, boolean defaultValue) {
         Property p = namedProperties.get(name);
         if (p != null) {
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/PixelPerfectModel.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/PixelPerfectModel.java
index 4f19368..e8f8240 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/PixelPerfectModel.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/PixelPerfectModel.java
@@ -20,6 +20,10 @@
 import com.android.ddmlib.RawImage;
 import com.android.hierarchyviewerlib.device.ViewNode;
 
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Display;
+
 import java.util.ArrayList;
 
 public class PixelPerfectModel {
@@ -30,20 +34,9 @@
 
     private static final int DEFAULT_ZOOM = 8;
 
-    public static class Point {
-        public int x;
-
-        public int y;
-
-        Point(int x, int y) {
-            this.x = x;
-            this.y = y;
-        }
-    }
-
     private IDevice device;
 
-    private RawImage image;
+    private Image image;
 
     private Point crosshairLocation;
 
@@ -56,19 +49,31 @@
     private final ArrayList<ImageChangeListener> imageChangeListeners =
             new ArrayList<ImageChangeListener>();
 
-    public void setData(IDevice device, RawImage image, ViewNode viewNode) {
-        synchronized (this) {
-            this.device = device;
-            this.image = image;
-            this.viewNode = viewNode;
-            if (image != null) {
-                this.crosshairLocation = new Point(image.width / 2, image.height / 2);
-            } else {
-                this.crosshairLocation = null;
+    private Image overlayImage;
+
+    private double overlayTransparency = 0.5;
+
+    public void setData(final IDevice device, final Image image, final ViewNode viewNode) {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    PixelPerfectModel.this.device = device;
+                    if (PixelPerfectModel.this.image != null) {
+                        PixelPerfectModel.this.image.dispose();
+                    }
+                    PixelPerfectModel.this.image = image;
+                    PixelPerfectModel.this.viewNode = viewNode;
+                    if (image != null) {
+                        PixelPerfectModel.this.crosshairLocation =
+                                new Point(image.getBounds().width / 2, image.getBounds().height / 2);
+                    } else {
+                        PixelPerfectModel.this.crosshairLocation = null;
+                    }
+                    PixelPerfectModel.this.selected = null;
+                    zoom = DEFAULT_ZOOM;
+                }
             }
-            this.selected = null;
-            zoom = DEFAULT_ZOOM;
-        }
+        });
         notifyImageLoaded();
     }
 
@@ -86,12 +91,19 @@
         notifySelectionChanged();
     }
 
-    public void setFocusData(RawImage image, ViewNode viewNode) {
-        synchronized (this) {
-            this.image = image;
-            this.viewNode = viewNode;
-            this.selected = null;
-        }
+    public void setFocusData(final Image image, final ViewNode viewNode) {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    if (PixelPerfectModel.this.image != null) {
+                        PixelPerfectModel.this.image.dispose();
+                    }
+                    PixelPerfectModel.this.image = image;
+                    PixelPerfectModel.this.viewNode = viewNode;
+                    PixelPerfectModel.this.selected = null;
+                }
+            }
+        });
         notifyFocusChanged();
     }
 
@@ -108,6 +120,29 @@
         notifyZoomChanged();
     }
 
+    public void setOverlayImage(final Image overlayImage) {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (PixelPerfectModel.this) {
+                    if (PixelPerfectModel.this.overlayImage != null) {
+                        PixelPerfectModel.this.overlayImage.dispose();
+                    }
+                    PixelPerfectModel.this.overlayImage = overlayImage;
+                }
+            }
+        });
+        notifyOverlayChanged();
+    }
+
+    public void setOverlayTransparency(double value) {
+        synchronized (this) {
+            value = Math.max(value, 0);
+            value = Math.min(value, 1);
+            overlayTransparency = value;
+        }
+        notifyOverlayTransparencyChanged();
+    }
+
     public ViewNode getViewNode() {
         synchronized (this) {
             return viewNode;
@@ -120,7 +155,7 @@
         }
     }
 
-    public RawImage getImage() {
+    public Image getImage() {
         synchronized (this) {
             return image;
         }
@@ -144,6 +179,18 @@
         }
     }
 
+    public Image getOverlayImage() {
+        synchronized (this) {
+            return overlayImage;
+        }
+    }
+
+    public double getOverlayTransparency() {
+        synchronized (this) {
+            return overlayTransparency;
+        }
+    }
+
     public static interface ImageChangeListener {
         public void imageLoaded();
 
@@ -156,6 +203,10 @@
         public void focusChanged();
 
         public void zoomChanged();
+
+        public void overlayChanged();
+
+        public void overlayTransparencyChanged();
     }
 
     private ImageChangeListener[] getImageChangeListenerList() {
@@ -225,6 +276,24 @@
         }
     }
 
+    public void notifyOverlayChanged() {
+        ImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].overlayChanged();
+            }
+        }
+    }
+
+    public void notifyOverlayTransparencyChanged() {
+        ImageChangeListener[] listeners = getImageChangeListenerList();
+        if (listeners != null) {
+            for (int i = 0; i < listeners.length; i++) {
+                listeners[i].overlayTransparencyChanged();
+            }
+        }
+    }
+
     public void addImageChangeListener(ImageChangeListener listener) {
         synchronized (imageChangeListeners) {
             imageChangeListeners.add(listener);
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/TreeViewModel.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/TreeViewModel.java
index f6279df..c49ce95 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/TreeViewModel.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/models/TreeViewModel.java
@@ -18,9 +18,9 @@
 
 import com.android.hierarchyviewerlib.device.ViewNode;
 import com.android.hierarchyviewerlib.device.Window;
-import com.android.hierarchyviewerlib.scene.DrawableViewNode;
-import com.android.hierarchyviewerlib.scene.DrawableViewNode.Point;
-import com.android.hierarchyviewerlib.scene.DrawableViewNode.Rectangle;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
 
 import java.util.ArrayList;
 
@@ -44,6 +44,9 @@
 
     public void setData(Window window, ViewNode viewNode) {
         synchronized (this) {
+            if (tree != null) {
+                tree.viewNode.dispose();
+            }
             this.window = window;
             tree = new DrawableViewNode(viewNode);
             tree.setLeft();
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/CaptureDisplay.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/CaptureDisplay.java
new file mode 100644
index 0000000..54a94fd
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/CaptureDisplay.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.device.ViewNode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+
+public class CaptureDisplay {
+    private static Shell shell;
+
+    private static Canvas canvas;
+
+    private static Image image;
+
+    private static ViewNode viewNode;
+
+    private static Composite buttonBar;
+
+    private static Button onWhite;
+
+    private static Button onBlack;
+
+    private static Button showExtras;
+
+    public static void show(Shell parentShell, ViewNode viewNode, Image image) {
+        if (shell == null) {
+            createShell();
+        }
+        if (shell.isVisible() && CaptureDisplay.viewNode != null) {
+            CaptureDisplay.viewNode.dereferenceImage();
+        }
+        CaptureDisplay.image = image;
+        CaptureDisplay.viewNode = viewNode;
+        viewNode.referenceImage();
+        int dotIndex = viewNode.name.lastIndexOf('.');
+        if (dotIndex != -1) {
+            shell.setText(viewNode.name.substring(dotIndex + 1));
+        } else {
+            shell.setText(viewNode.name);
+        }
+
+        boolean shellVisible = shell.isVisible();
+        if (!shellVisible) {
+            shell.setSize(0, 0);
+        }
+        shell.open();
+        Rectangle bounds =
+                shell.computeTrim(0, 0, Math.max(buttonBar.getBounds().width,
+                        image.getBounds().width), buttonBar.getBounds().height
+                        + image.getBounds().height + 5);
+        System.out.println(bounds);
+        shell.setSize(bounds.width, bounds.height);
+        if (!shellVisible) {
+            shell.setLocation(parentShell.getBounds().x
+                    + (parentShell.getBounds().width - bounds.width) / 2, parentShell.getBounds().y
+                    + (parentShell.getBounds().height - bounds.height) / 2);
+        }
+        if (shellVisible) {
+            canvas.redraw();
+        }
+    }
+
+    private static void createShell() {
+        shell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE);
+        GridLayout gridLayout = new GridLayout();
+        gridLayout.marginWidth = 0;
+        gridLayout.marginHeight = 0;
+        shell.setLayout(gridLayout);
+
+        buttonBar = new Composite(shell, SWT.NONE);
+        RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
+        rowLayout.pack = true;
+        rowLayout.center = true;
+        buttonBar.setLayout(rowLayout);
+        Composite buttons = new Composite(buttonBar, SWT.NONE);
+        buttons.setLayout(new FillLayout());
+
+        onWhite = new Button(buttons, SWT.TOGGLE);
+        onWhite.setText("On White");
+        onBlack = new Button(buttons, SWT.TOGGLE);
+        onBlack.setText("On Black");
+        onBlack.setSelection(true);
+        onWhite.addSelectionListener(whiteSelectionListener);
+        onBlack.addSelectionListener(blackSelectionListener);
+
+        showExtras = new Button(buttonBar, SWT.CHECK);
+        showExtras.setText("Show Extras");
+        showExtras.addSelectionListener(extrasSelectionListener);
+
+        canvas = new Canvas(shell, SWT.NONE);
+        canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+        canvas.addPaintListener(paintListener);
+
+        shell.addShellListener(shellListener);
+    }
+
+    private static PaintListener paintListener = new PaintListener() {
+
+        public void paintControl(PaintEvent e) {
+            if (onWhite.getSelection()) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+            } else {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+            }
+            e.gc.fillRectangle(0, 0, canvas.getBounds().width, canvas.getBounds().height);
+            if (image != null) {
+                int width = image.getBounds().width;
+                int height = image.getBounds().height;
+                int x = (canvas.getBounds().width - width) / 2;
+                int y = (canvas.getBounds().height - height) / 2;
+                e.gc.drawImage(image, x, y);
+                if (showExtras.getSelection()) {
+                    if ((viewNode.paddingLeft | viewNode.paddingRight | viewNode.paddingTop | viewNode.paddingBottom) != 0) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE));
+                        e.gc.drawRectangle(x + viewNode.paddingLeft, y + viewNode.paddingTop, width
+                                - viewNode.paddingLeft - viewNode.paddingRight - 1, height
+                                - viewNode.paddingTop - viewNode.paddingBottom - 1);
+                    }
+                    if (viewNode.hasMargins) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN));
+                        e.gc.drawRectangle(x - viewNode.marginLeft, y - viewNode.marginTop, width
+                                + viewNode.marginLeft + viewNode.marginRight - 1, height
+                                + viewNode.marginTop + viewNode.marginBottom - 1);
+                    }
+                    if (viewNode.baseline != -1) {
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
+                        e.gc.drawLine(x, y + viewNode.baseline, x + width - 1, viewNode.baseline);
+                    }
+                }
+            }
+        }
+    };
+
+    private static ShellAdapter shellListener = new ShellAdapter() {
+        @Override
+        public void shellClosed(ShellEvent e) {
+            e.doit = false;
+            shell.setVisible(false);
+            if (viewNode != null) {
+                viewNode.dereferenceImage();
+            }
+        }
+
+    };
+
+    private static SelectionListener whiteSelectionListener = new SelectionListener() {
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        public void widgetSelected(SelectionEvent e) {
+            onWhite.setSelection(true);
+            onBlack.setSelection(false);
+            canvas.redraw();
+        }
+    };
+
+    private static SelectionListener blackSelectionListener = new SelectionListener() {
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        public void widgetSelected(SelectionEvent e) {
+            onBlack.setSelection(true);
+            onWhite.setSelection(false);
+            canvas.redraw();
+        }
+    };
+
+    private static SelectionListener extrasSelectionListener = new SelectionListener() {
+        public void widgetDefaultSelected(SelectionEvent e) {
+            // pass
+        }
+
+        public void widgetSelected(SelectionEvent e) {
+            canvas.redraw();
+        }
+    };
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/DeviceSelector.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/DeviceSelector.java
new file mode 100644
index 0000000..5e7c606
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/DeviceSelector.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.device.Window;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
+import com.android.hierarchyviewerlib.models.DeviceSelectionModel.WindowChangeListener;
+
+import org.eclipse.jface.viewers.IFontProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+public class DeviceSelector extends Composite implements WindowChangeListener, SelectionListener {
+    private TreeViewer treeViewer;
+
+    private Tree tree;
+
+    private DeviceSelectionModel model;
+
+    private Font boldFont;
+
+    private Image deviceImage;
+
+    private Image emulatorImage;
+
+    private final static int ICON_WIDTH = 16;
+
+    private class ContentProvider implements ITreeContentProvider, ILabelProvider, IFontProvider {
+        public Object[] getChildren(Object parentElement) {
+            if (parentElement instanceof IDevice) {
+                Window[] list = model.getWindows((IDevice) parentElement);
+                if (list != null) {
+                    return list;
+                }
+            }
+            return new Object[0];
+        }
+
+        public Object getParent(Object element) {
+            if (element instanceof Window) {
+                return ((Window) element).getDevice();
+            }
+            return null;
+        }
+
+        public boolean hasChildren(Object element) {
+            if (element instanceof IDevice) {
+                Window[] list = model.getWindows((IDevice) element);
+                if (list != null) {
+                    return list.length != 0;
+                }
+            }
+            return false;
+        }
+
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof DeviceSelectionModel) {
+                return model.getDevices();
+            }
+            return new Object[0];
+        }
+
+        public void dispose() {
+            // pass
+        }
+
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        public Image getImage(Object element) {
+            if (element instanceof IDevice) {
+                if (((IDevice) element).isEmulator()) {
+                    return emulatorImage;
+                }
+                return deviceImage;
+            }
+            return null;
+        }
+
+        public String getText(Object element) {
+            if (element instanceof IDevice) {
+                return ((IDevice) element).toString();
+            } else if (element instanceof Window) {
+                return ((Window) element).getTitle();
+            }
+            return null;
+        }
+
+        public Font getFont(Object element) {
+            if (element instanceof Window) {
+                int focusedWindow = model.getFocusedWindow(((Window) element).getDevice());
+                if (focusedWindow == ((Window) element).getHashCode()) {
+                    return boldFont;
+                }
+            }
+            return null;
+        }
+
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public DeviceSelector(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        treeViewer = new TreeViewer(this, SWT.SINGLE);
+        treeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+        tree = treeViewer.getTree();
+        tree.setLinesVisible(true);
+        tree.addSelectionListener(this);
+
+        addDisposeListener(disposeListener);
+
+        loadResources();
+
+        model = ComponentRegistry.getDeviceSelectionModel();
+        ContentProvider contentProvider = new ContentProvider();
+        treeViewer.setContentProvider(contentProvider);
+        treeViewer.setLabelProvider(contentProvider);
+        model.addWindowChangeListener(this);
+        treeViewer.setInput(model);
+    }
+
+    public void loadResources() {
+        Display display = Display.getDefault();
+        Font systemFont = display.getSystemFont();
+        FontData[] fontData = systemFont.getFontData();
+        FontData[] newFontData = new FontData[fontData.length];
+        for (int i = 0; i < fontData.length; i++) {
+            newFontData[i] =
+                    new FontData(fontData[i].getName(), fontData[i].getHeight(), fontData[i]
+                            .getStyle()
+                            | SWT.BOLD);
+        }
+        boldFont = new Font(Display.getDefault(), newFontData);
+
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        deviceImage =
+                loader.loadImage(display, "device.png", ICON_WIDTH, ICON_WIDTH, display
+                        .getSystemColor(SWT.COLOR_RED));
+
+        emulatorImage =
+                loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, display
+                        .getSystemColor(SWT.COLOR_BLUE));
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeWindowChangeListener(DeviceSelector.this);
+            boldFont.dispose();
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return tree.setFocus();
+    }
+
+    public void deviceConnected(final IDevice device) {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                treeViewer.refresh();
+                treeViewer.setExpandedState(device, true);
+            }
+        });
+    }
+
+    public void deviceChanged(final IDevice device) {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                TreeSelection selection = (TreeSelection) treeViewer.getSelection();
+                treeViewer.refresh(device);
+                if (selection.getFirstElement() instanceof Window
+                        && ((Window) selection.getFirstElement()).getDevice() == device) {
+                    treeViewer.setSelection(selection, true);
+                }
+            }
+        });
+    }
+
+    public void deviceDisconnected(final IDevice device) {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                treeViewer.refresh();
+            }
+        });
+    }
+
+    public void focusChanged(final IDevice device) {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                TreeSelection selection = (TreeSelection) treeViewer.getSelection();
+                treeViewer.refresh(device);
+                if (selection.getFirstElement() instanceof Window
+                        && ((Window) selection.getFirstElement()).getDevice() == device) {
+                    treeViewer.setSelection(selection, true);
+                }
+            }
+        });
+    }
+
+    public void widgetDefaultSelected(SelectionEvent e) {
+        // TODO: Double click to open view hierarchy
+        Object selection = ((TreeItem) e.item).getData();
+        if (selection instanceof IDevice) {
+            ComponentRegistry.getDirector().loadPixelPerfectData((IDevice) selection);
+        } else if (selection instanceof Window) {
+            ComponentRegistry.getDirector().loadViewTreeData((Window) selection);
+        }
+    }
+
+    public void widgetSelected(SelectionEvent e) {
+        Object selection = ((TreeItem) e.item).getData();
+        if (selection instanceof IDevice) {
+            model.setSelection((IDevice) selection, null);
+        } else if (selection instanceof Window) {
+            model.setSelection(((Window) selection).getDevice(), (Window) selection);
+        }
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/LayoutViewer.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/LayoutViewer.java
new file mode 100644
index 0000000..917e96e
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/LayoutViewer.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.TreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+import java.util.ArrayList;
+
+public class LayoutViewer extends Canvas implements TreeChangeListener {
+
+    private TreeViewModel model;
+
+    private DrawableViewNode tree;
+
+    private DrawableViewNode selectedNode;
+
+    private Transform transform;
+
+    private Transform inverse;
+
+    private double scale;
+
+    private boolean showExtras = true;
+
+    public LayoutViewer(Composite parent) {
+        super(parent, SWT.NONE);
+        model = ComponentRegistry.getTreeViewModel();
+        model.addTreeChangeListener(this);
+
+        addDisposeListener(disposeListener);
+        addPaintListener(paintListener);
+        addListener(SWT.Resize, resizeListener);
+        addMouseListener(mouseListener);
+
+        transform = new Transform(Display.getDefault());
+        inverse = new Transform(Display.getDefault());
+    }
+
+    public void setShowExtras(boolean show) {
+        showExtras = show;
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            transform.dispose();
+            inverse.dispose();
+        }
+    };
+
+    private Listener resizeListener = new Listener() {
+        public void handleEvent(Event e) {
+            synchronized (this) {
+                setTransform();
+            }
+        }
+    };
+
+    private MouseListener mouseListener = new MouseListener() {
+
+        public void mouseDoubleClick(MouseEvent e) {
+            if (selectedNode != null) {
+                ComponentRegistry.getDirector().showCapture(getShell(), selectedNode.viewNode);
+            }
+        }
+
+        public void mouseDown(MouseEvent e) {
+            System.out.println("CLICK");
+            boolean selectionChanged = false;
+            DrawableViewNode newSelection = null;
+            synchronized (LayoutViewer.this) {
+                if (tree != null) {
+                    float[] pt = {
+                            e.x, e.y
+                    };
+                    inverse.transform(pt);
+                    newSelection =
+                            updateSelection(tree, pt[0], pt[1], 0, 0, 0, 0, tree.viewNode.width,
+                                    tree.viewNode.height);
+                    if (selectedNode != newSelection) {
+                        selectionChanged = true;
+                    }
+                }
+            }
+            if (selectionChanged) {
+                model.setSelection(newSelection);
+            }
+        }
+
+        public void mouseUp(MouseEvent e) {
+            // pass
+        }
+    };
+
+    private DrawableViewNode updateSelection(DrawableViewNode node, float x, float y, int left, int top,
+            int clipX, int clipY, int clipWidth, int clipHeight) {
+        if (!node.treeDrawn) {
+            return null;
+        }
+        // Update the clip
+        int x1 = Math.max(left, clipX);
+        int x2 = Math.min(left + node.viewNode.width, clipX + clipWidth);
+        int y1 = Math.max(top, clipY);
+        int y2 = Math.min(top + node.viewNode.height, clipY + clipHeight);
+        clipX = x1;
+        clipY = y1;
+        clipWidth = x2 - x1;
+        clipHeight = y2 - y1;
+        if (x < clipX || x > clipX + clipWidth || y < clipY || y > clipY + clipHeight) {
+            return null;
+        }
+        final int N = node.children.size();
+        for (int i = N - 1; i >= 0; i--) {
+            DrawableViewNode child = node.children.get(i);
+            DrawableViewNode ret = updateSelection(child, x, y, left + child.viewNode.left - node.viewNode.scrollX,
+                    top + child.viewNode.top - node.viewNode.scrollY, clipX, clipY, clipWidth,
+                    clipHeight);
+            if(ret != null) {
+                return ret;
+            }
+        }
+        return node;
+    }
+
+    private PaintListener paintListener = new PaintListener() {
+        public void paintControl(PaintEvent e) {
+            synchronized (LayoutViewer.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                if (tree != null) {
+                    e.gc.setLineWidth((int) Math.ceil(0.2 / scale));
+                    e.gc.setTransform(transform);
+                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    Rectangle parentClipping = e.gc.getClipping();
+                    e.gc.setClipping(0, 0, tree.viewNode.width, tree.viewNode.height);
+                    paintRecursive(e.gc, tree, 0, 0, true);
+
+                    if (selectedNode != null) {
+                        e.gc.setClipping(parentClipping);
+
+                        // w00t, let's be nice and display the whole path in
+                        // light red and the selected node in dark red.
+                        ArrayList<Point> rightLeftDistances = new ArrayList<Point>();
+                        int left = 0;
+                        int top = 0;
+                        DrawableViewNode currentNode = selectedNode;
+                        while (currentNode != tree) {
+                            left += currentNode.viewNode.left;
+                            top += currentNode.viewNode.top;
+                            currentNode = currentNode.parent;
+                            left -= currentNode.viewNode.scrollX;
+                            top -= currentNode.viewNode.scrollY;
+                            rightLeftDistances.add(new Point(left, top));
+                        }
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED));
+                        currentNode = selectedNode.parent;
+                        final int N = rightLeftDistances.size();
+                        for (int i = 0; i < N; i++) {
+                            e.gc.drawRectangle((int) (left - rightLeftDistances.get(i).x),
+                                    (int) (top - rightLeftDistances.get(i).y),
+                                    currentNode.viewNode.width - (int) Math.ceil(0.2 / scale),
+                                    currentNode.viewNode.height - (int) Math.ceil(0.2 / scale));
+                            currentNode = currentNode.parent;
+                        }
+
+                        if (showExtras && selectedNode.viewNode.image != null) {
+                            e.gc.drawImage(selectedNode.viewNode.image, left, top);
+                            e.gc
+                                    .setForeground(Display.getDefault().getSystemColor(
+                                            SWT.COLOR_WHITE));
+                            paintRecursive(e.gc, selectedNode, left, top, true);
+
+                        }
+
+                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
+                        e.gc.setLineWidth((int) Math.ceil(2 / scale));
+                        e.gc.drawRectangle(left, top, selectedNode.viewNode.width
+                                - (int) Math.ceil(2 / scale) + 1, selectedNode.viewNode.height
+                                - (int) Math.ceil(2 / scale) + 1);
+                    }
+                }
+            }
+        }
+    };
+
+    private void paintRecursive(GC gc, DrawableViewNode node, int left, int top, boolean root) {
+        if (!node.treeDrawn) {
+            return;
+        }
+        // Don't shift the root
+        if (!root) {
+            left += node.viewNode.left;
+            top += node.viewNode.top;
+        }
+        Rectangle parentClipping = gc.getClipping();
+        int x1 = Math.max(parentClipping.x, left);
+        int x2 = Math.min(parentClipping.x + parentClipping.width, left + node.viewNode.width);
+        int y1 = Math.max(parentClipping.y, top);
+        int y2 = Math.min(parentClipping.y + parentClipping.height, top + node.viewNode.height);
+        gc.setClipping(x1, y1, x2 - x1, y2 - y1);
+        final int N = node.children.size();
+        for (int i = 0; i < N; i++) {
+            paintRecursive(gc, node.children.get(i), left - node.viewNode.scrollX, top
+                    - node.viewNode.scrollY, false);
+        }
+        gc.setClipping(parentClipping);
+        if (!node.viewNode.willNotDraw) {
+            gc.drawRectangle(left, top, node.viewNode.width - (int) Math.ceil(0.2 / scale),
+                    node.viewNode.height - (int) Math.ceil(0.2 / scale));
+        }
+
+    }
+
+    private void doRedraw() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    private void setTransform() {
+        if (tree != null) {
+            Rectangle bounds = getBounds();
+            int leftRightPadding = bounds.width <= 30 ? 0 : 5;
+            int topBottomPadding = bounds.height <= 30 ? 0 : 5;
+            scale =
+                    Math.min(1.0 * (bounds.width - leftRightPadding * 2) / tree.viewNode.width, 1.0
+                            * (bounds.height - topBottomPadding * 2) / tree.viewNode.height);
+            int scaledWidth = (int) Math.ceil(tree.viewNode.width * scale);
+            int scaledHeight = (int) Math.ceil(tree.viewNode.height * scale);
+
+            transform.identity();
+            inverse.identity();
+            transform.translate((bounds.width - scaledWidth) / 2.0f,
+                    (bounds.height - scaledHeight) / 2.0f);
+            inverse.translate((bounds.width - scaledWidth) / 2.0f,
+                    (bounds.height - scaledHeight) / 2.0f);
+            transform.scale((float) scale, (float) scale);
+            inverse.scale((float) scale, (float) scale);
+            if (bounds.width != 0 && bounds.height != 0) {
+                inverse.invert();
+            }
+        }
+    }
+
+    public void selectionChanged() {
+        synchronized (this) {
+            if (selectedNode != null) {
+                selectedNode.viewNode.dereferenceImage();
+            }
+            selectedNode = model.getSelection();
+            if (selectedNode != null) {
+                selectedNode.viewNode.referenceImage();
+            }
+        }
+        doRedraw();
+    }
+
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    if (selectedNode != null) {
+                        selectedNode.viewNode.dereferenceImage();
+                    }
+                    tree = model.getTree();
+                    selectedNode = model.getSelection();
+                    if (selectedNode != null) {
+                        selectedNode.viewNode.referenceImage();
+                    }
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void viewportChanged() {
+        // pass
+    }
+
+    public void zoomChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfect.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfect.java
new file mode 100644
index 0000000..1a2876b
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfect.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.ddmlib.RawImage;
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.ImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfect extends ScrolledComposite implements ImageChangeListener {
+    private Canvas canvas;
+
+    private PixelPerfectModel model;
+
+    private Image image;
+
+    private Color crosshairColor;
+
+    private Color marginColor;
+
+    private Color borderColor;
+
+    private Color paddingColor;
+
+    private int width;
+
+    private int height;
+
+    private Point crosshairLocation;
+
+    private ViewNode selectedNode;
+
+    private Image overlayImage;
+
+    private double overlayTransparency;
+
+    public PixelPerfect(Composite parent) {
+        super(parent, SWT.H_SCROLL | SWT.V_SCROLL);
+        canvas = new Canvas(this, SWT.NONE);
+        setContent(canvas);
+        setExpandHorizontal(true);
+        setExpandVertical(true);
+        model = ComponentRegistry.getPixelPerfectModel();
+        model.addImageChangeListener(this);
+
+        canvas.addPaintListener(paintListener);
+        canvas.addMouseListener(mouseListener);
+        canvas.addMouseMoveListener(mouseMoveListener);
+
+        addDisposeListener(disposeListener);
+
+        crosshairColor = new Color(Display.getDefault(), new RGB(0, 255, 255));
+        borderColor = new Color(Display.getDefault(), new RGB(255, 0, 0));
+        marginColor = new Color(Display.getDefault(), new RGB(0, 255, 0));
+        paddingColor = new Color(Display.getDefault(), new RGB(0, 0, 255));
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeImageChangeListener(PixelPerfect.this);
+            if (image != null) {
+                image.dispose();
+            }
+            crosshairColor.dispose();
+            borderColor.dispose();
+            paddingColor.dispose();
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return canvas.setFocus();
+    }
+
+    private MouseListener mouseListener = new MouseListener() {
+
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        public void mouseDown(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+        public void mouseUp(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+    };
+
+    private MouseMoveListener mouseMoveListener = new MouseMoveListener() {
+        public void mouseMove(MouseEvent e) {
+            if (e.stateMask != 0) {
+                handleMouseEvent(e);
+            }
+        }
+    };
+
+    private void handleMouseEvent(MouseEvent e) {
+        synchronized (PixelPerfect.this) {
+            if (image == null) {
+                return;
+            }
+            int leftOffset = canvas.getSize().x / 2 - width / 2;
+            int topOffset = canvas.getSize().y / 2 - height / 2;
+            e.x -= leftOffset;
+            e.y -= topOffset;
+            e.x = Math.max(e.x, 0);
+            e.x = Math.min(e.x, width - 1);
+            e.y = Math.max(e.y, 0);
+            e.y = Math.min(e.y, height - 1);
+        }
+        model.setCrosshairLocation(e.x, e.y);
+    }
+
+    private PaintListener paintListener = new PaintListener() {
+        public void paintControl(PaintEvent e) {
+            synchronized (PixelPerfect.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, canvas.getSize().x, canvas.getSize().y);
+                if (image != null) {
+                    // Let's be cool and put it in the center...
+                    int leftOffset = canvas.getSize().x / 2 - width / 2;
+                    int topOffset = canvas.getSize().y / 2 - height / 2;
+                    e.gc.drawImage(image, leftOffset, topOffset);
+                    if (overlayImage != null) {
+                        e.gc.setAlpha((int) (overlayTransparency * 255));
+                        int overlayTopOffset =
+                                canvas.getSize().y / 2 + height / 2
+                                        - overlayImage.getBounds().height;
+                        e.gc.drawImage(overlayImage, leftOffset, overlayTopOffset);
+                        e.gc.setAlpha(255);
+                    }
+
+                    if (selectedNode != null) {
+                        // There are a few quirks here. First of all, margins
+                        // are sometimes negative or positive numbers... Yet,
+                        // they are always treated as positive.
+                        // Secondly, if the screen is in landscape mode, the
+                        // coordinates are backwards.
+                        int leftShift = 0;
+                        int topShift = 0;
+                        int nodeLeft = selectedNode.left;
+                        int nodeTop = selectedNode.top;
+                        int nodeWidth = selectedNode.width;
+                        int nodeHeight = selectedNode.height;
+                        int nodeMarginLeft = selectedNode.marginLeft;
+                        int nodeMarginTop = selectedNode.marginTop;
+                        int nodeMarginRight = selectedNode.marginRight;
+                        int nodeMarginBottom = selectedNode.marginBottom;
+                        int nodePadLeft = selectedNode.paddingLeft;
+                        int nodePadTop = selectedNode.paddingTop;
+                        int nodePadRight = selectedNode.paddingRight;
+                        int nodePadBottom = selectedNode.paddingBottom;
+                        ViewNode cur = selectedNode;
+                        while (cur.parent != null) {
+                            leftShift += cur.parent.left - cur.parent.scrollX;
+                            topShift += cur.parent.top - cur.parent.scrollY;
+                            cur = cur.parent;
+                        }
+
+                        // Everything is sideways.
+                        if (cur.width > cur.height) {
+                            e.gc.setForeground(paddingColor);
+                            e.gc.drawRectangle(leftOffset + width - nodeTop - topShift - nodeHeight
+                                    + nodePadBottom,
+                                    topOffset + leftShift + nodeLeft + nodePadLeft, nodeHeight
+                                            - nodePadBottom - nodePadTop, nodeWidth - nodePadRight
+                                            - nodePadLeft);
+                            e.gc.setForeground(marginColor);
+                            e.gc.drawRectangle(leftOffset + width - nodeTop - topShift - nodeHeight
+                                    - nodeMarginBottom, topOffset + leftShift + nodeLeft
+                                    - nodeMarginLeft,
+                                    nodeHeight + nodeMarginBottom + nodeMarginTop, nodeWidth
+                                            + nodeMarginRight + nodeMarginLeft);
+                            e.gc.setForeground(borderColor);
+                            e.gc.drawRectangle(
+                                    leftOffset + width - nodeTop - topShift - nodeHeight, topOffset
+                                            + leftShift + nodeLeft, nodeHeight, nodeWidth);
+                        } else {
+                            e.gc.setForeground(paddingColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft + nodePadLeft,
+                                    topOffset + topShift + nodeTop + nodePadTop, nodeWidth
+                                            - nodePadRight - nodePadLeft, nodeHeight
+                                            - nodePadBottom - nodePadTop);
+                            e.gc.setForeground(marginColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft - nodeMarginLeft,
+                                    topOffset + topShift + nodeTop - nodeMarginTop, nodeWidth
+                                            + nodeMarginRight + nodeMarginLeft, nodeHeight
+                                            + nodeMarginBottom + nodeMarginTop);
+                            e.gc.setForeground(borderColor);
+                            e.gc.drawRectangle(leftOffset + leftShift + nodeLeft, topOffset
+                                    + topShift + nodeTop, nodeWidth, nodeHeight);
+                        }
+                    }
+                    if (crosshairLocation != null) {
+                        e.gc.setForeground(crosshairColor);
+                        e.gc.drawLine(leftOffset, topOffset + crosshairLocation.y, leftOffset
+                                + width - 1, topOffset + crosshairLocation.y);
+                        e.gc.drawLine(leftOffset + crosshairLocation.x, topOffset, leftOffset
+                                + crosshairLocation.x, topOffset + height - 1);
+                    }
+                }
+            }
+        }
+    };
+
+    private void doRedraw() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                canvas.redraw();
+            }
+        });
+    }
+
+    private void loadImage() {
+        image = model.getImage();
+        if (image != null) {
+            width = image.getBounds().width;
+            height = image.getBounds().height;
+        } else {
+            width = 0;
+            height = 0;
+        }
+        setMinSize(width, height);
+    }
+
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                    crosshairLocation = model.getCrosshairLocation();
+                    selectedNode = model.getSelected();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void imageChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void crosshairMoved() {
+        synchronized (this) {
+            crosshairLocation = model.getCrosshairLocation();
+        }
+        doRedraw();
+    }
+
+    public void selectionChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelected();
+        }
+        doRedraw();
+    }
+
+    public void focusChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                    selectedNode = model.getSelected();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void zoomChanged() {
+        // pass
+    }
+
+    public void overlayChanged() {
+        synchronized (this) {
+            overlayImage = model.getOverlayImage();
+            overlayTransparency = model.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+
+    public void overlayTransparencyChanged() {
+        synchronized (this) {
+            overlayTransparency = model.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java
new file mode 100644
index 0000000..84ce08f
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.ddmlib.RawImage;
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.ImageChangeListener;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class PixelPerfectLoupe extends Canvas implements ImageChangeListener {
+    private PixelPerfectModel model;
+
+    private Image image;
+
+    private Image grid;
+
+    private Color crosshairColor;
+
+    private int width;
+
+    private int height;
+
+    private Point crosshairLocation;
+
+    private int zoom;
+
+    private Transform transform;
+
+    private int canvasWidth;
+
+    private int canvasHeight;
+
+    private Image overlayImage;
+
+    private double overlayTransparency;
+
+    private boolean showOverlay = false;
+
+    public PixelPerfectLoupe(Composite parent) {
+        super(parent, SWT.NONE);
+        model = ComponentRegistry.getPixelPerfectModel();
+        model.addImageChangeListener(this);
+
+        addPaintListener(paintListener);
+        addMouseListener(mouseListener);
+        addMouseWheelListener(mouseWheelListener);
+        addDisposeListener(disposeListener);
+
+        crosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254));
+
+        transform = new Transform(Display.getDefault());
+    }
+
+    public void setShowOverlay(boolean value) {
+        synchronized (this) {
+            showOverlay = value;
+        }
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeImageChangeListener(PixelPerfectLoupe.this);
+            if (image != null) {
+                image.dispose();
+            }
+            crosshairColor.dispose();
+            transform.dispose();
+            if (grid != null) {
+                grid.dispose();
+            }
+        }
+    };
+
+    private MouseListener mouseListener = new MouseListener() {
+
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        public void mouseDown(MouseEvent e) {
+            handleMouseEvent(e);
+        }
+
+        public void mouseUp(MouseEvent e) {
+            //
+        }
+
+    };
+
+    private MouseWheelListener mouseWheelListener = new MouseWheelListener() {
+        public void mouseScrolled(MouseEvent e) {
+            int newZoom = -1;
+            synchronized (PixelPerfectLoupe.this) {
+                if (image != null && crosshairLocation != null) {
+                    if (e.count > 0) {
+                        newZoom = zoom + 1;
+                    } else {
+                        newZoom = zoom - 1;
+                    }
+                }
+            }
+            if (newZoom != -1) {
+                model.setZoom(newZoom);
+            }
+        }
+    };
+
+    private void handleMouseEvent(MouseEvent e) {
+        int newX = -1;
+        int newY = -1;
+        synchronized (PixelPerfectLoupe.this) {
+            if (image == null) {
+                return;
+            }
+            int zoomedX = -crosshairLocation.x * zoom - zoom / 2 + getBounds().width / 2;
+            int zoomedY = -crosshairLocation.y * zoom - zoom / 2 + getBounds().height / 2;
+            int x = (e.x - zoomedX) / zoom;
+            int y = (e.y - zoomedY) / zoom;
+            if (x >= 0 && x < width && y >= 0 && y < height) {
+                newX = x;
+                newY = y;
+            }
+        }
+        if (newX != -1) {
+            model.setCrosshairLocation(newX, newY);
+        }
+    }
+
+    private PaintListener paintListener = new PaintListener() {
+        public void paintControl(PaintEvent e) {
+            synchronized (PixelPerfectLoupe.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                e.gc.fillRectangle(0, 0, getSize().x, getSize().y);
+                if (image != null && crosshairLocation != null) {
+                    int zoomedX = -crosshairLocation.x * zoom - zoom / 2 + getBounds().width / 2;
+                    int zoomedY = -crosshairLocation.y * zoom - zoom / 2 + getBounds().height / 2;
+                    transform.translate(zoomedX, zoomedY);
+                    transform.scale(zoom, zoom);
+                    e.gc.setInterpolation(SWT.NONE);
+                    e.gc.setTransform(transform);
+                    e.gc.drawImage(image, 0, 0);
+                    if (showOverlay && overlayImage != null) {
+                        e.gc.setAlpha((int) (overlayTransparency * 255));
+                        e.gc.drawImage(overlayImage, 0, height - overlayImage.getBounds().height);
+                        e.gc.setAlpha(255);
+                    }
+
+                    transform.identity();
+                    e.gc.setTransform(transform);
+
+                    // If the size of the canvas has changed, we need to make
+                    // another grid.
+                    if (grid != null
+                            && (canvasWidth != getBounds().width || canvasHeight != getBounds().height)) {
+                        grid.dispose();
+                        grid = null;
+                    }
+                    canvasWidth = getBounds().width;
+                    canvasHeight = getBounds().height;
+                    if (grid == null) {
+                        // Make a transparent image;
+                        ImageData imageData =
+                                new ImageData(canvasWidth + zoom + 1, canvasHeight + zoom + 1, 1,
+                                        new PaletteData(new RGB[] {
+                                            new RGB(0, 0, 0)
+                                        }));
+                        imageData.transparentPixel = 0;
+
+                        // Draw the grid.
+                        grid = new Image(Display.getDefault(), imageData);
+                        GC gc = new GC(grid);
+                        gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                        for (int x = 0; x <= canvasWidth + zoom; x += zoom) {
+                            gc.drawLine(x, 0, x, canvasHeight + zoom);
+                        }
+                        for (int y = 0; y <= canvasHeight + zoom; y += zoom) {
+                            gc.drawLine(0, y, canvasWidth + zoom, y);
+                        }
+                        gc.dispose();
+                    }
+
+                    e.gc.setClipping(new Rectangle(zoomedX, zoomedY, width * zoom + 1, height
+                            * zoom + 1));
+                    e.gc.setAlpha(76);
+                    e.gc.drawImage(grid, (canvasWidth / 2 - zoom / 2) % zoom - zoom,
+                            (canvasHeight / 2 - zoom / 2) % zoom - zoom);
+                    e.gc.setAlpha(255);
+
+                    e.gc.setForeground(crosshairColor);
+                    e.gc.drawLine(0, canvasHeight / 2, canvasWidth - 1, canvasHeight / 2);
+                    e.gc.drawLine(canvasWidth / 2, 0, canvasWidth / 2, canvasHeight - 1);
+                }
+            }
+        }
+    };
+
+    private void doRedraw() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    private void loadImage() {
+        image = model.getImage();
+        if (image != null) {
+            width = image.getBounds().width;
+            height = image.getBounds().height;
+        } else {
+            width = 0;
+            height = 0;
+        }
+    }
+
+    public void imageLoaded() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                    crosshairLocation = model.getCrosshairLocation();
+                    zoom = model.getZoom();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void imageChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    loadImage();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void crosshairMoved() {
+        synchronized (this) {
+            crosshairLocation = model.getCrosshairLocation();
+        }
+        doRedraw();
+    }
+
+    public void selectionChanged() {
+        // pass
+    }
+
+    public void focusChanged() {
+        imageChanged();
+    }
+
+    public void zoomChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    if (grid != null) {
+                        // To notify that the zoom level has changed, we get rid
+                        // of the
+                        // grid.
+                        grid.dispose();
+                        grid = null;
+                        zoom = model.getZoom();
+                    }
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void overlayChanged() {
+        synchronized (this) {
+            overlayImage = model.getOverlayImage();
+            overlayTransparency = model.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+
+    public void overlayTransparencyChanged() {
+        synchronized (this) {
+            overlayTransparency = model.getOverlayTransparency();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java
new file mode 100644
index 0000000..7df4d9d
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel;
+import com.android.hierarchyviewerlib.models.PixelPerfectModel.ImageChangeListener;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+
+import java.util.List;
+
+public class PixelPerfectTree extends Composite implements ImageChangeListener, SelectionListener {
+
+    private TreeViewer treeViewer;
+
+    private Tree tree;
+
+    private PixelPerfectModel model;
+
+    private Image folderImage;
+
+    private Image fileImage;
+
+    private class ContentProvider implements ITreeContentProvider, ILabelProvider {
+        public Object[] getChildren(Object element) {
+            if (element instanceof ViewNode) {
+                List<ViewNode> children = ((ViewNode) element).children;
+                return children.toArray(new ViewNode[children.size()]);
+            }
+            return null;
+        }
+
+        public Object getParent(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).parent;
+            }
+            return null;
+        }
+
+        public boolean hasChildren(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).children.size() != 0;
+            }
+            return false;
+        }
+
+        public Object[] getElements(Object element) {
+            if (element instanceof PixelPerfectModel) {
+                ViewNode viewNode = ((PixelPerfectModel) element).getViewNode();
+                if (viewNode == null) {
+                    return new Object[0];
+                }
+                return new Object[] {
+                    viewNode
+                };
+            }
+            return new Object[0];
+        }
+
+        public void dispose() {
+            // pass
+        }
+
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        public Image getImage(Object element) {
+            if (element instanceof ViewNode) {
+                if (hasChildren(element)) {
+                    return folderImage;
+                }
+                return fileImage;
+            }
+            return null;
+        }
+
+        public String getText(Object element) {
+            if (element instanceof ViewNode) {
+                return ((ViewNode) element).name;
+            }
+            return null;
+        }
+
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public PixelPerfectTree(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        treeViewer = new TreeViewer(this, SWT.SINGLE);
+        treeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
+
+        tree = treeViewer.getTree();
+        tree.addSelectionListener(this);
+
+        loadResources();
+
+        addDisposeListener(disposeListener);
+
+        model = ComponentRegistry.getPixelPerfectModel();
+        ContentProvider contentProvider = new ContentProvider();
+        treeViewer.setContentProvider(contentProvider);
+        treeViewer.setLabelProvider(contentProvider);
+        treeViewer.setInput(model);
+        model.addImageChangeListener(this);
+
+    }
+
+    public void loadResources() {
+        ImageLoader loader = ImageLoader.getDdmUiLibLoader();
+        fileImage = loader.loadImage("file.png", Display.getDefault());
+
+        folderImage = loader.loadImage("folder.png", Display.getDefault());
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeImageChangeListener(PixelPerfectTree.this);
+            fileImage.dispose();
+            folderImage.dispose();
+        }
+    };
+
+    @Override
+    public boolean setFocus() {
+        return tree.setFocus();
+    }
+
+    public void imageLoaded() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                treeViewer.refresh();
+                treeViewer.expandAll();
+            }
+        });
+    }
+
+    public void imageChanged() {
+        // pass
+    }
+
+    public void crosshairMoved() {
+        // pass
+    }
+
+    public void selectionChanged() {
+        // pass
+    }
+
+    public void focusChanged() {
+        imageLoaded();
+    }
+
+    public void widgetDefaultSelected(SelectionEvent e) {
+        // pass
+    }
+
+    public void widgetSelected(SelectionEvent e) {
+        // To combat phantom selection...
+        if (((TreeSelection) treeViewer.getSelection()).isEmpty()) {
+            model.setSelected(null);
+        } else {
+            model.setSelected((ViewNode) e.item.getData());
+        }
+    }
+
+    public void zoomChanged() {
+        // pass
+    }
+
+    public void overlayChanged() {
+        // pass
+    }
+
+    public void overlayTransparencyChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/ProfileViewer.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/ProfileViewer.java
new file mode 100644
index 0000000..f83ba3d
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/ProfileViewer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.TreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.text.DecimalFormat;
+
+public class ProfileViewer extends Composite implements TreeChangeListener {
+    private TreeViewModel model;
+
+    private TreeViewer treeViewer;
+
+    private Tree tree;
+
+    private DrawableViewNode selectedNode;
+
+    private class ContentProvider implements ITreeContentProvider, ITableLabelProvider {
+
+        public Object[] getChildren(Object parentElement) {
+            synchronized (ProfileViewer.this) {
+                return new Object[0];
+            }
+        }
+
+        public Object getParent(Object element) {
+            synchronized (ProfileViewer.this) {
+                return new Object[0];
+            }
+        }
+
+        public boolean hasChildren(Object element) {
+            synchronized (ProfileViewer.this) {
+                return false;
+            }
+        }
+
+        public Object[] getElements(Object inputElement) {
+            synchronized (ProfileViewer.this) {
+                if (selectedNode != null && selectedNode.viewNode.measureTime != -1) {
+                    return new String[] {
+                            "measure", "layout", "draw"
+                    };
+                }
+                return new Object[0];
+            }
+        }
+
+        public void dispose() {
+            // pass
+        }
+
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        public Image getColumnImage(Object element, int column) {
+            return null;
+        }
+
+        public String getColumnText(Object element, int column) {
+            synchronized (ProfileViewer.this) {
+                if (selectedNode != null) {
+                    if (column == 0) {
+                        return (String) element;
+                    } else if (column == 1) {
+                        DecimalFormat formatter = new DecimalFormat("0.000");
+                        if(((String)element).equals("measure")) {
+                            if (selectedNode.viewNode.measureTime == -1) {
+                                return "unknown";
+                            }
+                            return formatter.format(selectedNode.viewNode.measureTime);
+                        } else if (((String) element).equals("layout")) {
+                            if (selectedNode.viewNode.layoutTime == -1) {
+                                return "unknown";
+                            }
+                            return formatter.format(selectedNode.viewNode.layoutTime);
+                        } else {
+                            if (selectedNode.viewNode.drawTime == -1) {
+                                return "unknown";
+                            }
+                            return formatter.format(selectedNode.viewNode.drawTime);
+                        }
+                    }
+                }
+                return "";
+            }
+        }
+
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public ProfileViewer(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        treeViewer = new TreeViewer(this, SWT.NONE);
+
+        tree = treeViewer.getTree();
+        tree.setLinesVisible(true);
+        tree.setHeaderVisible(true);
+
+        TreeColumn operationColumn = new TreeColumn(tree, SWT.NONE);
+        operationColumn.setText("Operation");
+        TreeColumn durationColumn = new TreeColumn(tree, SWT.NONE);
+        durationColumn.setText("Duration (ms)");
+
+        model = ComponentRegistry.getTreeViewModel();
+        ContentProvider contentProvider = new ContentProvider();
+        treeViewer.setContentProvider(contentProvider);
+        treeViewer.setLabelProvider(contentProvider);
+        treeViewer.setInput(model);
+        model.addTreeChangeListener(this);
+
+        new TreeColumnResizer(this, operationColumn, durationColumn);
+    }
+
+    public void selectionChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelection();
+        }
+        doRefresh();
+    }
+
+    public void treeChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelection();
+        }
+        doRefresh();
+    }
+
+    public void viewportChanged() {
+        // pass
+    }
+
+    public void zoomChanged() {
+        // pass
+    }
+
+    private void doRefresh() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                treeViewer.refresh();
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PropertyViewer.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PropertyViewer.java
new file mode 100644
index 0000000..d262d16
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/PropertyViewer.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.device.ViewNode.Property;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.TreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer;
+
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.util.ArrayList;
+
+public class PropertyViewer extends Composite implements TreeChangeListener {
+    private TreeViewModel model;
+
+    private TreeViewer treeViewer;
+
+    private Tree tree;
+
+    private DrawableViewNode selectedNode;
+
+    private class ContentProvider implements ITreeContentProvider, ITableLabelProvider {
+
+        public Object[] getChildren(Object parentElement) {
+            synchronized (PropertyViewer.this) {
+                if (selectedNode != null && parentElement instanceof String) {
+                    String category = (String) parentElement;
+                    ArrayList<Property> returnValue = new ArrayList<Property>();
+                    for (Property property : selectedNode.viewNode.properties) {
+                        if (category.equals(ViewNode.MISCELLANIOUS)) {
+                            if (property.name.indexOf(':') == -1) {
+                                returnValue.add(property);
+                            }
+                        } else {
+                            if (property.name.startsWith(((String) parentElement) + ":")) {
+                                returnValue.add(property);
+                            }
+                        }
+                    }
+                    return returnValue.toArray(new Property[returnValue.size()]);
+                }
+                return new Object[0];
+            }
+        }
+
+        public Object getParent(Object element) {
+            synchronized (PropertyViewer.this) {
+                if (selectedNode != null && element instanceof Property) {
+                    if (selectedNode.viewNode.categories.size() == 0) {
+                        return null;
+                    }
+                    String name = ((Property) element).name;
+                    int index = name.indexOf(':');
+                    if (index == -1) {
+                        return ViewNode.MISCELLANIOUS;
+                    }
+                    return name.substring(0, index);
+                }
+                return null;
+            }
+        }
+
+        public boolean hasChildren(Object element) {
+            synchronized (PropertyViewer.this) {
+                if (selectedNode != null && element instanceof String) {
+                    String category = (String) element;
+                    for (String name : selectedNode.viewNode.namedProperties.keySet()) {
+                        if (category.equals(ViewNode.MISCELLANIOUS)) {
+                            if (name.indexOf(':') == -1) {
+                                return true;
+                            }
+                        } else {
+                            if (name.startsWith(((String) element) + ":")) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                return false;
+            }
+        }
+
+        public Object[] getElements(Object inputElement) {
+            synchronized (PropertyViewer.this) {
+                if (selectedNode != null && inputElement instanceof TreeViewModel) {
+                    if (selectedNode.viewNode.categories.size() == 0) {
+                        return selectedNode.viewNode.properties
+                                .toArray(new Property[selectedNode.viewNode.properties.size()]);
+                    } else {
+                        return selectedNode.viewNode.categories
+                                .toArray(new String[selectedNode.viewNode.categories.size()]);
+                    }
+                }
+                return new Object[0];
+            }
+        }
+
+        public void dispose() {
+            // pass
+        }
+
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            // pass
+        }
+
+        public Image getColumnImage(Object element, int column) {
+            return null;
+        }
+
+        public String getColumnText(Object element, int column) {
+            synchronized (PropertyViewer.this) {
+                if (selectedNode != null) {
+                    if (element instanceof String && column == 0) {
+                        String category = (String) element;
+                        return Character.toUpperCase(category.charAt(0)) + category.substring(1);
+                    } else if (element instanceof Property) {
+                        if (column == 0) {
+                            String returnValue = ((Property) element).name;
+                            int index = returnValue.indexOf(':');
+                            if (index != -1) {
+                                return returnValue.substring(index + 1);
+                            }
+                            return returnValue;
+                        } else if (column == 1) {
+                            return ((Property) element).value;
+                        }
+                    }
+                }
+                return "";
+            }
+        }
+
+        public void addListener(ILabelProviderListener listener) {
+            // pass
+        }
+
+        public boolean isLabelProperty(Object element, String property) {
+            // pass
+            return false;
+        }
+
+        public void removeListener(ILabelProviderListener listener) {
+            // pass
+        }
+    }
+
+    public PropertyViewer(Composite parent) {
+        super(parent, SWT.NONE);
+        setLayout(new FillLayout());
+        treeViewer = new TreeViewer(this, SWT.NONE);
+
+        tree = treeViewer.getTree();
+        tree.setLinesVisible(true);
+        tree.setHeaderVisible(true);
+
+        TreeColumn propertyColumn = new TreeColumn(tree, SWT.NONE);
+        propertyColumn.setText("Property");
+        TreeColumn valueColumn = new TreeColumn(tree, SWT.NONE);
+        valueColumn.setText("Value");
+
+        model = ComponentRegistry.getTreeViewModel();
+        ContentProvider contentProvider = new ContentProvider();
+        treeViewer.setContentProvider(contentProvider);
+        treeViewer.setLabelProvider(contentProvider);
+        treeViewer.setInput(model);
+        model.addTreeChangeListener(this);
+
+        new TreeColumnResizer(this, propertyColumn, valueColumn);
+    }
+
+
+    public void selectionChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelection();
+        }
+        doRefresh();
+    }
+
+    public void treeChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelection();
+        }
+        doRefresh();
+    }
+
+    public void viewportChanged() {
+        // pass
+    }
+
+    public void zoomChanged() {
+        // pass
+    }
+
+    private void doRefresh() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                treeViewer.refresh();
+            }
+        });
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeView.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeView.java
new file mode 100644
index 0000000..abf5690
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeView.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.device.ViewNode.ProfileRating;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.TreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Path;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+public class TreeView extends Canvas implements TreeChangeListener {
+
+    private TreeViewModel model;
+
+    private DrawableViewNode tree;
+
+    private DrawableViewNode selectedNode;
+
+    private Rectangle viewport;
+
+    private Transform transform;
+
+    private Transform inverse;
+
+    private double zoom;
+
+    private Point lastPoint;
+
+    private DrawableViewNode draggedNode;
+
+    public static final int LINE_PADDING = 10;
+
+    public static final float BEZIER_FRACTION = 0.35f;
+
+    private Image redImage;
+
+    private Image yellowImage;
+
+    private Image greenImage;
+
+    public TreeView(Composite parent) {
+        super(parent, SWT.NONE);
+
+        model = ComponentRegistry.getTreeViewModel();
+        model.addTreeChangeListener(this);
+
+        addPaintListener(paintListener);
+        addMouseListener(mouseListener);
+        addMouseMoveListener(mouseMoveListener);
+        addMouseWheelListener(mouseWheelListener);
+        addListener(SWT.Resize, resizeListener);
+        addDisposeListener(disposeListener);
+        addKeyListener(keyListener);
+
+        transform = new Transform(Display.getDefault());
+        inverse = new Transform(Display.getDefault());
+
+        ImageLoader loader = ImageLoader.getLoader(this.getClass());
+        redImage = loader.loadImage("red.png", Display.getDefault());
+        yellowImage = loader.loadImage("yellow.png", Display.getDefault());
+        greenImage = loader.loadImage("green.png", Display.getDefault());
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeTreeChangeListener(TreeView.this);
+            transform.dispose();
+            inverse.dispose();
+        }
+    };
+
+    private Listener resizeListener = new Listener() {
+        public void handleEvent(Event e) {
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null) {
+
+                    // I don't know what the best behaviour is... This seems
+                    // like a good idea.
+                    Point viewCenter =
+                            new Point(viewport.x + viewport.width / 2, viewport.y + viewport.height
+                                    / 2);
+                    viewport.width = getBounds().width / zoom;
+                    viewport.height = getBounds().height / zoom;
+                    viewport.x = viewCenter.x - viewport.width / 2;
+                    viewport.y = viewCenter.y - viewport.height / 2;
+                }
+            }
+            if (viewport != null) {
+                model.setViewport(viewport);
+            }
+        }
+    };
+
+    private KeyListener keyListener = new KeyListener() {
+
+        public void keyPressed(KeyEvent e) {
+            boolean selectionChanged = false;
+            DrawableViewNode clickedNode = null;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null && selectedNode != null) {
+                    switch (e.keyCode) {
+                        case SWT.ARROW_LEFT:
+                            if(selectedNode.parent != null) {
+                                selectedNode = selectedNode.parent;
+                                selectionChanged = true;
+                            }
+                            break;
+                        case SWT.ARROW_UP:
+                            int levelsOut = 0;
+                            DrawableViewNode currentNode = selectedNode;
+                            while (currentNode.parent != null && currentNode.viewNode.index == 0) {
+                                levelsOut++;
+                                currentNode = currentNode.parent;
+                            }
+                            if (currentNode.parent != null) {
+                                selectionChanged = true;
+                                currentNode =
+                                        currentNode.parent.children
+                                                .get(currentNode.viewNode.index - 1);
+                                while (currentNode.children.size() != 0) {
+                                    currentNode =
+                                            currentNode.children
+                                                    .get(currentNode.children.size() - 1);
+                                    levelsOut--;
+                                }
+                            }
+                            if (selectionChanged) {
+                                selectedNode = currentNode;
+                            }
+                            break;
+                        case SWT.ARROW_DOWN:
+                            levelsOut = 0;
+                            currentNode = selectedNode;
+                            while (currentNode.parent != null
+                                    && currentNode.viewNode.index + 1 == currentNode.parent.children
+                                    .size()) {
+                                levelsOut++;
+                                currentNode = currentNode.parent;
+                            }
+                            if (currentNode.parent != null) {
+                                selectionChanged = true;
+                                currentNode =
+                                        currentNode.parent.children
+                                                .get(currentNode.viewNode.index + 1);
+                                while (currentNode.children.size() != 0) {
+                                    currentNode = currentNode.children.get(0);
+                                    levelsOut--;
+                                }
+                            }
+                            if (selectionChanged) {
+                                selectedNode = currentNode;
+                            }
+                            break;
+                        case SWT.ARROW_RIGHT:
+                            DrawableViewNode rightNode = null;
+                            double mostOverlap = 0;
+                            final int N = selectedNode.children.size();
+                            for(int i = 0; i<N; i++) {
+                                DrawableViewNode child = selectedNode.children.get(i);
+                                DrawableViewNode topMostChild = child;
+                                while (topMostChild.children.size() != 0) {
+                                    topMostChild = topMostChild.children.get(0);
+                                }
+                                double overlap =
+                                        Math.min(DrawableViewNode.NODE_HEIGHT, Math.min(
+                                                selectedNode.top + DrawableViewNode.NODE_HEIGHT
+                                                        - topMostChild.top, topMostChild.top
+                                                        + child.treeHeight - selectedNode.top));
+                                if (overlap > mostOverlap) {
+                                    mostOverlap = overlap;
+                                    rightNode = child;
+                                }
+                            }
+                            if (rightNode != null) {
+                                selectedNode = rightNode;
+                                selectionChanged = true;
+                            }
+                            break;
+                        case SWT.CR:
+                            clickedNode = selectedNode;
+                            break;
+                    }
+                }
+            }
+            if (selectionChanged) {
+                model.setSelection(selectedNode);
+            }
+            if (clickedNode != null) {
+                ComponentRegistry.getDirector().showCapture(getShell(), clickedNode.viewNode);
+            }
+        }
+
+        public void keyReleased(KeyEvent e) {
+        }
+    };
+
+    private MouseListener mouseListener = new MouseListener() {
+
+        public void mouseDoubleClick(MouseEvent e) {
+            DrawableViewNode clickedNode = null;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null) {
+                    Point pt = transformPoint(e.x, e.y);
+                    clickedNode = tree.getSelected(pt.x, pt.y);
+                }
+            }
+            if (clickedNode != null) {
+                ComponentRegistry.getDirector().showCapture(getShell(), clickedNode.viewNode);
+            }
+        }
+
+        public void mouseDown(MouseEvent e) {
+            boolean selectionChanged = false;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null) {
+                    Point pt = transformPoint(e.x, e.y);
+                    draggedNode = tree.getSelected(pt.x, pt.y);
+                    if (draggedNode != null && draggedNode != selectedNode) {
+                        selectedNode = draggedNode;
+                        selectionChanged = true;
+                    }
+                    if (draggedNode == tree) {
+                        draggedNode = null;
+                    }
+                    if (draggedNode != null) {
+                        lastPoint = pt;
+                    } else {
+                        lastPoint = new Point(e.x, e.y);
+                    }
+                }
+            }
+            if (selectionChanged) {
+                model.setSelection(selectedNode);
+            }
+        }
+
+        public void mouseUp(MouseEvent e) {
+            boolean redraw = false;
+            boolean viewportChanged = false;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null && lastPoint != null) {
+                    if (draggedNode == null) {
+                        handleMouseDrag(new Point(e.x, e.y));
+                        viewportChanged = true;
+                    } else {
+                        handleMouseDrag(transformPoint(e.x, e.y));
+                    }
+                    lastPoint = null;
+                    draggedNode = null;
+                    redraw = true;
+                }
+            }
+            if (viewportChanged) {
+                model.setViewport(viewport);
+            } else if (redraw) {
+                model.removeTreeChangeListener(TreeView.this);
+                model.notifyViewportChanged();
+                model.addTreeChangeListener(TreeView.this);
+                doRedraw();
+            }
+        }
+
+    };
+
+    private MouseMoveListener mouseMoveListener = new MouseMoveListener() {
+        public void mouseMove(MouseEvent e) {
+            boolean redraw = false;
+            boolean viewportChanged = false;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null && lastPoint != null) {
+                    if (draggedNode == null) {
+                        handleMouseDrag(new Point(e.x, e.y));
+                        viewportChanged = true;
+                    } else {
+                        handleMouseDrag(transformPoint(e.x, e.y));
+                    }
+                    redraw = true;
+                }
+            }
+            if (viewportChanged) {
+                model.setViewport(viewport);
+            } else if (redraw) {
+                model.removeTreeChangeListener(TreeView.this);
+                model.notifyViewportChanged();
+                model.addTreeChangeListener(TreeView.this);
+                doRedraw();
+            }
+        }
+    };
+
+    private void handleMouseDrag(Point pt) {
+        if (draggedNode != null) {
+            draggedNode.move(lastPoint.y - pt.y);
+            lastPoint = pt;
+            return;
+        }
+        double xDif = (lastPoint.x - pt.x) / zoom;
+        double yDif = (lastPoint.y - pt.y) / zoom;
+
+        if (viewport.width > tree.bounds.width) {
+            if (xDif < 0 && viewport.x + viewport.width > tree.bounds.x + tree.bounds.width) {
+                viewport.x =
+                        Math.max(viewport.x + xDif, tree.bounds.x + tree.bounds.width
+                                - viewport.width);
+            } else if (xDif > 0 && viewport.x < tree.bounds.x) {
+                viewport.x = Math.min(viewport.x + xDif, tree.bounds.x);
+            }
+        } else {
+            if (xDif < 0 && viewport.x > tree.bounds.x) {
+                viewport.x = Math.max(viewport.x + xDif, tree.bounds.x);
+            } else if (xDif > 0 && viewport.x + viewport.width < tree.bounds.x + tree.bounds.width) {
+                viewport.x =
+                        Math.min(viewport.x + xDif, tree.bounds.x + tree.bounds.width
+                                - viewport.width);
+            }
+        }
+        if (viewport.height > tree.bounds.height) {
+            if (yDif < 0 && viewport.y + viewport.height > tree.bounds.y + tree.bounds.height) {
+                viewport.y =
+                        Math.max(viewport.y + yDif, tree.bounds.y + tree.bounds.height
+                                - viewport.height);
+            } else if (yDif > 0 && viewport.y < tree.bounds.y) {
+                viewport.y = Math.min(viewport.y + yDif, tree.bounds.y);
+            }
+        } else {
+            if (yDif < 0 && viewport.y > tree.bounds.y) {
+                viewport.y = Math.max(viewport.y + yDif, tree.bounds.y);
+            } else if (yDif > 0
+                    && viewport.y + viewport.height < tree.bounds.y + tree.bounds.height) {
+                viewport.y =
+                        Math.min(viewport.y + yDif, tree.bounds.y + tree.bounds.height
+                                - viewport.height);
+            }
+        }
+        lastPoint = pt;
+    }
+
+    private Point transformPoint(double x, double y) {
+        float[] pt = {
+                (float) x, (float) y
+        };
+        inverse.transform(pt);
+        return new Point(pt[0], pt[1]);
+    }
+
+    private MouseWheelListener mouseWheelListener = new MouseWheelListener() {
+        public void mouseScrolled(MouseEvent e) {
+            Point zoomPoint = null;
+            synchronized (TreeView.this) {
+                if (tree != null && viewport != null) {
+                    zoom += Math.ceil(e.count / 3.0) * 0.1;
+                    zoomPoint = transformPoint(e.x, e.y);
+                }
+            }
+            if (zoomPoint != null) {
+                model.zoomOnPoint(zoom, zoomPoint);
+            }
+        }
+    };
+
+    private PaintListener paintListener = new PaintListener() {
+        public void paintControl(PaintEvent e) {
+            synchronized (TreeView.this) {
+                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                if (tree != null && viewport != null) {
+                    e.gc.setTransform(transform);
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
+                    Path connectionPath = new Path(Display.getDefault());
+                    paintRecursive(e.gc, tree, connectionPath);
+                    e.gc.drawPath(connectionPath);
+                    connectionPath.dispose();
+                }
+            }
+        }
+    };
+
+    private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) {
+        if (selectedNode == node) {
+            gc.fillRectangle(node.left, (int) Math.round(node.top), DrawableViewNode.NODE_WIDTH,
+                    DrawableViewNode.NODE_HEIGHT);
+        } else {
+            gc.drawRectangle(node.left, (int) Math.round(node.top), DrawableViewNode.NODE_WIDTH,
+                    DrawableViewNode.NODE_HEIGHT);
+        }
+
+        int fontHeight = gc.getFontMetrics().getHeight();
+
+        // Draw the text...
+        int contentWidth =
+                DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
+        String name = node.viewNode.name;
+        int dotIndex = name.lastIndexOf('.');
+        if (dotIndex != -1) {
+            name = name.substring(dotIndex + 1);
+        }
+        double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
+        double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING;
+        drawTextInArea(gc, name, x, y, contentWidth, fontHeight);
+
+        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
+
+        gc.drawText("@" + node.viewNode.hashCode, (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+
+        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
+        if (!node.viewNode.id.equals("NO_ID")) {
+            drawTextInArea(gc, node.viewNode.id, x, y, contentWidth, fontHeight);
+        }
+
+        if (node.viewNode.measureRating != ProfileRating.NONE) {
+            y =
+                    node.top + DrawableViewNode.NODE_HEIGHT
+                            - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING
+                            - redImage.getBounds().height;
+            x +=
+                    (contentWidth - (redImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2;
+            switch (node.viewNode.measureRating) {
+                case GREEN:
+                    gc.drawImage(greenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(yellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(redImage, (int) x, (int) y);
+                    break;
+            }
+
+            x += redImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
+            switch (node.viewNode.layoutRating) {
+                case GREEN:
+                    gc.drawImage(greenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(yellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(redImage, (int) x, (int) y);
+                    break;
+            }
+
+            x += redImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
+            switch (node.viewNode.drawRating) {
+                case GREEN:
+                    gc.drawImage(greenImage, (int) x, (int) y);
+                    break;
+                case YELLOW:
+                    gc.drawImage(yellowImage, (int) x, (int) y);
+                    break;
+                case RED:
+                    gc.drawImage(redImage, (int) x, (int) y);
+                    break;
+            }
+        }
+
+
+        org.eclipse.swt.graphics.Point indexExtent =
+                gc.stringExtent(Integer.toString(node.viewNode.index));
+        x = node.left+DrawableViewNode.NODE_WIDTH-DrawableViewNode.INDEX_PADDING-indexExtent.x;
+        y = node.top+DrawableViewNode.NODE_HEIGHT-DrawableViewNode.INDEX_PADDING-indexExtent.y;
+        gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+
+
+
+        int N = node.children.size();
+        if (N == 0) {
+            return;
+        }
+        float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N;
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = node.children.get(i);
+            paintRecursive(gc, child, connectionPath);
+            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
+            float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2;
+            float x2 = child.left;
+            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
+            float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy1 = y1;
+            float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy2 = y2;
+            connectionPath.moveTo(x1, y1);
+            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
+        }
+    }
+
+    private void drawTextInArea(GC gc, String text, double x, double y, double width, double height) {
+        org.eclipse.swt.graphics.Point extent = gc.stringExtent(text);
+
+        if (extent.x > width) {
+            // Oh no... we need to scale it.
+            double scale = width / extent.x;
+            float[] transformElements = new float[6];
+            transform.getElements(transformElements);
+            transform.scale((float) scale, (float) scale);
+            gc.setTransform(transform);
+
+            x/=scale;
+            y/=scale;
+            y += (extent.y / scale - extent.y) / 2;
+
+            gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+
+            transform.setElements(transformElements[0], transformElements[1], transformElements[2],
+                    transformElements[3], transformElements[4], transformElements[5]);
+            gc.setTransform(transform);
+        } else {
+            gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT);
+        }
+
+    }
+
+    private void doRedraw() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    tree = model.getTree();
+                    selectedNode = model.getSelection();
+                    if (tree == null) {
+                        viewport = null;
+                    } else {
+                        viewport =
+                                new Rectangle((tree.bounds.width - getBounds().width) / 2,
+                                        (tree.bounds.height - getBounds().height) / 2,
+                                        getBounds().width, getBounds().height);
+                    }
+                }
+            }
+        });
+        if (viewport != null) {
+            model.setViewport(viewport);
+        }
+    }
+
+    private void setTransform() {
+        if (viewport != null && tree != null) {
+            // Set the transform.
+            transform.identity();
+            inverse.identity();
+
+            transform.scale((float) zoom, (float) zoom);
+            inverse.scale((float) zoom, (float) zoom);
+            transform.translate((float) -viewport.x, (float) -viewport.y);
+            inverse.translate((float) -viewport.x, (float) -viewport.y);
+            inverse.invert();
+        }
+    }
+
+    public void viewportChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    viewport = model.getViewport();
+                    zoom = model.getZoom();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void zoomChanged() {
+        viewportChanged();
+    }
+
+    public void selectionChanged() {
+        synchronized (this) {
+            selectedNode = model.getSelection();
+        }
+        doRedraw();
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeViewOverview.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeViewOverview.java
new file mode 100644
index 0000000..83a2b0d
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/TreeViewOverview.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui;
+
+import com.android.hierarchyviewerlib.ComponentRegistry;
+import com.android.hierarchyviewerlib.models.TreeViewModel;
+import com.android.hierarchyviewerlib.models.TreeViewModel.TreeChangeListener;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
+import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Path;
+import org.eclipse.swt.graphics.Transform;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+public class TreeViewOverview extends Canvas implements TreeChangeListener {
+
+    private TreeViewModel model;
+
+    private DrawableViewNode tree;
+
+    private Rectangle viewport;
+
+    private Transform transform;
+
+    private Transform inverse;
+
+    private Rectangle bounds = new Rectangle();
+
+    private double scale;
+
+    private boolean dragging = false;
+
+    public TreeViewOverview(Composite parent) {
+        super(parent, SWT.NONE);
+
+        model = ComponentRegistry.getTreeViewModel();
+        model.addTreeChangeListener(this);
+
+        addPaintListener(paintListener);
+        addMouseListener(mouseListener);
+        addMouseMoveListener(mouseMoveListener);
+        addListener(SWT.Resize, resizeListener);
+        addDisposeListener(disposeListener);
+
+        transform = new Transform(Display.getDefault());
+        inverse = new Transform(Display.getDefault());
+    }
+
+    private DisposeListener disposeListener = new DisposeListener() {
+        public void widgetDisposed(DisposeEvent e) {
+            model.removeTreeChangeListener(TreeViewOverview.this);
+            transform.dispose();
+            inverse.dispose();
+        }
+    };
+
+    private MouseListener mouseListener = new MouseListener() {
+
+        public void mouseDoubleClick(MouseEvent e) {
+            // pass
+        }
+
+        public void mouseDown(MouseEvent e) {
+            boolean redraw = false;
+            synchronized (TreeViewOverview.this) {
+                if (tree != null && viewport != null) {
+                    dragging = true;
+                    redraw = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+                }
+            }
+            if (redraw) {
+                model.removeTreeChangeListener(TreeViewOverview.this);
+                model.setViewport(viewport);
+                model.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+
+        public void mouseUp(MouseEvent e) {
+            boolean redraw = false;
+            synchronized (TreeViewOverview.this) {
+                if (tree != null && viewport != null) {
+                    dragging = false;
+                    redraw = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+                    setBounds();
+                    setTransform();
+                }
+            }
+            if (redraw) {
+                model.removeTreeChangeListener(TreeViewOverview.this);
+                model.setViewport(viewport);
+                model.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+
+    };
+
+    private MouseMoveListener mouseMoveListener = new MouseMoveListener() {
+        public void mouseMove(MouseEvent e) {
+            boolean moved = false;
+            synchronized (TreeViewOverview.this) {
+                if (dragging) {
+                    moved = true;
+                    handleMouseEvent(transformPoint(e.x, e.y));
+                }
+            }
+            if (moved) {
+                model.removeTreeChangeListener(TreeViewOverview.this);
+                model.setViewport(viewport);
+                model.addTreeChangeListener(TreeViewOverview.this);
+                doRedraw();
+            }
+        }
+    };
+
+    private void handleMouseEvent(Point pt) {
+        viewport.x = pt.x - viewport.width / 2;
+        viewport.y = pt.y - viewport.height / 2;
+        if (viewport.x < bounds.x) {
+            viewport.x = bounds.x;
+        }
+        if (viewport.y < bounds.y) {
+            viewport.y = bounds.y;
+        }
+        if (viewport.x + viewport.width > bounds.x + bounds.width) {
+            viewport.x = bounds.x + bounds.width - viewport.width;
+        }
+        if (viewport.y + viewport.height > bounds.y + bounds.height) {
+            viewport.y = bounds.y + bounds.height - viewport.height;
+        }
+    }
+
+    private Point transformPoint(double x, double y) {
+        float[] pt = {
+                (float) x, (float) y
+        };
+        inverse.transform(pt);
+        return new Point(pt[0], pt[1]);
+    }
+
+    private Listener resizeListener = new Listener() {
+        public void handleEvent(Event arg0) {
+            synchronized (TreeViewOverview.this) {
+                setTransform();
+            }
+            doRedraw();
+        }
+    };
+
+    private PaintListener paintListener = new PaintListener() {
+        public void paintControl(PaintEvent e) {
+            synchronized (TreeViewOverview.this) {
+                if (tree != null && viewport != null) {
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
+                    e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
+                    e.gc.setTransform(transform);
+                    Path connectionPath = new Path(Display.getDefault());
+                    paintRecursive(e.gc, tree, connectionPath);
+                    e.gc.drawPath(connectionPath);
+                    connectionPath.dispose();
+
+                    e.gc.setAlpha(80);
+                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY));
+                    e.gc.fillRectangle((int) viewport.x, (int) viewport.y, (int) Math
+                            .ceil(viewport.width), (int) Math.ceil(viewport.height));
+
+                    e.gc.setAlpha(255);
+                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
+                    e.gc.setLineWidth((int) Math.ceil(2 / scale));
+                    e.gc.drawRectangle((int) viewport.x, (int) viewport.y, (int) Math
+                            .ceil(viewport.width), (int) Math.ceil(viewport.height));
+                }
+            }
+        }
+    };
+
+    private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) {
+        gc.drawRectangle(node.left, (int) Math.round(node.top), DrawableViewNode.NODE_WIDTH,
+                DrawableViewNode.NODE_HEIGHT);
+        int N = node.children.size();
+        if (N == 0) {
+            return;
+        }
+        float childSpacing =
+                (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N;
+        for (int i = 0; i < N; i++) {
+            DrawableViewNode child = node.children.get(i);
+            paintRecursive(gc, child, connectionPath);
+            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
+            float y1 =
+                    (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2;
+            float x2 = child.left;
+            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
+            float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy1 = y1;
+            float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
+            float cy2 = y2;
+            connectionPath.moveTo(x1, y1);
+            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
+        }
+    }
+
+    private void doRedraw() {
+        Display.getDefault().asyncExec(new Runnable() {
+            public void run() {
+                redraw();
+            }
+        });
+    }
+
+    public void treeChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    tree = model.getTree();
+                    setBounds();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    private void setBounds() {
+        if (viewport != null && tree != null) {
+            bounds.x = Math.min(viewport.x, tree.bounds.x);
+            bounds.y = Math.min(viewport.y, tree.bounds.y);
+            bounds.width =
+                    Math.max(viewport.x + viewport.width, tree.bounds.x + tree.bounds.width)
+                            - bounds.x;
+            bounds.height =
+                    Math.max(viewport.y + viewport.height, tree.bounds.y + tree.bounds.height)
+                            - bounds.y;
+        }
+    }
+
+    private void setTransform() {
+        if (viewport != null && tree != null) {
+
+            transform.identity();
+            inverse.identity();
+            final Point size = new Point();
+            size.x = getBounds().width;
+            size.y = getBounds().height;
+            if (bounds.width == 0 || bounds.height == 0 || size.x == 0 || size.y == 0) {
+                scale = 1;
+            } else {
+                scale = Math.min(size.x / bounds.width, size.y / bounds.height);
+            }
+            transform.scale((float) scale, (float) scale);
+            inverse.scale((float) scale, (float) scale);
+            transform.translate((float) -bounds.x, (float) -bounds.y);
+            inverse.translate((float) -bounds.x, (float) -bounds.y);
+            if (size.x / bounds.width < size.y / bounds.height) {
+                transform.translate(0, (float) (size.y / scale - bounds.height) / 2);
+                inverse.translate(0, (float) (size.y / scale - bounds.height) / 2);
+            } else {
+                transform.translate((float) (size.x / scale - bounds.width) / 2, 0);
+                inverse.translate((float) (size.x / scale - bounds.width) / 2, 0);
+            }
+            inverse.invert();
+        }
+    }
+
+    public void viewportChanged() {
+        Display.getDefault().syncExec(new Runnable() {
+            public void run() {
+                synchronized (this) {
+                    viewport = model.getViewport();
+                    setBounds();
+                    setTransform();
+                }
+            }
+        });
+        doRedraw();
+    }
+
+    public void zoomChanged() {
+        viewportChanged();
+    }
+
+    public void selectionChanged() {
+        // pass
+    }
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/scene/DrawableViewNode.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java
similarity index 90%
rename from hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/scene/DrawableViewNode.java
rename to hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java
index aff3d6d..fccc3ba 100644
--- a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/scene/DrawableViewNode.java
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.hierarchyviewerlib.scene;
+package com.android.hierarchyviewerlib.ui.util;
 
 import com.android.hierarchyviewerlib.device.ViewNode;
 
@@ -25,15 +25,23 @@
 
     public final ArrayList<DrawableViewNode> children = new ArrayList<DrawableViewNode>();
 
-    public final static int NODE_HEIGHT = 70;
+    public final static int NODE_HEIGHT = 110;
 
-    public final static int NODE_WIDTH = 100;
+    public final static int NODE_WIDTH = 170;
 
-    public final static int LEAF_NODE_SPACING = 5;
+    public final static int CONTENT_LEFT_RIGHT_PADDING = 3;
 
-    public final static int NON_LEAF_NODE_SPACING = 10;
+    public final static int CONTENT_TOP_BOTTOM_PADDING = 7;
 
-    public final static int PARENT_CHILD_SPACING = 40;
+    public final static int CONTENT_INTER_PADDING = 3;
+
+    public final static int INDEX_PADDING = 5;
+
+    public final static int LEAF_NODE_SPACING = 9;
+
+    public final static int NON_LEAF_NODE_SPACING = 15;
+
+    public final static int PARENT_CHILD_SPACING = 50;
 
     public final static int PADDING = 30;
 
@@ -53,6 +61,8 @@
 
     public int bottomSpacing;
 
+    public boolean treeDrawn;
+
     public static class Rectangle {
         public double x, y, width, height;
 
@@ -91,12 +101,18 @@
             this.x = x;
             this.y = y;
         }
+
+        @Override
+        public String toString() {
+            return "(" + x + ", " + y + ")";
+        }
     }
 
     public Rectangle bounds = new Rectangle();
 
     public DrawableViewNode(ViewNode viewNode) {
         this.viewNode = viewNode;
+        treeDrawn = !viewNode.willNotDraw;
         if (viewNode.children.size() == 0) {
             treeHeight = NODE_HEIGHT;
             treeWidth = NODE_WIDTH;
@@ -124,6 +140,7 @@
                         child.topSpacing = NON_LEAF_NODE_SPACING;
                     }
                 }
+                treeDrawn |= child.treeDrawn;
             }
             treeWidth += NODE_WIDTH + PARENT_CHILD_SPACING;
         }
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java
new file mode 100644
index 0000000..ad18540
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.hierarchyviewerlib.ui.util;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.TreeColumn;
+
+public class TreeColumnResizer {
+
+    private TreeColumn column1;
+    private TreeColumn column2;
+
+    private Composite control;
+    private int column1Width;
+    private int column2Width;
+
+    private final static int MIN_COLUMN1_WIDTH = 18;
+
+    private final static int MIN_COLUMN2_WIDTH = 3;
+
+    public TreeColumnResizer(Composite control, TreeColumn column1, TreeColumn column2) {
+        this.control = control;
+        this.column1 = column1;
+        this.column2 = column2;
+        control.addListener(SWT.Resize, resizeListener);
+        column1.addListener(SWT.Resize, column1ResizeListener);
+        column2.setResizable(false);
+    }
+
+    private Listener resizeListener = new Listener() {
+        public void handleEvent(Event e) {
+            if (column1Width == 0 && column2Width == 0) {
+                column1Width = (control.getBounds().width - 18) / 2;
+                column2Width = (control.getBounds().width - 18) / 2;
+            } else {
+                int dif = control.getBounds().width - 18 - (column1Width + column2Width);
+                int columnDif = Math.abs(column1Width - column2Width);
+                int mainColumnChange = Math.min(Math.abs(dif), columnDif);
+                int left = Math.max(0, Math.abs(dif) - columnDif);
+                if (dif < 0) {
+                    if (column1Width > column2Width) {
+                        column1Width -= mainColumnChange;
+                    } else {
+                        column2Width -= mainColumnChange;
+                    }
+                    column1Width -= left / 2;
+                    column2Width -= left - left / 2;
+                } else {
+                    if (column1Width > column2Width) {
+                        column2Width += mainColumnChange;
+                    } else {
+                        column1Width += mainColumnChange;
+                    }
+                    column1Width += left / 2;
+                    column2Width += left - left / 2;
+                }
+            }
+            column1.removeListener(SWT.Resize, column1ResizeListener);
+            column1.setWidth(column1Width);
+            column2.setWidth(column2Width);
+            column1.addListener(SWT.Resize, column1ResizeListener);
+        }
+    };
+
+    private Listener column1ResizeListener = new Listener() {
+        public void handleEvent(Event e) {
+            int widthDif = column1Width - column1.getWidth();
+            column1Width -= widthDif;
+            column2Width += widthDif;
+            boolean column1Changed = false;
+
+            // Strange, but these constants make the columns look the same.
+
+            if (column1Width < MIN_COLUMN1_WIDTH) {
+                column2Width -= MIN_COLUMN1_WIDTH - column1Width;
+                column1Width += MIN_COLUMN1_WIDTH - column1Width;
+                column1Changed = true;
+            }
+            if (column2Width < MIN_COLUMN2_WIDTH) {
+                column1Width += column2Width - MIN_COLUMN2_WIDTH;
+                column2Width = MIN_COLUMN2_WIDTH;
+                column1Changed = true;
+            }
+            if (column1Changed) {
+                column1.removeListener(SWT.Resize, this);
+                column1.setWidth(column1Width);
+                column1.addListener(SWT.Resize, this);
+            }
+            column2.setWidth(column2Width);
+        }
+    };
+}
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/green.png b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/green.png
new file mode 100644
index 0000000..b52a342
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/green.png
Binary files differ
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/red.png b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/red.png
new file mode 100644
index 0000000..338c2d9
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/red.png
Binary files differ
diff --git a/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/yellow.png b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/yellow.png
new file mode 100644
index 0000000..b6fadac
--- /dev/null
+++ b/hierarchyviewer2/libs/hierarchyviewerlib/src/resources/images/yellow.png
Binary files differ