Merge "Move components to separate classes."
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/CorruptPatch.java b/draw9patch/src/main/java/com/android/draw9patch/ui/CorruptPatch.java
new file mode 100644
index 0000000..4f0763a
--- /dev/null
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/CorruptPatch.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 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.draw9patch.ui;
+
+import com.android.draw9patch.graphics.GraphicsUtilities;
+
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class CorruptPatch {
+    public static List<Rectangle> findBadPatches(BufferedImage image, PatchInfo patchInfo) {
+        List<Rectangle> corruptedPatches = new ArrayList<Rectangle>();
+
+        for (Rectangle patch : patchInfo.patches) {
+            if (corruptPatch(image, patch)) {
+                corruptedPatches.add(patch);
+            }
+        }
+
+        for (Rectangle patch : patchInfo.horizontalPatches) {
+            if (corruptHorizontalPatch(image, patch)) {
+                corruptedPatches.add(patch);
+            }
+        }
+
+        for (Rectangle patch : patchInfo.verticalPatches) {
+            if (corruptVerticalPatch(image, patch)) {
+                corruptedPatches.add(patch);
+            }
+        }
+
+        return corruptedPatches;
+    }
+
+    private static boolean corruptPatch(BufferedImage image, Rectangle patch) {
+        int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y,
+                patch.width, patch.height, null);
+
+        if (pixels.length > 0) {
+            int reference = pixels[0];
+            for (int pixel : pixels) {
+                if (pixel != reference) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private static boolean corruptHorizontalPatch(BufferedImage image, Rectangle patch) {
+        int[] reference = new int[patch.height];
+        int[] column = new int[patch.height];
+        reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
+                1, patch.height, reference);
+
+        for (int i = 1; i < patch.width; i++) {
+            column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y,
+                    1, patch.height, column);
+            if (!Arrays.equals(reference, column)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static boolean corruptVerticalPatch(BufferedImage image, Rectangle patch) {
+        int[] reference = new int[patch.width];
+        int[] row = new int[patch.width];
+        reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
+                patch.width, 1, reference);
+
+        for (int i = 1; i < patch.height; i++) {
+            row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row);
+            if (!Arrays.equals(reference, row)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/ImageEditorPanel.java b/draw9patch/src/main/java/com/android/draw9patch/ui/ImageEditorPanel.java
index 845ee54..c707c38 100644
--- a/draw9patch/src/main/java/com/android/draw9patch/ui/ImageEditorPanel.java
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/ImageEditorPanel.java
@@ -18,66 +18,36 @@
 
 import com.android.draw9patch.graphics.GraphicsUtilities;
 
-import javax.swing.JPanel;
-import javax.swing.JLabel;
-import javax.swing.BorderFactory;
-import javax.swing.JSlider;
-import javax.swing.JComponent;
-import javax.swing.JScrollPane;
-import javax.swing.JCheckBox;
-import javax.swing.Box;
-import javax.swing.JFileChooser;
-import javax.swing.JSplitPane;
-import javax.swing.JButton;
-import javax.swing.border.EmptyBorder;
-import javax.swing.event.AncestorEvent;
-import javax.swing.event.AncestorListener;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.ChangeEvent;
-import java.awt.image.BufferedImage;
-import java.awt.image.RenderedImage;
-import java.awt.Graphics2D;
 import java.awt.BorderLayout;
 import java.awt.Color;
-import java.awt.Graphics;
-import java.awt.Dimension;
-import java.awt.TexturePaint;
-import java.awt.Shape;
-import java.awt.BasicStroke;
-import java.awt.RenderingHints;
-import java.awt.Rectangle;
-import java.awt.GridBagLayout;
+import java.awt.Graphics2D;
 import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
 import java.awt.Insets;
-import java.awt.Toolkit;
-import java.awt.AWTEvent;
-import java.awt.event.MouseMotionAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.ActionListener;
+import java.awt.TexturePaint;
 import java.awt.event.ActionEvent;
-import java.awt.event.KeyEvent;
-import java.awt.event.AWTEventListener;
+import java.awt.event.ActionListener;
 import java.awt.geom.Rectangle2D;
-import java.awt.geom.Line2D;
-import java.awt.geom.Area;
-import java.awt.geom.RoundRectangle2D;
-import java.io.IOException;
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
 import java.io.File;
+import java.io.IOException;
 import java.net.URL;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Arrays;
+
+import javax.swing.Box;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSlider;
+import javax.swing.JSplitPane;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 
 class ImageEditorPanel extends JPanel {
     private static final String EXTENSION_9PATCH = ".9.png";
-    private static final int DEFAULT_ZOOM = 8;
-    private static final float DEFAULT_SCALE = 2.0f;
-
-    // For stretch regions and padding
-    private static final int BLACK_TICK = 0xFF000000;
-    // For Layout Bounds
-    private static final int RED_TICK = 0xFFFF0000;
 
     private String name;
     private BufferedImage image;
@@ -90,30 +60,20 @@
 
     private TexturePaint texture;
 
-    private List<Rectangle> patches;
-    private List<Rectangle> horizontalPatches;
-    private List<Rectangle> verticalPatches;
-    private List<Rectangle> fixed;
-    private boolean verticalStartWithPatch;
-    private boolean horizontalStartWithPatch;
-
-    private Pair<Integer> horizontalPadding;
-    private Pair<Integer> verticalPadding;
-
     ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name) {
         this.image = image;
         this.name = name;
 
         setTransferHandler(new ImageTransferHandler(mainFrame));
 
-        checkImage();
-
         setOpaque(false);
         setLayout(new BorderLayout());
 
         loadSupport();
         buildImageViewer();
         buildStatusPanel();
+
+        checkImage();
     }
 
     private void loadSupport() {
@@ -128,7 +88,13 @@
     }
 
     private void buildImageViewer() {
-        viewer = new ImageViewer();
+        viewer = new ImageViewer(this, texture, image, new ImageViewer.StatusBar() {
+            @Override
+            public void setPointerLocation(int x, int y) {
+                xLabel.setText(x + " px");
+                yLabel.setText(y + " px");
+            }
+        });
 
         JSplitPane splitter = new JSplitPane();
         splitter.setContinuousLayout(true);
@@ -148,7 +114,7 @@
     }
 
     private JComponent buildStretchesViewer() {
-        stretchesViewer = new StretchesViewer();
+        stretchesViewer = new StretchesViewer(this, viewer, texture);
         JScrollPane scroller = new JScrollPane(stretchesViewer);
         scroller.setBorder(null);
         scroller.getViewport().setBorder(null);
@@ -177,7 +143,8 @@
                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
                 new Insets(0, 0, 0, 0), 0, 0));
 
-        JSlider zoomSlider = new JSlider(1, 16, DEFAULT_ZOOM);
+        JSlider zoomSlider = new JSlider(ImageViewer.MIN_ZOOM, ImageViewer.MAX_ZOOM,
+                ImageViewer.DEFAULT_ZOOM);
         zoomSlider.setSnapToTicks(true);
         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
         zoomSlider.addChangeListener(new ChangeListener() {
@@ -213,7 +180,7 @@
                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
                 new Insets(0, 0, 0, 0), 0, 0));
 
-        zoomSlider = new JSlider(200, 600, (int) (DEFAULT_SCALE * 100.0f));
+        zoomSlider = new JSlider(200, 600, (int) (StretchesViewer.DEFAULT_SCALE * 100.0f));
         zoomSlider.setSnapToTicks(true);
         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
         zoomSlider.addChangeListener(new ChangeListener() {
@@ -322,21 +289,21 @@
         int height = image.getHeight();
         for (int i = 0; i < width; i++) {
             int pixel = image.getRGB(i, 0);
-            if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
+            if (pixel != 0 && pixel != PatchInfo.BLACK_TICK && pixel != PatchInfo.RED_TICK) {
                 image.setRGB(i, 0, 0);
             }
             pixel = image.getRGB(i, height - 1);
-            if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
+            if (pixel != 0 && pixel != PatchInfo.BLACK_TICK && pixel != PatchInfo.RED_TICK) {
                 image.setRGB(i, height - 1, 0);
             }
         }
         for (int i = 0; i < height; i++) {
             int pixel = image.getRGB(0, i);
-            if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
+            if (pixel != 0 && pixel != PatchInfo.BLACK_TICK && pixel != PatchInfo.RED_TICK) {
                 image.setRGB(0, i, 0);
             }
             pixel = image.getRGB(width - 1, i);
-            if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
+            if (pixel != 0 && pixel != PatchInfo.BLACK_TICK && pixel != PatchInfo.RED_TICK) {
                 image.setRGB(width - 1, i, 0);
             }
         }
@@ -351,6 +318,7 @@
         g2.dispose();
 
         image = buffer;
+        viewer.setImage(image);
         name = name.substring(0, name.lastIndexOf('.')) + ".9.png";
     }
 
@@ -385,944 +353,4 @@
     RenderedImage getImage() {
         return image;
     }
-
-    private class StretchesViewer extends JPanel {
-        private static final int MARGIN = 24;
-
-        private StretchView horizontal;
-        private StretchView vertical;
-        private StretchView both;
-
-        private Dimension size;
-
-        private float horizontalPatchesSum;
-        private float verticalPatchesSum;
-
-        private boolean showPadding;
-
-        StretchesViewer() {
-            setOpaque(false);
-            setLayout(new GridBagLayout());
-            setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
-
-            horizontal = new StretchView();
-            vertical = new StretchView();
-            both = new StretchView();
-
-            setScale(DEFAULT_SCALE);
-
-            add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
-                    GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
-            add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
-                    GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
-            add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
-                    GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
-        }
-
-        @Override
-        protected void paintComponent(Graphics g) {
-            Graphics2D g2 = (Graphics2D) g.create();
-            g2.setPaint(texture);
-            g2.fillRect(0, 0, getWidth(), getHeight());
-            g2.dispose();
-        }
-
-        void setScale(float scale) {
-            int patchWidth = image.getWidth() - 2;
-            int patchHeight = image.getHeight() - 2;
-
-            int scaledWidth = (int) (patchWidth * scale);
-            int scaledHeight = (int) (patchHeight * scale);
-
-            horizontal.scaledWidth = scaledWidth;
-            vertical.scaledHeight = scaledHeight;
-            both.scaledWidth = scaledWidth;
-            both.scaledHeight = scaledHeight;
-
-            size = new Dimension(scaledWidth, scaledHeight);
-
-            computePatches();
-        }
-
-        void computePatches() {
-            boolean measuredWidth = false;
-            boolean endRow = true;
-
-            int remainderHorizontal = 0;
-            int remainderVertical = 0;
-
-            if (fixed.size() > 0) {
-                int start = fixed.get(0).y;
-                for (Rectangle rect : fixed) {
-                    if (rect.y > start) {
-                        endRow = true;
-                        measuredWidth = true;
-                    }
-                    if (!measuredWidth) {
-                        remainderHorizontal += rect.width;
-                    }
-                    if (endRow) {
-                        remainderVertical += rect.height;
-                        endRow = false;
-                        start = rect.y;
-                    }
-                }
-            }
-
-            horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal;
-            vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal;
-            both.remainderHorizontal = both.scaledWidth - remainderHorizontal;
-
-            horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical;
-            vertical.remainderVertical = vertical.scaledHeight - remainderVertical;
-            both.remainderVertical = both.scaledHeight - remainderVertical;
-
-            horizontalPatchesSum = 0;
-            if (horizontalPatches.size() > 0) {
-                int start = -1;
-                for (Rectangle rect : horizontalPatches) {
-                    if (rect.x > start) {
-                        horizontalPatchesSum += rect.width;
-                        start = rect.x;
-                    }
-                }
-            } else {
-                int start = -1;
-                for (Rectangle rect : patches) {
-                    if (rect.x > start) {
-                        horizontalPatchesSum += rect.width;
-                        start = rect.x;
-                    }
-                }
-            }
-
-            verticalPatchesSum = 0;
-            if (verticalPatches.size() > 0) {
-                int start = -1;
-                for (Rectangle rect : verticalPatches) {
-                    if (rect.y > start) {
-                        verticalPatchesSum += rect.height;
-                        start = rect.y;
-                    }
-                }
-            } else {
-                int start = -1;
-                for (Rectangle rect : patches) {
-                    if (rect.y > start) {
-                        verticalPatchesSum += rect.height;
-                        start = rect.y;
-                    }
-                }
-            }
-
-            setSize(size);
-            ImageEditorPanel.this.validate();
-            repaint();
-        }
-
-        void setPaddingVisible(boolean visible) {
-            showPadding = visible;
-            repaint();
-        }
-
-        private class StretchView extends JComponent {
-            private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f);
-
-            int scaledWidth;
-            int scaledHeight;
-
-            int remainderHorizontal;
-            int remainderVertical;
-
-            StretchView() {
-                scaledWidth = image.getWidth();
-                scaledHeight = image.getHeight();
-            }
-
-            @Override
-            protected void paintComponent(Graphics g) {
-                int x = (getWidth() - scaledWidth) / 2;
-                int y = (getHeight() - scaledHeight) / 2;
-
-                Graphics2D g2 = (Graphics2D) g.create();
-                g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
-                        RenderingHints.VALUE_INTERPOLATION_BILINEAR);
-                g.translate(x, y);
-
-                x = 0;
-                y = 0;
-
-                if (patches.size() == 0) {
-                    g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
-                    g2.dispose();
-                    return;
-                }
-
-                int fixedIndex = 0;
-                int horizontalIndex = 0;
-                int verticalIndex = 0;
-                int patchIndex = 0;
-
-                boolean hStretch;
-                boolean vStretch;
-
-                float vWeightSum = 1.0f;
-                float vRemainder = remainderVertical;
-
-                vStretch = verticalStartWithPatch;
-                while (y < scaledHeight - 1) {
-                    hStretch = horizontalStartWithPatch;
-
-                    int height = 0;
-                    float vExtra = 0.0f;
-
-                    float hWeightSum = 1.0f;
-                    float hRemainder = remainderHorizontal;
-
-                    while (x < scaledWidth - 1) {
-                        Rectangle r;
-                        if (!vStretch) {
-                            if (hStretch) {
-                                r = horizontalPatches.get(horizontalIndex++);
-                                float extra = r.width / horizontalPatchesSum;
-                                int width = (int) (extra * hRemainder / hWeightSum);
-                                hWeightSum -= extra;
-                                hRemainder -= width;
-                                g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
-                                        r.x + r.width, r.y + r.height, null);
-                                x += width;
-                            } else {
-                                r = fixed.get(fixedIndex++);
-                                g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
-                                        r.x + r.width, r.y + r.height, null);
-                                x += r.width;
-                            }
-                            height = r.height;
-                        } else {
-                            if (hStretch) {
-                                r = patches.get(patchIndex++);
-                                vExtra = r.height / verticalPatchesSum;
-                                height = (int) (vExtra * vRemainder / vWeightSum);
-                                float extra = r.width / horizontalPatchesSum;
-                                int width = (int) (extra * hRemainder / hWeightSum);
-                                hWeightSum -= extra;
-                                hRemainder -= width;
-                                g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
-                                        r.x + r.width, r.y + r.height, null);
-                                x += width;
-                            } else {
-                                r = verticalPatches.get(verticalIndex++);
-                                vExtra = r.height / verticalPatchesSum;
-                                height = (int) (vExtra * vRemainder / vWeightSum);
-                                g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
-                                        r.x + r.width, r.y + r.height, null);
-                                x += r.width;
-                            }
-                            
-                        }
-                        hStretch = !hStretch;
-                    }
-                    x = 0;
-                    y += height;
-                    if (vStretch) {
-                        vWeightSum -= vExtra;
-                        vRemainder -= height;
-                    }
-                    vStretch = !vStretch;
-                }
-
-                if (showPadding) {
-                    g.setColor(PADDING_COLOR);
-                    g.fillRect(horizontalPadding.first, verticalPadding.first,
-                            scaledWidth - horizontalPadding.first - horizontalPadding.second,
-                            scaledHeight - verticalPadding.first - verticalPadding.second);
-                }
-
-                g2.dispose();
-            }
-
-            @Override
-            public Dimension getPreferredSize() {
-                return size;
-            }
-        }
-    }
-
-    private class ImageViewer extends JComponent {
-        private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f);
-        private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f);
-        private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f);
-        private final Color BACK_COLOR = new Color(0xc0c0c0);
-        private final Color HELP_COLOR = new Color(0xffffe1);
-        private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f);
-        private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f);
-
-        private static final float STRIPES_WIDTH = 4.0f;
-        private static final double STRIPES_SPACING = 6.0;
-        private static final int STRIPES_ANGLE = 45;
-
-        private int zoom = DEFAULT_ZOOM;
-        private boolean showPatches;
-        private boolean showLock = true;
-
-        private final Dimension size;
-
-        private boolean locked;
-
-        private int[] row;
-        private int[] column;
-
-        private int lastPositionX;
-        private int lastPositionY;
-        private int currentButton;
-        private boolean showCursor;
-
-        private JLabel helpLabel;
-        private boolean eraseMode;
-
-        private JButton checkButton;
-        private List<Rectangle> corruptedPatches;
-        private boolean showBadPatches;
-
-        private JPanel helpPanel;
-        private boolean drawingLine;
-        private int lineFromX;
-        private int lineFromY;
-        private int lineToX;
-        private int lineToY;
-        private boolean showDrawingLine;
-
-        ImageViewer() {
-            setLayout(new GridBagLayout());
-            helpPanel = new JPanel(new BorderLayout());
-            helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
-            helpPanel.setBackground(HELP_COLOR);
-            helpLabel = new JLabel("Press Shift to erase pixels."
-                    + " Press Control to draw layout bounds");
-            helpLabel.putClientProperty("JComponent.sizeVariant", "small");            
-            helpPanel.add(helpLabel, BorderLayout.WEST);
-            checkButton = new JButton("Show bad patches");
-            checkButton.putClientProperty("JComponent.sizeVariant", "small");
-            checkButton.putClientProperty("JButton.buttonType", "roundRect");
-            helpPanel.add(checkButton, BorderLayout.EAST);
-
-            add(helpPanel, new GridBagConstraints(0, 0, 1, 1,
-                    1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
-                    new Insets(0, 0, 0, 0), 0, 0));
-
-            setOpaque(true);
-
-            // Exact size will be set by setZoom() in AncestorListener#ancestorMoved.
-            size = new Dimension(0, 0);
-
-            addAncestorListener(new AncestorListener() {
-                @Override
-                public void ancestorRemoved(AncestorEvent event) {
-                }
-                @Override
-                public void ancestorMoved(AncestorEvent event) {
-                    // Set exactly size.
-                    viewer.setZoom(DEFAULT_ZOOM);
-                    viewer.removeAncestorListener(this);
-                }
-                @Override
-                public void ancestorAdded(AncestorEvent event) {
-                }
-            });
-
-            findPatches();
-
-            addMouseListener(new MouseAdapter() {
-                @Override
-                public void mousePressed(MouseEvent event) {
-                    // Store the button here instead of retrieving it again in MouseDragged
-                    // below, because on linux, calling MouseEvent.getButton() for the drag
-                    // event returns 0, which appears to be technically correct (no button
-                    // changed state).
-                    currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
-                    currentButton = event.isControlDown() ? MouseEvent.BUTTON2 : currentButton;
-                    startDrawingLine(event.getX(), event.getY(), currentButton);
-                }
-
-                @Override
-                public void mouseReleased(MouseEvent event) {
-                    endDrawingLine();
-                }
-            });
-            addMouseMotionListener(new MouseMotionAdapter() {
-                @Override
-                public void mouseDragged(MouseEvent event) {
-                    if (!checkLockedRegion(event.getX(), event.getY())) {
-                        // use the stored button, see note above
-
-                        moveLine(event.getX(), event.getY());
-                    }
-                }
-
-                @Override
-                public void mouseMoved(MouseEvent event) {
-                    checkLockedRegion(event.getX(), event.getY());
-                }
-            });
-            Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
-                public void eventDispatched(AWTEvent event) {
-                    enableEraseMode((KeyEvent) event);
-                }
-            }, AWTEvent.KEY_EVENT_MASK);
-
-            checkButton.addActionListener(new ActionListener() {
-                public void actionPerformed(ActionEvent event) {
-                    if (!showBadPatches) {
-                        findBadPatches();
-                        checkButton.setText("Hide bad patches");
-                    } else {
-                        checkButton.setText("Show bad patches");
-                        corruptedPatches = null;
-                    }
-                    repaint();
-                    showBadPatches = !showBadPatches;
-                }
-            });
-        }
-
-        private void findBadPatches() {
-            corruptedPatches = new ArrayList<Rectangle>();
-
-            for (Rectangle patch : patches) {
-                if (corruptPatch(patch)) {
-                    corruptedPatches.add(patch);
-                }
-            }
-
-            for (Rectangle patch : horizontalPatches) {
-                if (corruptHorizontalPatch(patch)) {
-                    corruptedPatches.add(patch);
-                }
-            }
-
-            for (Rectangle patch : verticalPatches) {
-                if (corruptVerticalPatch(patch)) {
-                    corruptedPatches.add(patch);
-                }
-            }
-        }
-
-        private boolean corruptPatch(Rectangle patch) {
-            int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y,
-                    patch.width, patch.height, null);
-
-            if (pixels.length > 0) {
-                int reference = pixels[0];
-                for (int pixel : pixels) {
-                    if (pixel != reference) {
-                        return true;
-                    }
-                }
-            }
-
-            return false;
-        }
-
-        private boolean corruptHorizontalPatch(Rectangle patch) {
-            int[] reference = new int[patch.height];
-            int[] column = new int[patch.height];
-            reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
-                    1, patch.height, reference);
-
-            for (int i = 1; i < patch.width; i++) {
-                column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y,
-                        1, patch.height, column);
-                if (!Arrays.equals(reference, column)) {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        private boolean corruptVerticalPatch(Rectangle patch) {
-            int[] reference = new int[patch.width];
-            int[] row = new int[patch.width];
-            reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
-                    patch.width, 1, reference);
-
-            for (int i = 1; i < patch.height; i++) {
-                row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row);
-                if (!Arrays.equals(reference, row)) {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        private void enableEraseMode(KeyEvent event) {
-            boolean oldEraseMode = eraseMode;
-            eraseMode = event.isShiftDown();
-            if (eraseMode != oldEraseMode) {
-                if (eraseMode) {
-                    helpLabel.setText("Release Shift to draw pixels");
-                } else {
-                    helpLabel.setText("Press Shift to erase pixels."
-                                      + " Press Control to draw layout bounds");
-                }
-            }
-        }
-
-        private void startDrawingLine(int x, int y, int button) {
-            int left = (getWidth() - size.width) / 2;
-            int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
-
-            x = (x - left) / zoom;
-            y = (y - top) / zoom;
-
-            int width = image.getWidth();
-            int height = image.getHeight();
-            if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1))
-                    || ((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) {
-                drawingLine = true;
-                lineFromX = x;
-                lineFromY = y;
-                lineToX = x;
-                lineToY = y;
-
-                showDrawingLine = true;
-
-                showCursor = false;
-
-                repaint();
-            }
-        }
-
-        private void moveLine(int x, int y) {
-            if (drawingLine == false)
-                return;
-
-            int left = (getWidth() - size.width) / 2;
-            int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
-
-            x = (x - left) / zoom;
-            y = (y - top) / zoom;
-
-            int width = image.getWidth();
-            int height = image.getHeight();
-
-            showDrawingLine = false;
-
-            if (((x == lineFromX) && (y > 0 && y < height - 1))
-                    || ((x > 0 && x < width - 1) && (y == lineFromY))) {
-                if (x == lineFromX || y == lineFromY) {
-                    lineToX = x;
-                    lineToY = y;
-
-                    showDrawingLine = true;
-                }
-            }
-
-            repaint();
-        }
-
-        private void endDrawingLine() {
-            if (drawingLine == false)
-                return;
-
-            drawingLine = false;
-
-            if (showDrawingLine == false)
-                return;
-
-            int color;
-            switch (currentButton) {
-            case MouseEvent.BUTTON1:
-                color = BLACK_TICK;
-                break;
-            case MouseEvent.BUTTON2:
-                color = RED_TICK;
-                break;
-            case MouseEvent.BUTTON3:
-                color = 0;
-                break;
-            default:
-                return;
-            }
-
-            int x = lineFromX;
-            int y = lineFromY;
-
-            int dx = 0;
-            int dy = 0;
-
-            if (lineToX != lineFromX)
-                dx = lineToX > lineFromX ? 1 : -1;
-            else if (lineToY != lineFromY)
-                dy = lineToY > lineFromY ? 1 : -1;
-
-            do {
-                image.setRGB(x, y, color);
-
-                if (x == lineToX && y == lineToY)
-                    break;
-
-                x += dx;
-                y += dy;
-            } while (true);
-
-            findPatches();
-            stretchesViewer.computePatches();
-            if (showBadPatches) {
-                findBadPatches();
-            }
-
-            repaint();
-        }
-
-        private boolean checkLockedRegion(int x, int y) {
-            int oldX = lastPositionX;
-            int oldY = lastPositionY;
-            lastPositionX = x;
-            lastPositionY = y;
-
-            int left = (getWidth() - size.width) / 2;
-            int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
-
-            x = (x - left) / zoom;
-            y = (y - top) / zoom;
-
-            int width = image.getWidth();
-            int height = image.getHeight();
-
-            xLabel.setText(Math.max(0, Math.min(x, width - 1)) + " px");
-            yLabel.setText(Math.max(0, Math.min(y, height - 1)) + " px");
-
-            boolean previousLock = locked;
-            locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
-
-            boolean previousCursor = showCursor;
-            showCursor =
-                    !drawingLine &&
-                    ( ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
-                      ((x > 0 && x < width - 1) && (y == 0 || y == height - 1)) );
-
-            if (locked != previousLock) {
-                repaint();
-            } else if (showCursor || (showCursor != previousCursor)) {
-                Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2,
-                        lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2);
-                clip = clip.union(new Rectangle(oldX - 1 - zoom / 2,
-                        oldY - 1 - zoom / 2, zoom + 2, zoom + 2));
-                repaint(clip);
-            }
-
-            return locked;
-        }
-
-        @Override
-        protected void paintComponent(Graphics g) {
-            int x = (getWidth() - size.width) / 2;
-            int y = helpPanel.getHeight() + (getHeight() - size.height) / 2;
-
-            Graphics2D g2 = (Graphics2D) g.create();
-            g2.setColor(BACK_COLOR);
-            g2.fillRect(0, 0, getWidth(), getHeight());
-
-            g2.translate(x, y);
-            g2.setPaint(texture);
-            g2.fillRect(0, 0, size.width, size.height);
-            g2.scale(zoom, zoom);
-            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
-                    RenderingHints.VALUE_ANTIALIAS_ON);
-            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
-                    RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
-            g2.drawImage(image, 0, 0, null);
-
-            if (showPatches) {
-                g2.setColor(PATCH_COLOR);
-                for (Rectangle patch : patches) {
-                    g2.fillRect(patch.x, patch.y, patch.width, patch.height);
-                }
-                g2.setColor(PATCH_ONEWAY_COLOR);
-                for (Rectangle patch : horizontalPatches) {
-                    g2.fillRect(patch.x, patch.y, patch.width, patch.height);
-                }
-                for (Rectangle patch : verticalPatches) {
-                    g2.fillRect(patch.x, patch.y, patch.width, patch.height);
-                }
-            }
-
-            if (corruptedPatches != null) {
-                g2.setColor(CORRUPTED_COLOR);
-                g2.setStroke(new BasicStroke(3.0f / zoom));
-                for (Rectangle patch : corruptedPatches) {
-                    g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom,
-                            patch.width + 2.0f / zoom, patch.height + 2.0f / zoom,
-                            6.0f / zoom, 6.0f / zoom));
-                }
-            }
-
-            if (showLock && locked) {
-                int width = image.getWidth();
-                int height = image.getHeight();
-
-                g2.setColor(LOCK_COLOR);
-                g2.fillRect(1, 1, width - 2, height - 2);
-
-                g2.setColor(STRIPES_COLOR);
-                g2.translate(1, 1);
-                paintStripes(g2, width - 2, height - 2);
-                g2.translate(-1, -1);
-            }
-
-            g2.dispose();
-
-            if (drawingLine && showDrawingLine) {
-                Graphics cursor = g.create();
-                cursor.setXORMode(Color.WHITE);
-                cursor.setColor(Color.BLACK);
-
-                x = Math.min(lineFromX, lineToX);
-                y = Math.min(lineFromY, lineToY);
-                int w = Math.abs(lineFromX - lineToX) + 1;
-                int h = Math.abs(lineFromY - lineToY) + 1;
-
-                x = x * zoom;
-                y = y * zoom;
-                w = w * zoom;
-                h = h * zoom;
-
-                int left = (getWidth() - size.width) / 2;
-                int top = helpPanel.getHeight() + (getHeight() - size.height)
-                        / 2;
-
-                x += left;
-                y += top;
-
-                cursor.drawRect(x, y, w, h);
-                cursor.dispose();
-            }
-
-            if (showCursor) {
-                Graphics cursor = g.create();
-                cursor.setXORMode(Color.WHITE);
-                cursor.setColor(Color.BLACK);
-                cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom);
-                cursor.dispose();
-            }
-        }
-
-        private void paintStripes(Graphics2D g, int width, int height) {
-            //draws pinstripes at the angle specified in this class
-            //and at the given distance apart
-            Shape oldClip = g.getClip();
-            Area area = new Area(new Rectangle(0, 0, width, height));
-            if(oldClip != null) {
-                area = new Area(oldClip);
-            }
-            area.intersect(new Area(new Rectangle(0,0,width,height)));
-            g.setClip(area);
-
-            g.setStroke(new BasicStroke(STRIPES_WIDTH));
-
-            double hypLength = Math.sqrt((width * width) +
-                    (height * height));
-
-            double radians = Math.toRadians(STRIPES_ANGLE);
-            g.rotate(radians);
-
-            double spacing = STRIPES_SPACING;
-            spacing += STRIPES_WIDTH;
-            int numLines = (int)(hypLength / spacing);
-
-            for (int i=0; i<numLines; i++) {
-                double x = i * spacing;
-                Line2D line = new Line2D.Double(x, -hypLength, x, hypLength);
-                g.draw(line);
-            }
-            g.setClip(oldClip);
-        }
-
-        @Override
-        public Dimension getPreferredSize() {
-            return size;
-        }
-
-        void setZoom(int value) {
-            int width = image.getWidth();
-            int height = image.getHeight();
-
-            zoom = value;
-            if (size.height == 0 || (getHeight() - size.height) == 0) {
-                size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
-            } else {
-                size.setSize(width * zoom, height * zoom);
-            }
-
-            if (!size.equals(getSize())) {
-                setSize(size);
-                ImageEditorPanel.this.validate();
-                repaint();
-            }
-        }
-
-        void setPatchesVisible(boolean visible) {
-            showPatches = visible;
-            findPatches();
-            repaint();
-        }
-
-        private void findPatches() {
-            int width = image.getWidth();
-            int height = image.getHeight();
-
-            row = GraphicsUtilities.getPixels(image, 0, 0, width, 1, row);
-            column = GraphicsUtilities.getPixels(image, 0, 0, 1, height, column);
-
-            boolean[] result = new boolean[1];
-            Pair<List<Pair<Integer>>> left = getPatches(column, result);
-            verticalStartWithPatch = result[0];
-
-            result = new boolean[1];
-            Pair<List<Pair<Integer>>> top = getPatches(row, result);
-            horizontalStartWithPatch = result[0];
-
-            fixed = getRectangles(left.first, top.first);
-            patches = getRectangles(left.second, top.second);
-
-            if (fixed.size() > 0) {
-                horizontalPatches = getRectangles(left.first, top.second);
-                verticalPatches = getRectangles(left.second, top.first);
-            } else {
-                if (top.first.size() > 0) {
-                    horizontalPatches = new ArrayList<Rectangle>(0);
-                    verticalPatches = getVerticalRectangles(top.first);
-                } else if (left.first.size() > 0) {
-                    horizontalPatches = getHorizontalRectangles(left.first);
-                    verticalPatches = new ArrayList<Rectangle>(0);
-                } else {
-                    horizontalPatches = verticalPatches = new ArrayList<Rectangle>(0);
-                }
-            }
-
-            row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row);
-            column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column);
-
-            top = getPatches(row, result);
-            horizontalPadding = getPadding(top.first);
-
-            left = getPatches(column, result);
-            verticalPadding = getPadding(left.first);
-        }
-
-        private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
-            List<Rectangle> rectangles = new ArrayList<Rectangle>();
-            for (Pair<Integer> top : topPairs) {
-                int x = top.first;
-                int width = top.second - top.first;
-
-                rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2));
-            }
-            return rectangles;
-        }
-
-        private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
-            List<Rectangle> rectangles = new ArrayList<Rectangle>();
-            for (Pair<Integer> left : leftPairs) {
-                int y = left.first;
-                int height = left.second - left.first;
-
-                rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height));
-            }
-            return rectangles;
-        }
-
-        private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
-            if (pairs.size() == 0) {
-                return new Pair<Integer>(0, 0);
-            } else if (pairs.size() == 1) {
-                if (pairs.get(0).first == 1) {
-                    return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first, 0);
-                } else {
-                    return new Pair<Integer>(0, pairs.get(0).second - pairs.get(0).first);                    
-                }
-            } else {
-                int index = pairs.size() - 1;
-                return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first,
-                        pairs.get(index).second - pairs.get(index).first);
-            }
-        }
-
-        private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
-                List<Pair<Integer>> topPairs) {
-            List<Rectangle> rectangles = new ArrayList<Rectangle>();
-            for (Pair<Integer> left : leftPairs) {
-                int y = left.first;
-                int height = left.second - left.first;
-                for (Pair<Integer> top : topPairs) {
-                    int x = top.first;
-                    int width = top.second - top.first;
-
-                    rectangles.add(new Rectangle(x, y, width, height));
-                }
-            }
-            return rectangles;
-        }
-
-        private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
-            int lastIndex = 1;
-            int lastPixel = pixels[1];
-            boolean first = true;
-
-            List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
-            List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
-
-            for (int i = 1; i < pixels.length - 1; i++) {
-                int pixel = pixels[i];
-                if (pixel != lastPixel) {
-                    if (lastPixel == BLACK_TICK) {
-                        if (first) startWithPatch[0] = true;
-                        patches.add(new Pair<Integer>(lastIndex, i));
-                    } else {
-                        fixed.add(new Pair<Integer>(lastIndex, i));
-                    }
-                    first = false;
-
-                    lastIndex = i;
-                    lastPixel = pixel;
-                }
-            }
-            if (lastPixel == BLACK_TICK) {
-                if (first) startWithPatch[0] = true;
-                patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
-            } else {
-                fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
-            }
-
-            if (patches.size() == 0) {
-                patches.add(new Pair<Integer>(1, pixels.length - 1));
-                startWithPatch[0] = true;
-                fixed.clear();
-            }
-
-            return new Pair<List<Pair<Integer>>>(fixed, patches);
-        }
-
-        void setLockVisible(boolean visible) {
-            showLock = visible;
-            repaint();
-        }
-    }
-
-    static class Pair<E> {
-        E first;
-        E second;
-
-        Pair(E first, E second) {
-            this.first = first;
-            this.second = second;
-        }
-
-        @Override
-        public String toString() {
-            return "Pair[" + first + ", " + second + "]";
-        }
-    }
 }
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/ImageViewer.java b/draw9patch/src/main/java/com/android/draw9patch/ui/ImageViewer.java
new file mode 100644
index 0000000..e36afc1
--- /dev/null
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/ImageViewer.java
@@ -0,0 +1,572 @@
+/*
+ *
+ *  Copyright (C) 2013 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.draw9patch.ui;
+
+import java.awt.AWTEvent;
+import java.awt.BasicStroke;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.TexturePaint;
+import java.awt.Toolkit;
+import java.awt.event.AWTEventListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+import java.awt.geom.Area;
+import java.awt.geom.Line2D;
+import java.awt.geom.RoundRectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.AncestorEvent;
+import javax.swing.event.AncestorListener;
+
+public class ImageViewer extends JComponent {
+    private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f);
+    private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f);
+    private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f);
+    private final Color BACK_COLOR = new Color(0xc0c0c0);
+    private final Color HELP_COLOR = new Color(0xffffe1);
+    private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f);
+    private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f);
+
+    private static final float STRIPES_WIDTH = 4.0f;
+    private static final double STRIPES_SPACING = 6.0;
+    private static final int STRIPES_ANGLE = 45;
+
+    /** Default zoom level for the 9patch image. */
+    public static final int DEFAULT_ZOOM = 8;
+
+    /** Minimum zoom level for the 9patch image. */
+    public static final int MIN_ZOOM = 1;
+
+    /** Maximum zoom level for the 9patch image. */
+    public static final int MAX_ZOOM = 16;
+
+    /** Current 9patch zoom level, {@link #MIN_ZOOM} <= zoom <= {@link #MAX_ZOOM} */
+    private int zoom = DEFAULT_ZOOM;
+    private boolean showPatches;
+    private boolean showLock = true;
+
+    private final TexturePaint texture;
+    private final Container container;
+    private final StatusBar statusBar;
+
+    private final Dimension size;
+
+    private boolean locked;
+
+    private int lastPositionX;
+    private int lastPositionY;
+    private int currentButton;
+    private boolean showCursor;
+
+    private JLabel helpLabel;
+    private boolean eraseMode;
+
+    private JButton checkButton;
+    private List<Rectangle> corruptedPatches;
+    private boolean showBadPatches;
+
+    private JPanel helpPanel;
+    private boolean drawingLine;
+    private int lineFromX;
+    private int lineFromY;
+    private int lineToX;
+    private int lineToY;
+    private boolean showDrawingLine;
+
+    private BufferedImage image;
+    private PatchInfo patchInfo;
+
+    ImageViewer(Container container, TexturePaint texture, BufferedImage image,
+                StatusBar statusBar) {
+        this.container = container;
+        this.texture = texture;
+        this.image = image;
+        this.statusBar = statusBar;
+
+        setLayout(new GridBagLayout());
+        helpPanel = new JPanel(new BorderLayout());
+        helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
+        helpPanel.setBackground(HELP_COLOR);
+        helpLabel = new JLabel("Press Shift to erase pixels."
+                + " Press Control to draw layout bounds");
+        helpLabel.putClientProperty("JComponent.sizeVariant", "small");
+        helpPanel.add(helpLabel, BorderLayout.WEST);
+        checkButton = new JButton("Show bad patches");
+        checkButton.putClientProperty("JComponent.sizeVariant", "small");
+        checkButton.putClientProperty("JButton.buttonType", "roundRect");
+        helpPanel.add(checkButton, BorderLayout.EAST);
+
+        add(helpPanel, new GridBagConstraints(0, 0, 1, 1,
+                1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
+                new Insets(0, 0, 0, 0), 0, 0));
+
+        setOpaque(true);
+
+        // Exact size will be set by setZoom() in AncestorListener#ancestorMoved.
+        size = new Dimension(0, 0);
+
+        addAncestorListener(new AncestorListener() {
+            @Override
+            public void ancestorRemoved(AncestorEvent event) {
+            }
+            @Override
+            public void ancestorMoved(AncestorEvent event) {
+                // Set exactly size.
+                setZoom(DEFAULT_ZOOM);
+                removeAncestorListener(this);
+            }
+            @Override
+            public void ancestorAdded(AncestorEvent event) {
+            }
+        });
+
+        updatePatchInfo();
+
+        addMouseListener(new MouseAdapter() {
+            @Override
+            public void mousePressed(MouseEvent event) {
+                // Store the button here instead of retrieving it again in MouseDragged
+                // below, because on linux, calling MouseEvent.getButton() for the drag
+                // event returns 0, which appears to be technically correct (no button
+                // changed state).
+                currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
+                currentButton = event.isControlDown() ? MouseEvent.BUTTON2 : currentButton;
+                startDrawingLine(event.getX(), event.getY());
+            }
+
+            @Override
+            public void mouseReleased(MouseEvent event) {
+                endDrawingLine();
+            }
+        });
+        addMouseMotionListener(new MouseMotionAdapter() {
+            @Override
+            public void mouseDragged(MouseEvent event) {
+                if (!checkLockedRegion(event.getX(), event.getY())) {
+                    // use the stored button, see note above
+
+                    moveLine(event.getX(), event.getY());
+                }
+            }
+
+            @Override
+            public void mouseMoved(MouseEvent event) {
+                checkLockedRegion(event.getX(), event.getY());
+            }
+        });
+        Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
+            public void eventDispatched(AWTEvent event) {
+                enableEraseMode((KeyEvent) event);
+            }
+        }, AWTEvent.KEY_EVENT_MASK);
+
+        checkButton.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent event) {
+                if (!showBadPatches) {
+                    corruptedPatches = CorruptPatch.findBadPatches(ImageViewer.this.image,
+                            patchInfo);
+                    checkButton.setText("Hide bad patches");
+                } else {
+                    checkButton.setText("Show bad patches");
+                    corruptedPatches = null;
+                }
+                repaint();
+                showBadPatches = !showBadPatches;
+            }
+        });
+    }
+
+    private void updatePatchInfo() {
+        patchInfo = new PatchInfo(image);
+    }
+
+    private void enableEraseMode(KeyEvent event) {
+        boolean oldEraseMode = eraseMode;
+        eraseMode = event.isShiftDown();
+        if (eraseMode != oldEraseMode) {
+            if (eraseMode) {
+                helpLabel.setText("Release Shift to draw pixels");
+            } else {
+                helpLabel.setText("Press Shift to erase pixels."
+                        + " Press Control to draw layout bounds");
+            }
+        }
+    }
+
+    private void startDrawingLine(int x, int y) {
+        int left = (getWidth() - size.width) / 2;
+        int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
+
+        x = (x - left) / zoom;
+        y = (y - top) / zoom;
+
+        int width = image.getWidth();
+        int height = image.getHeight();
+        if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1))
+                || ((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) {
+            drawingLine = true;
+            lineFromX = x;
+            lineFromY = y;
+            lineToX = x;
+            lineToY = y;
+
+            showDrawingLine = true;
+
+            showCursor = false;
+
+            repaint();
+        }
+    }
+
+    private void moveLine(int x, int y) {
+        if (!drawingLine) {
+            return;
+        }
+
+        int left = (getWidth() - size.width) / 2;
+        int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
+
+        x = (x - left) / zoom;
+        y = (y - top) / zoom;
+
+        int width = image.getWidth();
+        int height = image.getHeight();
+
+        showDrawingLine = false;
+
+        if (((x == lineFromX) && (y > 0 && y < height - 1))
+                || ((x > 0 && x < width - 1) && (y == lineFromY))) {
+            lineToX = x;
+            lineToY = y;
+
+            showDrawingLine = true;
+        }
+
+        repaint();
+    }
+
+    private void endDrawingLine() {
+        if (!drawingLine) {
+            return;
+        }
+
+        drawingLine = false;
+
+        if (!showDrawingLine) {
+            return;
+        }
+
+        int color;
+        switch (currentButton) {
+            case MouseEvent.BUTTON1:
+                color = PatchInfo.BLACK_TICK;
+                break;
+            case MouseEvent.BUTTON2:
+                color = PatchInfo.RED_TICK;
+                break;
+            case MouseEvent.BUTTON3:
+                color = 0;
+                break;
+            default:
+                return;
+        }
+
+        int x = lineFromX;
+        int y = lineFromY;
+
+        int dx = 0;
+        int dy = 0;
+
+        if (lineToX != lineFromX)
+            dx = lineToX > lineFromX ? 1 : -1;
+        else if (lineToY != lineFromY)
+            dy = lineToY > lineFromY ? 1 : -1;
+
+        do {
+            image.setRGB(x, y, color);
+
+            if (x == lineToX && y == lineToY)
+                break;
+
+            x += dx;
+            y += dy;
+        } while (true);
+
+        updatePatchInfo();
+        notifyPatchesUpdated();
+        if (showBadPatches) {
+            corruptedPatches = CorruptPatch.findBadPatches(image, patchInfo);
+        }
+
+        repaint();
+    }
+
+    private boolean checkLockedRegion(int x, int y) {
+        int oldX = lastPositionX;
+        int oldY = lastPositionY;
+        lastPositionX = x;
+        lastPositionY = y;
+
+        int left = (getWidth() - size.width) / 2;
+        int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
+
+        x = (x - left) / zoom;
+        y = (y - top) / zoom;
+
+        int width = image.getWidth();
+        int height = image.getHeight();
+
+        statusBar.setPointerLocation(Math.max(0, Math.min(x, width - 1)),
+                Math.max(0, Math.min(y, height - 1)));
+
+        boolean previousLock = locked;
+        locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
+
+        boolean previousCursor = showCursor;
+        showCursor =
+                !drawingLine &&
+                        ( ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
+                                ((x > 0 && x < width - 1) && (y == 0 || y == height - 1)) );
+
+        if (locked != previousLock) {
+            repaint();
+        } else if (showCursor || (showCursor != previousCursor)) {
+            Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2,
+                    lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2);
+            clip = clip.union(new Rectangle(oldX - 1 - zoom / 2,
+                    oldY - 1 - zoom / 2, zoom + 2, zoom + 2));
+            repaint(clip);
+        }
+
+        return locked;
+    }
+
+    @Override
+    protected void paintComponent(Graphics g) {
+        int x = (getWidth() - size.width) / 2;
+        int y = helpPanel.getHeight() + (getHeight() - size.height) / 2;
+
+        Graphics2D g2 = (Graphics2D) g.create();
+        g2.setColor(BACK_COLOR);
+        g2.fillRect(0, 0, getWidth(), getHeight());
+
+        g2.translate(x, y);
+        g2.setPaint(texture);
+        g2.fillRect(0, 0, size.width, size.height);
+        g2.scale(zoom, zoom);
+        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+                RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+        g2.drawImage(image, 0, 0, null);
+
+        if (showPatches) {
+            g2.setColor(PATCH_COLOR);
+            for (Rectangle patch : patchInfo.patches) {
+                g2.fillRect(patch.x, patch.y, patch.width, patch.height);
+            }
+            g2.setColor(PATCH_ONEWAY_COLOR);
+            for (Rectangle patch : patchInfo.horizontalPatches) {
+                g2.fillRect(patch.x, patch.y, patch.width, patch.height);
+            }
+            for (Rectangle patch : patchInfo.verticalPatches) {
+                g2.fillRect(patch.x, patch.y, patch.width, patch.height);
+            }
+        }
+
+        if (corruptedPatches != null) {
+            g2.setColor(CORRUPTED_COLOR);
+            g2.setStroke(new BasicStroke(3.0f / zoom));
+            for (Rectangle patch : corruptedPatches) {
+                g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom,
+                        patch.width + 2.0f / zoom, patch.height + 2.0f / zoom,
+                        6.0f / zoom, 6.0f / zoom));
+            }
+        }
+
+        if (showLock && locked) {
+            int width = image.getWidth();
+            int height = image.getHeight();
+
+            g2.setColor(LOCK_COLOR);
+            g2.fillRect(1, 1, width - 2, height - 2);
+
+            g2.setColor(STRIPES_COLOR);
+            g2.translate(1, 1);
+            paintStripes(g2, width - 2, height - 2);
+            g2.translate(-1, -1);
+        }
+
+        g2.dispose();
+
+        if (drawingLine && showDrawingLine) {
+            Graphics cursor = g.create();
+            cursor.setXORMode(Color.WHITE);
+            cursor.setColor(Color.BLACK);
+
+            x = Math.min(lineFromX, lineToX);
+            y = Math.min(lineFromY, lineToY);
+            int w = Math.abs(lineFromX - lineToX) + 1;
+            int h = Math.abs(lineFromY - lineToY) + 1;
+
+            x = x * zoom;
+            y = y * zoom;
+            w = w * zoom;
+            h = h * zoom;
+
+            int left = (getWidth() - size.width) / 2;
+            int top = helpPanel.getHeight() + (getHeight() - size.height)
+                    / 2;
+
+            x += left;
+            y += top;
+
+            cursor.drawRect(x, y, w, h);
+            cursor.dispose();
+        }
+
+        if (showCursor) {
+            Graphics cursor = g.create();
+            cursor.setXORMode(Color.WHITE);
+            cursor.setColor(Color.BLACK);
+            cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom);
+            cursor.dispose();
+        }
+    }
+
+    private void paintStripes(Graphics2D g, int width, int height) {
+        //draws pinstripes at the angle specified in this class
+        //and at the given distance apart
+        Shape oldClip = g.getClip();
+        Area area = new Area(new Rectangle(0, 0, width, height));
+        if(oldClip != null) {
+            area = new Area(oldClip);
+        }
+        area.intersect(new Area(new Rectangle(0,0,width,height)));
+        g.setClip(area);
+
+        g.setStroke(new BasicStroke(STRIPES_WIDTH));
+
+        double hypLength = Math.sqrt((width * width) +
+                (height * height));
+
+        double radians = Math.toRadians(STRIPES_ANGLE);
+        g.rotate(radians);
+
+        double spacing = STRIPES_SPACING;
+        spacing += STRIPES_WIDTH;
+        int numLines = (int)(hypLength / spacing);
+
+        for (int i=0; i<numLines; i++) {
+            double x = i * spacing;
+            Line2D line = new Line2D.Double(x, -hypLength, x, hypLength);
+            g.draw(line);
+        }
+        g.setClip(oldClip);
+    }
+
+    @Override
+    public Dimension getPreferredSize() {
+        return size;
+    }
+
+    void setZoom(int value) {
+        int width = image.getWidth();
+        int height = image.getHeight();
+
+        zoom = value;
+        if (size.height == 0 || (getHeight() - size.height) == 0) {
+            size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
+        } else {
+            size.setSize(width * zoom, height * zoom);
+        }
+
+        if (!size.equals(getSize())) {
+            setSize(size);
+            container.validate();
+            repaint();
+        }
+    }
+
+    void setPatchesVisible(boolean visible) {
+        showPatches = visible;
+        updatePatchInfo();
+        repaint();
+    }
+
+    void setLockVisible(boolean visible) {
+        showLock = visible;
+        repaint();
+    }
+
+    public void setImage(BufferedImage image) {
+        this.image = image;
+    }
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    public PatchInfo getPatchInfo() {
+        return patchInfo;
+    }
+
+    public interface StatusBar {
+        void setPointerLocation(int x, int y);
+    }
+
+    public interface PatchUpdateListener {
+        void patchesUpdated();
+    }
+
+    private final Set<PatchUpdateListener> listeners = new HashSet<PatchUpdateListener>();
+
+    public void addPatchUpdateListener(PatchUpdateListener p) {
+        listeners.add(p);
+    }
+
+    private void notifyPatchesUpdated() {
+        for (PatchUpdateListener p: listeners) {
+            p.patchesUpdated();
+        }
+    }
+}
\ No newline at end of file
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/Pair.java b/draw9patch/src/main/java/com/android/draw9patch/ui/Pair.java
new file mode 100644
index 0000000..bc38671
--- /dev/null
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/Pair.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 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.draw9patch.ui;
+
+public class Pair<E> {
+    E first;
+    E second;
+
+    Pair(E first, E second) {
+        this.first = first;
+        this.second = second;
+    }
+
+    @Override
+    public String toString() {
+        return "Pair[" + first + ", " + second + "]";
+    }
+}
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/PatchInfo.java b/draw9patch/src/main/java/com/android/draw9patch/ui/PatchInfo.java
new file mode 100644
index 0000000..f68740b
--- /dev/null
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/PatchInfo.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2013 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.draw9patch.ui;
+
+import com.android.draw9patch.graphics.GraphicsUtilities;
+
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PatchInfo {
+    /** Color used to indicate stretch regions and padding. */
+    public static final int BLACK_TICK = 0xFF000000;
+
+    /** Color used to indicate layout bounds. */
+    public static final int RED_TICK = 0xFFFF0000;
+
+    /** Areas of the image that are stretchable in both directions. */
+    public final List<Rectangle> patches;
+
+    /** Areas of the image that are not stretchable in either direction. */
+    public final List<Rectangle> fixed;
+
+    /** Areas of image stretchable horizontally. */
+    public final List<Rectangle> horizontalPatches;
+
+    /** Areas of image stretchable vertically. */
+    public final List<Rectangle> verticalPatches;
+
+    public final boolean verticalStartWithPatch;
+    public final boolean horizontalStartWithPatch;
+
+    /** Beginning and end padding in the horizontal direction */
+    public final Pair<Integer> horizontalPadding;
+
+    /** Beginning and end padding in the vertical direction */
+    public final Pair<Integer> verticalPadding;
+
+    private BufferedImage image;
+
+    public PatchInfo(BufferedImage image) {
+        this.image = image;
+
+        int width = image.getWidth();
+        int height = image.getHeight();
+
+        int[] row = GraphicsUtilities.getPixels(image, 0, 0, width, 1, null);
+        int[] column = GraphicsUtilities.getPixels(image, 0, 0, 1, height, null);
+
+        P left = getPatches(column);
+        verticalStartWithPatch = left.startsWithPatch;
+
+        P top = getPatches(row);
+        horizontalStartWithPatch = top.startsWithPatch;
+
+        fixed = getRectangles(left.fixed, top.fixed);
+        patches = getRectangles(left.patches, top.patches);
+
+        if (fixed.size() > 0) {
+            horizontalPatches = getRectangles(left.fixed, top.patches);
+            verticalPatches = getRectangles(left.patches, top.fixed);
+        } else {
+            if (top.fixed.size() > 0) {
+                horizontalPatches = new ArrayList<Rectangle>(0);
+                verticalPatches = getVerticalRectangles(top.fixed);
+            } else if (left.fixed.size() > 0) {
+                horizontalPatches = getHorizontalRectangles(left.fixed);
+                verticalPatches = new ArrayList<Rectangle>(0);
+            } else {
+                horizontalPatches = verticalPatches = new ArrayList<Rectangle>(0);
+            }
+        }
+
+        row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row);
+        column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column);
+
+        top = PatchInfo.getPatches(row);
+        horizontalPadding = getPadding(top.fixed);
+
+        left = PatchInfo.getPatches(column);
+        verticalPadding = getPadding(left.fixed);
+    }
+
+    private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
+        List<Rectangle> rectangles = new ArrayList<Rectangle>();
+        for (Pair<Integer> top : topPairs) {
+            int x = top.first;
+            int width = top.second - top.first;
+
+            rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2));
+        }
+        return rectangles;
+    }
+
+    private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
+        List<Rectangle> rectangles = new ArrayList<Rectangle>();
+        for (Pair<Integer> left : leftPairs) {
+            int y = left.first;
+            int height = left.second - left.first;
+
+            rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height));
+        }
+        return rectangles;
+    }
+
+    private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
+        if (pairs.size() == 0) {
+            return new Pair<Integer>(0, 0);
+        } else if (pairs.size() == 1) {
+            if (pairs.get(0).first == 1) {
+                return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first, 0);
+            } else {
+                return new Pair<Integer>(0, pairs.get(0).second - pairs.get(0).first);
+            }
+        } else {
+            int index = pairs.size() - 1;
+            return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first,
+                    pairs.get(index).second - pairs.get(index).first);
+        }
+    }
+
+    private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
+                                          List<Pair<Integer>> topPairs) {
+        List<Rectangle> rectangles = new ArrayList<Rectangle>();
+        for (Pair<Integer> left : leftPairs) {
+            int y = left.first;
+            int height = left.second - left.first;
+            for (Pair<Integer> top : topPairs) {
+                int x = top.first;
+                int width = top.second - top.first;
+
+                rectangles.add(new Rectangle(x, y, width, height));
+            }
+        }
+        return rectangles;
+    }
+
+    private static class P {
+        public final List<Pair<Integer>> fixed;
+        public final List<Pair<Integer>> patches;
+        public final boolean startsWithPatch;
+
+        private P(List<Pair<Integer>> f, List<Pair<Integer>> p, boolean s) {
+            fixed = f;
+            patches = p;
+            startsWithPatch = s;
+        }
+    }
+
+    private static P getPatches(int[] pixels) {
+        int lastIndex = 1;
+        int lastPixel = pixels[1];
+        boolean first = true;
+        boolean startWithPatch = false;
+
+        List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
+        List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
+
+        for (int i = 1; i < pixels.length - 1; i++) {
+            int pixel = pixels[i];
+            if (pixel != lastPixel) {
+                if (lastPixel == BLACK_TICK) {
+                    if (first) startWithPatch = true;
+                    patches.add(new Pair<Integer>(lastIndex, i));
+                } else {
+                    fixed.add(new Pair<Integer>(lastIndex, i));
+                }
+                first = false;
+
+                lastIndex = i;
+                lastPixel = pixel;
+            }
+        }
+        if (lastPixel == BLACK_TICK) {
+            if (first) startWithPatch = true;
+            patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
+        } else {
+            fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
+        }
+
+        if (patches.size() == 0) {
+            patches.add(new Pair<Integer>(1, pixels.length - 1));
+            startWithPatch = true;
+            fixed.clear();
+        }
+
+        return new P(fixed, patches, startWithPatch);
+    }
+}
diff --git a/draw9patch/src/main/java/com/android/draw9patch/ui/StretchesViewer.java b/draw9patch/src/main/java/com/android/draw9patch/ui/StretchesViewer.java
new file mode 100644
index 0000000..efe0055
--- /dev/null
+++ b/draw9patch/src/main/java/com/android/draw9patch/ui/StretchesViewer.java
@@ -0,0 +1,326 @@
+/*
+ *
+ *  Copyright (C) 2013 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.draw9patch.ui;
+
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.TexturePaint;
+import java.awt.image.BufferedImage;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+public class StretchesViewer extends JPanel {
+    public static final float DEFAULT_SCALE = 2.0f;
+    private static final int MARGIN = 24;
+
+    private final Container container;
+    private final ImageViewer viewer;
+    private final TexturePaint texture;
+
+    private BufferedImage image;
+    private PatchInfo patchInfo;
+
+    private StretchView horizontal;
+    private StretchView vertical;
+    private StretchView both;
+
+    private Dimension size;
+
+    private float horizontalPatchesSum;
+    private float verticalPatchesSum;
+
+    private boolean showPadding;
+
+    StretchesViewer(Container container, ImageViewer viewer, TexturePaint texture) {
+        this.container = container;
+        this.viewer = viewer;
+        this.texture = texture;
+
+        image = viewer.getImage();
+        patchInfo = viewer.getPatchInfo();
+
+        viewer.addPatchUpdateListener(new ImageViewer.PatchUpdateListener() {
+            @Override
+            public void patchesUpdated() {
+                computePatches();
+            }
+        });
+
+        setOpaque(false);
+        setLayout(new GridBagLayout());
+        setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
+
+        horizontal = new StretchView();
+        vertical = new StretchView();
+        both = new StretchView();
+
+        setScale(DEFAULT_SCALE);
+
+        add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
+                GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+        add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
+                GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+        add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
+                GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+    }
+
+    @Override
+    protected void paintComponent(Graphics g) {
+        Graphics2D g2 = (Graphics2D) g.create();
+        g2.setPaint(texture);
+        g2.fillRect(0, 0, getWidth(), getHeight());
+        g2.dispose();
+    }
+
+    void setScale(float scale) {
+        int patchWidth = image.getWidth() - 2;
+        int patchHeight = image.getHeight() - 2;
+
+        int scaledWidth = (int) (patchWidth * scale);
+        int scaledHeight = (int) (patchHeight * scale);
+
+        horizontal.scaledWidth = scaledWidth;
+        vertical.scaledHeight = scaledHeight;
+        both.scaledWidth = scaledWidth;
+        both.scaledHeight = scaledHeight;
+
+        size = new Dimension(scaledWidth, scaledHeight);
+
+        computePatches();
+    }
+
+    void computePatches() {
+        image = viewer.getImage();
+        patchInfo = viewer.getPatchInfo();
+
+        boolean measuredWidth = false;
+        boolean endRow = true;
+
+        int remainderHorizontal = 0;
+        int remainderVertical = 0;
+
+        if (patchInfo.fixed.size() > 0) {
+            int start = patchInfo.fixed.get(0).y;
+            for (Rectangle rect : patchInfo.fixed) {
+                if (rect.y > start) {
+                    endRow = true;
+                    measuredWidth = true;
+                }
+                if (!measuredWidth) {
+                    remainderHorizontal += rect.width;
+                }
+                if (endRow) {
+                    remainderVertical += rect.height;
+                    endRow = false;
+                    start = rect.y;
+                }
+            }
+        }
+
+        horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal;
+        vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal;
+        both.remainderHorizontal = both.scaledWidth - remainderHorizontal;
+
+        horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical;
+        vertical.remainderVertical = vertical.scaledHeight - remainderVertical;
+        both.remainderVertical = both.scaledHeight - remainderVertical;
+
+        horizontalPatchesSum = 0;
+        if (patchInfo.horizontalPatches.size() > 0) {
+            int start = -1;
+            for (Rectangle rect : patchInfo.horizontalPatches) {
+                if (rect.x > start) {
+                    horizontalPatchesSum += rect.width;
+                    start = rect.x;
+                }
+            }
+        } else {
+            int start = -1;
+            for (Rectangle rect : patchInfo.patches) {
+                if (rect.x > start) {
+                    horizontalPatchesSum += rect.width;
+                    start = rect.x;
+                }
+            }
+        }
+
+        verticalPatchesSum = 0;
+        if (patchInfo.verticalPatches.size() > 0) {
+            int start = -1;
+            for (Rectangle rect : patchInfo.verticalPatches) {
+                if (rect.y > start) {
+                    verticalPatchesSum += rect.height;
+                    start = rect.y;
+                }
+            }
+        } else {
+            int start = -1;
+            for (Rectangle rect : patchInfo.patches) {
+                if (rect.y > start) {
+                    verticalPatchesSum += rect.height;
+                    start = rect.y;
+                }
+            }
+        }
+
+        setSize(size);
+        container.validate();
+        repaint();
+    }
+
+    void setPaddingVisible(boolean visible) {
+        showPadding = visible;
+        repaint();
+    }
+
+    private class StretchView extends JComponent {
+        private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f);
+
+        int scaledWidth;
+        int scaledHeight;
+
+        int remainderHorizontal;
+        int remainderVertical;
+
+        StretchView() {
+            scaledWidth = image.getWidth();
+            scaledHeight = image.getHeight();
+        }
+
+        @Override
+        protected void paintComponent(Graphics g) {
+            int x = (getWidth() - scaledWidth) / 2;
+            int y = (getHeight() - scaledHeight) / 2;
+
+            Graphics2D g2 = (Graphics2D) g.create();
+            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+            g.translate(x, y);
+
+            x = 0;
+            y = 0;
+
+            if (patchInfo.patches.size() == 0) {
+                g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
+                g2.dispose();
+                return;
+            }
+
+            int fixedIndex = 0;
+            int horizontalIndex = 0;
+            int verticalIndex = 0;
+            int patchIndex = 0;
+
+            boolean hStretch;
+            boolean vStretch;
+
+            float vWeightSum = 1.0f;
+            float vRemainder = remainderVertical;
+
+            vStretch = patchInfo.verticalStartWithPatch;
+            while (y < scaledHeight - 1) {
+                hStretch = patchInfo.horizontalStartWithPatch;
+
+                int height = 0;
+                float vExtra = 0.0f;
+
+                float hWeightSum = 1.0f;
+                float hRemainder = remainderHorizontal;
+
+                while (x < scaledWidth - 1) {
+                    Rectangle r;
+                    if (!vStretch) {
+                        if (hStretch) {
+                            r = patchInfo.horizontalPatches.get(horizontalIndex++);
+                            float extra = r.width / horizontalPatchesSum;
+                            int width = (int) (extra * hRemainder / hWeightSum);
+                            hWeightSum -= extra;
+                            hRemainder -= width;
+                            g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
+                                    r.x + r.width, r.y + r.height, null);
+                            x += width;
+                        } else {
+                            r = patchInfo.fixed.get(fixedIndex++);
+                            g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
+                                    r.x + r.width, r.y + r.height, null);
+                            x += r.width;
+                        }
+                        height = r.height;
+                    } else {
+                        if (hStretch) {
+                            r = patchInfo.patches.get(patchIndex++);
+                            vExtra = r.height / verticalPatchesSum;
+                            height = (int) (vExtra * vRemainder / vWeightSum);
+                            float extra = r.width / horizontalPatchesSum;
+                            int width = (int) (extra * hRemainder / hWeightSum);
+                            hWeightSum -= extra;
+                            hRemainder -= width;
+                            g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
+                                    r.x + r.width, r.y + r.height, null);
+                            x += width;
+                        } else {
+                            r = patchInfo.verticalPatches.get(verticalIndex++);
+                            vExtra = r.height / verticalPatchesSum;
+                            height = (int) (vExtra * vRemainder / vWeightSum);
+                            g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
+                                    r.x + r.width, r.y + r.height, null);
+                            x += r.width;
+                        }
+
+                    }
+                    hStretch = !hStretch;
+                }
+                x = 0;
+                y += height;
+                if (vStretch) {
+                    vWeightSum -= vExtra;
+                    vRemainder -= height;
+                }
+                vStretch = !vStretch;
+            }
+
+            if (showPadding) {
+                g.setColor(PADDING_COLOR);
+                g.fillRect(patchInfo.horizontalPadding.first,
+                        patchInfo.verticalPadding.first,
+                        scaledWidth - patchInfo.horizontalPadding.first
+                                - patchInfo.horizontalPadding.second,
+                        scaledHeight - patchInfo.verticalPadding.first
+                                - patchInfo.verticalPadding.second);
+            }
+
+            g2.dispose();
+        }
+
+        @Override
+        public Dimension getPreferredSize() {
+            return size;
+        }
+    }
+}
\ No newline at end of file
diff --git a/draw9patch/src/test/java/com/android/draw9patch/ui/PatchInfoTest.java b/draw9patch/src/test/java/com/android/draw9patch/ui/PatchInfoTest.java
new file mode 100644
index 0000000..7ca4fdd
--- /dev/null
+++ b/draw9patch/src/test/java/com/android/draw9patch/ui/PatchInfoTest.java
@@ -0,0 +1,104 @@
+/*
+ *
+ *  Copyright (C) 2013 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.draw9patch.ui;
+
+import junit.framework.TestCase;
+
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+
+public class PatchInfoTest extends TestCase {
+    private BufferedImage createImage(String[] data) {
+        int h = data.length;
+        int w = data[0].length();
+        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+
+        for (int row = 0; row < h; row++) {
+            for (int col = 0; col < w; col++) {
+                char c = data[row].charAt(col);
+                image.setRGB(col, row, c == '*' ? PatchInfo.BLACK_TICK : 0);
+            }
+        }
+        return image;
+    }
+
+    public void testPatchInfo() {
+        BufferedImage image = createImage(new String[] {
+                "0123**6789",
+                "1........*",
+                "*........*",
+                "3........*",
+                "412*****89",
+        });
+        PatchInfo pi = new PatchInfo(image);
+
+        // The left and top patch markers don't begin from the first pixel
+        assertFalse(pi.horizontalStartWithPatch);
+        assertFalse(pi.verticalStartWithPatch);
+
+        // There should be one patch in the middle where the left and top patch markers intersect
+        assertEquals(1, pi.patches.size());
+        assertEquals(new Rectangle(4, 2, 2, 1), pi.patches.get(0));
+
+        // There should be 2 horizontal stretchable areas - area below the top marker but excluding
+        // the main patch
+        assertEquals(2, pi.horizontalPatches.size());
+        assertEquals(new Rectangle(4, 1, 2, 1), pi.horizontalPatches.get(0));
+        assertEquals(new Rectangle(4, 3, 2, 1), pi.horizontalPatches.get(1));
+
+        // Similarly, there should be 2 vertical stretchable areas
+        assertEquals(2, pi.verticalPatches.size());
+        assertEquals(new Rectangle(1, 2, 3, 1), pi.verticalPatches.get(0));
+        assertEquals(new Rectangle(6, 2, 3, 1), pi.verticalPatches.get(1));
+
+        // The should be 4 fixed regions - the regions that don't fall under the patches
+        assertEquals(4, pi.fixed.size());
+
+        // The horizontal padding is described by the bottom bar.
+        // In this case, there is a 2 pixel (pixels 1 & 2) padding at start and 1 pixel (pixel 8)
+        // padding at end
+        assertEquals(2, pi.horizontalPadding.first.intValue());
+        assertEquals(1, pi.horizontalPadding.second.intValue());
+
+        // The vertical padding is described by the bar at the right.
+        // In this case, there is no padding as the content area matches the image area
+        assertEquals(0, pi.verticalPadding.first.intValue());
+        assertEquals(0, pi.verticalPadding.second.intValue());
+    }
+
+    public void testPadding() {
+        BufferedImage image = createImage(new String[] {
+                "0123**6789",
+                "1.........",
+                "2.........",
+                "3........*",
+                "4........*",
+                "5***456789",
+        });
+        PatchInfo pi = new PatchInfo(image);
+
+        // 0 pixel padding at start and 5 pixel padding at the end (pixels 4 through 8 inclusive)
+        assertEquals(0, pi.horizontalPadding.first.intValue());
+        assertEquals(5, pi.horizontalPadding.second.intValue());
+
+        // 2 pixel padding at the start and 0 at the end
+        assertEquals(2, pi.verticalPadding.first.intValue());
+        assertEquals(0, pi.verticalPadding.second.intValue());
+    }
+}