OSDN Git Service

Support for included and merged views
authorTor Norbye <tnorbye@google.com>
Wed, 22 Dec 2010 23:44:00 +0000 (15:44 -0800)
committerTor Norbye <tnorbye@google.com>
Thu, 6 Jan 2011 22:02:16 +0000 (14:02 -0800)
We have support in the layout library for handling included views
where the include tag is associated with the root elements rendered in
that included view.

However, there are various scenarios where this is not adequate:
1) including <merge> views (see issue 13288)
2) older layout libraries

This changeset fixes the above scenarios, as well as an additional
scenario (where rendering included content in an outer layout did not
work properly if the included elements were not at the root level).

It does this by moving the CanvasViewInfo construction into a set of
factory methods which handle various different hierarchy types of
ViewInfos. ViewInfos with null keys at the top are handled one way;
ViewInfo subtrees that introduce null keys further down are handled
another, and it attempts to match up elements in the UiViewElementNode
hierarchy with ViewInfo objects. If it cannot match them one to one,
it will create a single "bounding box" view containing all unmatched
ViewInfos. This will for example produce a single box out of included
<merge> views.

This changeset also handles multiple includes. The "overlay mask"
could earlier only handle a single include whereas the rewritten code
can handle multiple "holes" in the mask by computing the set of
subrectangles remaining after punching out the include holes.

Change-Id: I163413c7fc301f25c4523b0ee03690f983a05322

eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlay.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlayTest.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeFactoryTest.java

index 73ef215..e1307c3 100644 (file)
@@ -23,6 +23,7 @@ breadcrumb
 builtin
 callback
 callbacks
+carlo
 checkbox
 classloader
 classpath
@@ -86,6 +87,7 @@ inline
 instanceof
 instantiatable
 int
+javac
 javadoc
 layoutlib
 leaky
@@ -103,6 +105,7 @@ macs
 marquee
 metadata
 min
+monte
 multi
 multimap
 multimaps
@@ -147,6 +150,7 @@ scrollbars
 sdk
 se
 searchable
+selectable
 semi
 serializer
 settable
index da408fa..89745a6 100644 (file)
@@ -40,6 +40,7 @@ public class LayoutConstants {
     public static final String LIST_VIEW = "ListView";                  //$NON-NLS-1$
     public static final String GALLERY = "Gallery";                     //$NON-NLS-1$
     public static final String GRID_VIEW = "GridView";                  //$NON-NLS-1$
+    public static final String SCROLL_VIEW = "ScrollView";              //$NON-NLS-1$
     public static final String EXPANDABLE_LIST_VIEW = "ExpandableListView";//$NON-NLS-1$
 
     public static final String ATTR_TEXT = "text";                      //$NON-NLS-1$
index 0bab655..84647bd 100755 (executable)
@@ -17,6 +17,7 @@
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
 import com.android.ide.common.api.Rect;
+import com.android.ide.common.rendering.api.Capability;
 import com.android.ide.common.rendering.api.ViewInfo;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser;
@@ -33,6 +34,8 @@ import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
 import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
 
 /**
  * Maps a {@link ViewInfo} in a structure more adapted to our needs.
@@ -74,72 +77,24 @@ public class CanvasViewInfo implements IPropertySource {
     private boolean mExploded;
 
     /**
-     * Constructs a {@link CanvasViewInfo} hierarchy based on a given {@link ViewInfo}
-     * hierarchy. This call is recursive and builds a full tree.
-     *
-     * @param viewInfo The root of the {@link ViewInfo} hierarchy.
+     * Constructs a {@link CanvasViewInfo} initialized with the given initial values.
      */
-    public CanvasViewInfo(ViewInfo viewInfo) {
-        this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/);
-    }
-
-    private CanvasViewInfo(ViewInfo viewInfo, CanvasViewInfo parent,
-            int parentX, int parentY) {
+    private CanvasViewInfo(CanvasViewInfo parent, String name,
+            Object viewObject, UiViewElementNode node, Rectangle absRect,
+            Rectangle selectionRect) {
         mParent = parent;
-        mName = viewInfo.getClassName();
-        mViewObject = viewInfo.getViewObject();
-
-        // The ViewInfo#getViewKey() method returns a cookie uniquely identifying the object
-        // they represent on this side of the API.
-        // In this case, the parser is guaranteed to be an UiElementPullParser, which creates
-        // cookies that are of type UiViewElementNode.
-        // We'll simply crash if the type is not right, as this is not supposed to happen
-        // and nothing could work if there's a type mismatch.
-        mUiViewNode  = (UiViewElementNode) viewInfo.getCookie();
-
-        int x = viewInfo.getLeft();
-        int y = viewInfo.getTop();
-        int w = viewInfo.getRight() - x;
-        int h = viewInfo.getBottom() - y;
-
-        if (parent != null) {
-            x += parentX;
-            y += parentY;
-        }
-
-        mAbsRect = new Rectangle(x, y, w - 1, h - 1);
-
-        if (viewInfo.getChildren() != null) {
-            for (ViewInfo child : viewInfo.getChildren()) {
-                // Only use children which have a ViewKey of the correct type.
-                // We can't interact with those when they have a null key or
-                // an incompatible type.
-                if (child.getCookie() instanceof UiViewElementNode) {
-                    mChildren.add(new CanvasViewInfo(child, this, x, y));
-                }
-            }
-        }
-
-        // adjust selection bounds for views which are too small to select
-
-        if (w < SELECTION_MIN_SIZE) {
-            int d = (SELECTION_MIN_SIZE - w) / 2;
-            x -= d;
-            w += SELECTION_MIN_SIZE - w;
-        }
-
-        if (h < SELECTION_MIN_SIZE) {
-            int d = (SELECTION_MIN_SIZE - h) / 2;
-            y -= d;
-            h += SELECTION_MIN_SIZE - h;
-        }
-
-        mSelectionRect = new Rectangle(x, y, w - 1, h - 1);
+        mName = name;
+        mViewObject = viewObject;
+        mUiViewNode  = node;
+        mAbsRect = absRect;
+        mSelectionRect = selectionRect;
     }
 
     /**
      * Returns the original {@link ViewInfo} bounds in absolute coordinates
      * over the whole graphic.
+     *
+     * @return the bounding box in absolute coordinates
      */
     public Rectangle getAbsRect() {
         return mAbsRect;
@@ -168,6 +123,8 @@ public class CanvasViewInfo implements IPropertySource {
     /**
      * Returns the parent {@link CanvasViewInfo}.
      * It is null for the root and non-null for children.
+     *
+     * @return the parent {@link CanvasViewInfo}, which can be null
      */
     public CanvasViewInfo getParent() {
         return mParent;
@@ -177,8 +134,10 @@ public class CanvasViewInfo implements IPropertySource {
      * Returns the list of children of this {@link CanvasViewInfo}.
      * The list is never null. It can be empty.
      * By contract, this.getChildren().get(0..n-1).getParent() == this.
+     *
+     * @return the children, never null
      */
-    public ArrayList<CanvasViewInfo> getChildren() {
+    public List<CanvasViewInfo> getChildren() {
         return mChildren;
     }
 
@@ -186,6 +145,9 @@ public class CanvasViewInfo implements IPropertySource {
      * Returns true if the specific {@link CanvasViewInfo} is a parent
      * of this {@link CanvasViewInfo}. It can be a direct parent or any
      * grand-parent higher in the hierarchy.
+     *
+     * @param potentialParent the view info to check
+     * @return true if the given info is a parent of this view
      */
     public boolean isParent(CanvasViewInfo potentialParent) {
         if (potentialParent == null) {
@@ -206,6 +168,8 @@ public class CanvasViewInfo implements IPropertySource {
      * Could be null, although unlikely.
      * Experience shows this is the full qualified Java name of the View.
      *
+     * @return the name of the view info, or null
+     *
      * @see ViewInfo#getClassName()
      */
     public String getName() {
@@ -411,4 +375,261 @@ public class CanvasViewInfo implements IPropertySource {
 
         return null;
     }
+
+    // ---- Factory functionality ----
+
+    /**
+     * Creates a new {@link CanvasViewInfo} hierarchy based on the given {@link ViewInfo}
+     * hierarchy. Note that this will not necessarily create one {@link CanvasViewInfo}
+     * for each {@link ViewInfo}. It will generally only create {@link CanvasViewInfo}
+     * objects for {@link ViewInfo} objects that contain a reference to an
+     * {@link UiViewElementNode}, meaning that it corresponds to an element in the XML
+     * file for this layout file. This is not always the case, such as in the following
+     * scenarios:
+     * <ul>
+     * <li>we link to other layouts with {@code <include>}
+     * <li>the current view is rendered within another view ("Show Included In") such that
+     * the outer file does not correspond to elements in the current included XML layout
+     * <li>on older platforms that don't support {@link Capability#EMBEDDED_LAYOUT} there
+     * is no reference to the {@code <include>} tag
+     * <li>with the {@code <merge>} tag we don't get a reference to the corresponding
+     * element
+     * <ul>
+     * <p>
+     * This method will build up a set of {@link CanvasViewInfo} that corresponds to the
+     * actual <b>selectable</b> views (which are also shown in the Outline).
+     *
+     * @param root the root {@link ViewInfo} to build from
+     * @return a {@link CanvasViewInfo} hierarchy
+     */
+    public static CanvasViewInfo create(ViewInfo root) {
+        if (root.getCookie() == null) {
+            // Special case: If the root-most view does not have a view cookie,
+            // then we are rendering some outer layout surrounding this layout, and in
+            // that case we must search down the hierarchy for the (possibly multiple)
+            // sub-roots that correspond to elements in this layout, and place them inside
+            // an outer view that has no node. In the outline this item will be used to
+            // show the inclusion-context.
+            CanvasViewInfo rootView = createView(null, root, 0, 0);
+            addKeyedSubtrees(rootView, root, 0, 0);
+            return rootView;
+        } else {
+            // We have a view key at the top, so just go and create {@link CanvasViewInfo}
+            // objects for each {@link ViewInfo} until we run into a null key.
+            return addKeyedSubtrees(null, root, 0, 0);
+        }
+    }
+
+    /** Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse */
+    private static CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+            int parentY) {
+        Object cookie = root.getCookie();
+        UiViewElementNode node = null;
+        if (cookie instanceof UiViewElementNode) {
+            node = (UiViewElementNode) cookie;
+        }
+
+        return createView(parent, root, parentX, parentY, node);
+    }
+
+    /**
+     * Creates a {@link CanvasViewInfo} for a given {@link ViewInfo} but does not recurse.
+     * This method specifies an explicit {@link UiViewElementNode} to use rather than
+     * relying on the view cookie in the info object.
+     */
+    private static CanvasViewInfo createView(CanvasViewInfo parent, ViewInfo root, int parentX,
+            int parentY, UiViewElementNode node) {
+
+        int x = root.getLeft();
+        int y = root.getTop();
+        int w = root.getRight() - x;
+        int h = root.getBottom() - y;
+
+        x += parentX;
+        y += parentY;
+
+        Rectangle absRect = new Rectangle(x, y, w - 1, h - 1);
+
+        if (w < SELECTION_MIN_SIZE) {
+            int d = (SELECTION_MIN_SIZE - w) / 2;
+            x -= d;
+            w += SELECTION_MIN_SIZE - w;
+        }
+
+        if (h < SELECTION_MIN_SIZE) {
+            int d = (SELECTION_MIN_SIZE - h) / 2;
+            y -= d;
+            h += SELECTION_MIN_SIZE - h;
+        }
+
+        Rectangle selectionRect = new Rectangle(x, y, w - 1, h - 1);
+
+        return new CanvasViewInfo(parent, root.getClassName(), root.getViewObject(), node, absRect,
+                selectionRect);
+    }
+
+    /** Create a subtree recursively until you run out of keys */
+    private static CanvasViewInfo createSubtree(CanvasViewInfo parent, ViewInfo viewInfo,
+            int parentX, int parentY) {
+        assert viewInfo.getCookie() != null;
+
+        CanvasViewInfo view = createView(parent, viewInfo, parentX, parentY);
+
+        // Process children:
+        parentX += viewInfo.getLeft();
+        parentY += viewInfo.getTop();
+
+        // See if we have any missing keys at this level
+        int missingNodes = 0;
+        List<ViewInfo> children = viewInfo.getChildren();
+        for (ViewInfo child : children) {
+            // Only use children which have a ViewKey of the correct type.
+            // We can't interact with those when they have a null key or
+            // an incompatible type.
+            Object cookie = child.getCookie();
+            if (!(cookie instanceof UiViewElementNode)) {
+                missingNodes++;
+            }
+        }
+
+        if (missingNodes == 0) {
+            // No missing nodes; this is the normal case, and we can just continue to
+            // recursively add our children
+            for (ViewInfo child : children) {
+                CanvasViewInfo childView = createSubtree(view, child, parentX, parentY);
+                view.addChild(childView);
+            }
+        } else {
+            // We don't have keys for one or more of the ViewInfos. There are many
+            // possible causes: we are on an SDK platform that does not support
+            // embedded_layout rendering, or we are including a view with a <merge>
+            // as the root element.
+
+            String containerName = view.getUiViewNode().getDescriptor().getXmlLocalName();
+            if (containerName.equals(LayoutDescriptors.VIEW_INCLUDE)) {
+                // This is expected -- we don't WANT to get node keys for the content
+                // of an include since it's in a different file and should be treated
+                // as a single unit that cannot be edited (hence, no CanvasViewInfo
+                // children)
+            } else {
+                // We are getting children with null keys where we don't expect it;
+                // this usually means that we are dealing with an Android platform
+                // that does not support {@link Capability#EMBEDDED_LAYOUT}, or
+                // that there are <merge> tags which are doing surprising things
+                // to the view hierarchy
+                LinkedList<UiViewElementNode> unused = new LinkedList<UiViewElementNode>();
+                for (UiElementNode child : view.getUiViewNode().getUiChildren()) {
+                    if (child instanceof UiViewElementNode) {
+                        unused.addLast((UiViewElementNode) child);
+                    }
+                }
+                for (ViewInfo child : children) {
+                    Object cookie = child.getCookie();
+                    if (cookie != null) {
+                        unused.remove(cookie);
+                    }
+                }
+                if (unused.size() > 0) {
+                    if (unused.size() == missingNodes) {
+                        // The number of unmatched elements and ViewInfos are identical;
+                        // it's very likely that they match one to one, so just use these
+                        for (ViewInfo child : children) {
+                            if (child.getCookie() == null) {
+                                // Only create a flat (non-recursive) view
+                                CanvasViewInfo childView = createView(view, child, parentX,
+                                        parentY, unused.removeFirst());
+                                view.addChild(childView);
+                            } else {
+                                CanvasViewInfo childView = createSubtree(view, child, parentX,
+                                        parentY);
+                                view.addChild(childView);
+                            }
+                        }
+                    } else {
+                        // We have an uneven match. In this case we might be dealing
+                        // with <merge> etc.
+                        // We have no way to associate elements back with the
+                        // corresponding <include> tags if there are more than one of
+                        // them. That's not a huge tragedy since visually you are not
+                        // allowed to edit these anyway; we just need to make a visual
+                        // block for these for selection and outline purposes.
+                        UiViewElementNode reference = unused.get(0);
+                        addBoundingView(view, children, reference, parentX, parentY);
+                    }
+                }
+            }
+        }
+
+        return view;
+    }
+
+    /**
+     * Add a single bounding view for all the non-keyed children with dimensions that span
+     * the bounding rectangle of all these children, and associate it with the given node
+     * reference. Keyed children are added in the normal way.
+     */
+    private static void addBoundingView(CanvasViewInfo parentView, List<ViewInfo> children,
+            UiViewElementNode reference, int parentX, int parentY) {
+        Rectangle absRect = null;
+        int insertIndex = -1;
+        for (int index = 0, size = children.size(); index < size; index++) {
+            ViewInfo child = children.get(index);
+            if (child.getCookie() == null) {
+                int x = child.getLeft();
+                int y = child.getTop();
+                int width = child.getRight() - x;
+                int height = child.getBottom() - y;
+                Rectangle rect = new Rectangle(x, y, width, height);
+                if (absRect == null) {
+                    absRect = rect;
+                    insertIndex = index;
+                } else {
+                    absRect = absRect.union(rect);
+                }
+            } else {
+                CanvasViewInfo childView = createSubtree(parentView, child, parentX, parentY);
+                parentView.addChild(childView);
+            }
+        }
+        if (absRect != null) {
+            absRect.x += parentX;
+            absRect.y += parentY;
+            String name = reference.getDescriptor().getXmlLocalName();
+            CanvasViewInfo childView = new CanvasViewInfo(parentView, name, null, reference,
+                    absRect, absRect);
+            parentView.addChild(childView, insertIndex);
+        }
+    }
+
+    /** Search for a subtree with valid keys and add those subtrees */
+    private static CanvasViewInfo addKeyedSubtrees(CanvasViewInfo parent, ViewInfo viewInfo,
+            int parentX, int parentY) {
+        if (viewInfo.getCookie() != null) {
+            CanvasViewInfo subtree = createSubtree(parent, viewInfo, parentX, parentY);
+            if (parent != null) {
+                parent.mChildren.add(subtree);
+            }
+            return subtree;
+        } else {
+            for (ViewInfo child : viewInfo.getChildren()) {
+                addKeyedSubtrees(parent, child, parentX + viewInfo.getLeft(), parentY
+                        + viewInfo.getTop());
+            }
+
+            return null;
+        }
+    }
+
+    /** Adds the given {@link CanvasViewInfo} as a new last child of this view */
+    private void addChild(CanvasViewInfo child) {
+        mChildren.add(child);
+    }
+
+    /** Adds the given {@link CanvasViewInfo} as a new child at the given index */
+    private void addChild(CanvasViewInfo child, int index) {
+        if (index < 0) {
+            index = mChildren.size();
+        }
+        mChildren.add(index, child);
+    }
 }
index d7bc7d5..750f7e4 100644 (file)
@@ -16,6 +16,7 @@
 
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
+import static com.android.ide.common.layout.LayoutConstants.SCROLL_VIEW;
 import static com.android.ide.eclipse.adt.AndroidConstants.ANDROID_PKG;
 import static com.android.sdklib.resources.Density.DEFAULT_DENSITY;
 
@@ -1591,7 +1592,7 @@ public class GraphicalEditorPart extends EditorPart
             // FIXME set the rendering mode using ViewRule or something.
             List<UiElementNode> children = model.getUiChildren();
             if (children.size() > 0 &&
-                    children.get(0).getDescriptor().getXmlLocalName().equals("ScrollView")) {
+                    children.get(0).getDescriptor().getXmlLocalName().equals(SCROLL_VIEW)) {
                 renderingMode = RenderingMode.V_SCROLL;
             }
         }
@@ -1616,7 +1617,13 @@ public class GraphicalEditorPart extends EditorPart
         // set the Image Overlay as the image factory.
         params.setImageFactory(getCanvasControl().getImageOverlay());
 
-        return layoutLib.createSession(params);
+        try {
+            return layoutLib.createSession(params);
+        } catch (RuntimeException t) {
+            // Exceptions from the bridge
+            displayError(t.getLocalizedMessage());
+            throw t;
+        }
     }
 
     /**
index 7b95e70..e201c14 100644 (file)
 
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
+import com.android.sdklib.annotations.VisibleForTesting;
+
 import org.eclipse.swt.SWT;
 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.Rectangle;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -48,8 +51,9 @@ public class IncludeOverlay extends Overlay {
 
     @Override
     public void paint(GC gc) {
-        List<CanvasViewInfo> included = mCanvas.getViewHierarchy().getIncluded();
-        if (included == null || included.size() != 1) {
+        ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+        List<Rectangle> includedBounds = viewHierarchy.getIncludedBounds();
+        if (includedBounds == null || includedBounds.size() == 0) {
             // We don't support multiple included children yet. When that works,
             // this code should use a BSP tree to figure out which regions to paint
             // to leave holes in the mask.
@@ -60,52 +64,87 @@ public class IncludeOverlay extends Overlay {
         if (image == null) {
             return;
         }
-        ImageData data = image.getImageData();
-
-        Rectangle hole = included.get(0).getAbsRect();
 
         int oldAlpha = gc.getAlpha();
         gc.setAlpha(MASK_TRANSPARENCY);
         Color bg = gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
         gc.setBackground(bg);
 
-        ControlPoint topLeft = LayoutPoint.create(mCanvas, hole.x, hole.y).toControl();
-        ControlPoint bottomRight = LayoutPoint.create(mCanvas, hole.x + hole.width,
-                hole.y + hole.height).toControl();
-        CanvasTransform hi = mCanvas.getHorizontalTransform();
-        CanvasTransform vi = mCanvas.getVerticalTransform();
-        int deltaX = hi.translate(0);
-        int deltaY = vi.translate(0);
-        int x1 = topLeft.x;
-        int y1 = topLeft.y;
-        int x2 = bottomRight.x;
-        int y2 = bottomRight.y;
-        int width = data.width;
-        int height = data.height;
-
-        width = hi.getScalledImgSize();
-        height = vi.getScalledImgSize();
-
-        if (y1 > deltaX) {
-            // Top
-            gc.fillRectangle(deltaX, deltaY, width, y1 - deltaY);
-        }
-
-        if (y2 < height) {
-            // Bottom
-            gc.fillRectangle(deltaX, y2, width, height - y2 + deltaY);
+        CanvasViewInfo root = viewHierarchy.getRoot();
+        Rectangle whole = root.getAbsRect();
+        whole = new Rectangle(whole.x, whole.y, whole.width + 1, whole.height + 1);
+        Collection<Rectangle> masks = subtractRectangles(whole, includedBounds);
+
+        for (Rectangle mask : masks) {
+            ControlPoint topLeft = LayoutPoint.create(mCanvas, mask.x, mask.y).toControl();
+            ControlPoint bottomRight = LayoutPoint.create(mCanvas, mask.x + mask.width,
+                    mask.y + mask.height).toControl();
+            int x1 = topLeft.x;
+            int y1 = topLeft.y;
+            int x2 = bottomRight.x;
+            int y2 = bottomRight.y;
+
+            gc.fillRectangle(x1, y1, x2 - x1, y2 - y1);
         }
 
-        if (x1 > deltaX) {
-            // Left
-            gc.fillRectangle(deltaX, y1, x1 - deltaX, y2 - y1);
-        }
+        gc.setAlpha(oldAlpha);
+    }
 
-        if (x2 < width) {
-            // Right
-            gc.fillRectangle(x2, y1, width - x2 + deltaX, y2 - y1);
+    /**
+     * Given a Rectangle, remove holes from it (specified as a collection of Rectangles) such
+     * that the result is a list of rectangles that cover everything that is not a hole.
+     *
+     * @param rectangle the rectangle to subtract from
+     * @param holes the holes to subtract from the rectangle
+     * @return a list of sub rectangles that remain after subtracting out the given list of holes
+     */
+    @VisibleForTesting
+    static Collection<Rectangle> subtractRectangles(
+            Rectangle rectangle, Collection<Rectangle> holes) {
+        List<Rectangle> result = new ArrayList<Rectangle>();
+        result.add(rectangle);
+
+        for (Rectangle hole : holes) {
+            List<Rectangle> tempResult = new ArrayList<Rectangle>();
+            for (Rectangle r : result) {
+                if (hole.intersects(r)) {
+                    // Clip the hole to fit the rectangle bounds
+                    Rectangle h = hole.intersection(r);
+
+                    // Split the rectangle
+
+                    // Above (includes the NW and NE corners)
+                    if (h.y > r.y) {
+                        tempResult.add(new Rectangle(r.x, r.y, r.width, h.y - r.y));
+                    }
+
+                    // Left (not including corners)
+                    if (h.x > r.x) {
+                        tempResult.add(new Rectangle(r.x, h.y, h.x - r.x, h.height));
+                    }
+
+                    int hx2 = h.x + h.width;
+                    int hy2 = h.y + h.height;
+                    int rx2 = r.x + r.width;
+                    int ry2 = r.y + r.height;
+
+                    // Below (includes the SW and SE corners)
+                    if (hy2 < ry2) {
+                        tempResult.add(new Rectangle(r.x, hy2, r.width, ry2 - hy2));
+                    }
+
+                    // Right (not including corners)
+                    if (hx2 < rx2) {
+                        tempResult.add(new Rectangle(hx2, h.y, rx2 - hx2, h.height));
+                    }
+                } else {
+                    tempResult.add(r);
+                }
+            }
+
+            result = tempResult;
         }
 
-        gc.setAlpha(oldAlpha);
+        return result;
     }
 }
index 61ae3a8..0b6c969 100755 (executable)
@@ -389,7 +389,7 @@ public class OutlinePage extends ContentOutlinePage
                 }
             }
             if (element instanceof CanvasViewInfo) {
-                ArrayList<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
+                List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
                 if (children != null) {
                     return children.toArray();
                 }
@@ -406,7 +406,7 @@ public class OutlinePage extends ContentOutlinePage
 
         public boolean hasChildren(Object element) {
             if (element instanceof CanvasViewInfo) {
-                ArrayList<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
+                List<CanvasViewInfo> children = ((CanvasViewInfo) element).getChildren();
                 if (children != null) {
                     return children.size() > 0;
                 }
index eb1dc15..ac60d80 100644 (file)
@@ -95,9 +95,9 @@ public class ViewHierarchy {
     private boolean mExplodedParents;
 
     /**
-     * List of included view infos in the current view hierarchy.
+     * Bounds of included views in the current view hierarchy when rendered in other context
      */
-    private List<CanvasViewInfo> mIncluded;
+    private List<Rectangle> mIncludedBounds;
 
     /** The render session for the current view hierarchy */
     private RenderSession mSession;
@@ -137,14 +137,14 @@ public class ViewHierarchy {
         mSession = session;
         mIsResultValid = (session != null && session.getResult().isSuccess());
         mExplodedParents = false;
-        mIncluded = null;
+        mIncludedBounds = null;
 
         if (mIsResultValid && session != null) {
             ViewInfo root = session.getRootView();
             if (root == null) {
                 mLastValidViewInfoRoot = null;
             } else {
-                mLastValidViewInfoRoot = new CanvasViewInfo(session.getRootView());
+                mLastValidViewInfoRoot = CanvasViewInfo.create(root);
             }
 
             updateNodeProxies(mLastValidViewInfoRoot, null);
@@ -183,12 +183,12 @@ public class ViewHierarchy {
         if (key != null) {
             mCanvas.getNodeFactory().create(vi);
 
-            if (key != null && parentKey == null && vi.getParent() != null) {
+            if (parentKey == null && vi.getParent() != null) {
                 // This is an included view root
-                if (mIncluded == null) {
-                    mIncluded = new ArrayList<CanvasViewInfo>();
+                if (mIncludedBounds == null) {
+                    mIncludedBounds = new ArrayList<Rectangle>();
                 }
-                mIncluded.add(vi);
+                mIncludedBounds.add(vi.getAbsRect());
             }
         }
 
@@ -243,8 +243,8 @@ public class ViewHierarchy {
     }
 
     /**
-     * Returns true when the last {@link #setResult} provided a valid
-     * {@link LayoutScene}.
+     * Returns true when the last {@link #setSession} provided a valid
+     * {@link RenderSession}.
      * <p/>
      * When false this means the canvas is displaying an out-dated result image & bounds and some
      * features should be disabled accordingly such a drag'n'drop.
@@ -596,12 +596,13 @@ public class ViewHierarchy {
     }
 
     /**
-     * Returns the list of included views in the current view hierarchy. Can be null
+     * Returns the list of bounds for included views in the current view hierarchy. Can be null
      * when there are no included views.
      *
-     * @return a list of included views, or null
+     * @return a list of included view bounds, or null
      */
-    public List<CanvasViewInfo> getIncluded() {
-        return mIncluded;
+    public List<Rectangle> getIncludedBounds() {
+        return mIncludedBounds;
     }
+
 }
index 7184c85..c6bdae5 100644 (file)
@@ -137,7 +137,7 @@ public class DescriptorsUtilsTest extends TestCase {
                 DescriptorsUtils.getFreeWidgetId(uiRoot, "LinearLayout"));
     }
 
-    private static ElementDescriptor createDesc(String name, String fqn, boolean hasChildren) {
+    private static ViewElementDescriptor createDesc(String name, String fqn, boolean hasChildren) {
         if (hasChildren) {
             return new ViewElementDescriptor(name, name, fqn, "", "", new AttributeDescriptor[0],
                     new AttributeDescriptor[0], new ElementDescriptor[1], false);
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfoTest.java
new file mode 100644 (file)
index 0000000..657a7f8
--- /dev/null
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import com.android.ide.common.rendering.api.Capability;
+import com.android.ide.common.rendering.api.ViewInfo;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.Arrays;
+
+import junit.framework.TestCase;
+
+public class CanvasViewInfoTest extends TestCase {
+
+    private static ViewElementDescriptor createDesc(String name, String fqn, boolean hasChildren) {
+        if (hasChildren) {
+            return new ViewElementDescriptor(name, name, fqn, "", "", new AttributeDescriptor[0],
+                    new AttributeDescriptor[0], new ElementDescriptor[1], false);
+        } else {
+            return new ViewElementDescriptor(name, fqn);
+        }
+    }
+
+    private static UiViewElementNode createNode(UiViewElementNode parent, String fqn,
+            boolean hasChildren) {
+        String name = fqn.substring(fqn.lastIndexOf('.') + 1);
+        ViewElementDescriptor descriptor = createDesc(name, fqn, hasChildren);
+        if (parent != null) {
+            return (UiViewElementNode) parent.appendNewUiChild(descriptor);
+        } else {
+            return new UiViewElementNode(descriptor);
+        }
+    }
+
+    private static UiViewElementNode createNode(String fqn, boolean hasChildren) {
+        return createNode(null, fqn, hasChildren);
+    }
+
+    public void testNormalCreate() throws Exception {
+        // Normal view hierarchy, no null keys anywhere
+
+        UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true);
+        ViewInfo root = new ViewInfo("LinearLayout", rootNode, 10, 10, 100, 100);
+        UiViewElementNode child1Node = createNode(rootNode, "android.widget.Button", false);
+        ViewInfo child1 = new ViewInfo("Button", child1Node, 0, 0, 50, 20);
+        UiViewElementNode child2Node = createNode(rootNode, "android.widget.Button", false);
+        ViewInfo child2 = new ViewInfo("Button", child2Node, 0, 20, 70, 25);
+        root.setChildren(Arrays.asList(child1, child2));
+
+        CanvasViewInfo rootView = CanvasViewInfo.create(root);
+        assertNotNull(rootView);
+        assertEquals("LinearLayout", rootView.getName());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect());
+        assertNull(rootView.getParent());
+        assertSame(rootView.getUiViewNode(), rootNode);
+        assertEquals(2, rootView.getChildren().size());
+        CanvasViewInfo childView1 = rootView.getChildren().get(0);
+        CanvasViewInfo childView2 = rootView.getChildren().get(1);
+
+        assertEquals("Button", childView1.getName());
+        assertSame(rootView, childView1.getParent());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect());
+        assertSame(childView1.getUiViewNode(), child1Node);
+
+        assertEquals("Button", childView2.getName());
+        assertSame(rootView, childView2.getParent());
+        assertEquals(new Rectangle(10, 30, 69, 4), childView2.getAbsRect());
+        assertEquals(new Rectangle(10, 30, 69, 5), childView2.getSelectionRect());
+        assertSame(childView2.getUiViewNode(), child2Node);
+    }
+
+    public void testShowIn() throws Exception {
+        // Test rendering of "Show Included In" (included content rendered
+        // within an outer content that has null keys)
+
+        ViewInfo root = new ViewInfo("LinearLayout", null, 10, 10, 100, 100);
+        ViewInfo child1 = new ViewInfo("CheckBox", null, 0, 0, 50, 20);
+        UiViewElementNode child2Node = createNode("android.widget.RelativeLayout", true);
+        ViewInfo child2 = new ViewInfo("RelativeLayout", child2Node, 0, 20, 70, 25);
+        root.setChildren(Arrays.asList(child1, child2));
+        UiViewElementNode child21Node = createNode("android.widget.Button", false);
+        ViewInfo child21 = new ViewInfo("RadioButton", child21Node, 0, 20, 70, 25);
+        child2.setChildren(Arrays.asList(child21));
+
+        CanvasViewInfo rootView = CanvasViewInfo.create(root);
+        assertNotNull(rootView);
+        assertEquals("LinearLayout", rootView.getName());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect());
+        assertNull(rootView.getParent());
+        assertNull(rootView.getUiViewNode());
+        assertEquals(1, rootView.getChildren().size());
+        CanvasViewInfo includedView = rootView.getChildren().get(0);
+
+        assertEquals("RelativeLayout", includedView.getName());
+        assertSame(rootView, includedView.getParent());
+        assertEquals(new Rectangle(10, 30, 69, 4), includedView.getAbsRect());
+        assertEquals(new Rectangle(10, 30, 69, 5), includedView.getSelectionRect());
+        assertSame(includedView.getUiViewNode(), child2Node);
+
+        CanvasViewInfo grandChild = includedView.getChildren().get(0);
+        assertNotNull(grandChild);
+        assertEquals("RadioButton", grandChild.getName());
+        assertSame(child21Node, grandChild.getUiViewNode());
+        assertEquals(new Rectangle(10, 50, 69, 4), grandChild.getAbsRect());
+        assertEquals(new Rectangle(10, 50, 69, 5), grandChild.getSelectionRect());
+    }
+
+    public void testIncludeTag() throws Exception {
+        // Test rendering of included views on layoutlib 5+ (e.g. has <include> tag)
+
+        UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true);
+        ViewInfo root = new ViewInfo("LinearLayout", rootNode, 10, 10, 100, 100);
+        UiViewElementNode child1Node = createNode(rootNode, "android.widget.Button", false);
+        ViewInfo child1 = new ViewInfo("CheckBox", child1Node, 0, 0, 50, 20);
+        UiViewElementNode child2Node = createNode(rootNode, "include", true);
+        ViewInfo child2 = new ViewInfo("RelativeLayout", child2Node, 0, 20, 70, 25);
+        root.setChildren(Arrays.asList(child1, child2));
+        ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25);
+        child2.setChildren(Arrays.asList(child21));
+
+        CanvasViewInfo rootView = CanvasViewInfo.create(root);
+        assertNotNull(rootView);
+        assertEquals("LinearLayout", rootView.getName());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect());
+        assertNull(rootView.getParent());
+        assertSame(rootNode, rootView.getUiViewNode());
+        assertEquals(2, rootView.getChildren().size());
+
+        CanvasViewInfo childView1 = rootView.getChildren().get(0);
+        CanvasViewInfo includedView = rootView.getChildren().get(1);
+
+        assertEquals("CheckBox", childView1.getName());
+        assertSame(rootView, childView1.getParent());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect());
+        assertSame(childView1.getUiViewNode(), child1Node);
+
+        assertEquals("RelativeLayout", includedView.getName());
+        assertSame(rootView, includedView.getParent());
+        assertEquals(new Rectangle(10, 30, 69, 4), includedView.getAbsRect());
+        assertEquals(new Rectangle(10, 30, 69, 5), includedView.getSelectionRect());
+        assertSame(includedView.getUiViewNode(), child2Node);
+        assertEquals(0, includedView.getChildren().size());
+    }
+
+    public void testNoIncludeTag() throws Exception {
+        // Test rendering of included views on layoutlib 4- (e.g. no <include> tag cookie
+        // in
+        // view info)
+
+        UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true);
+        ViewInfo root = new ViewInfo("LinearLayout", rootNode, 10, 10, 100, 100);
+        UiViewElementNode child1Node = createNode(rootNode, "android.widget.Button", false);
+        ViewInfo child1 = new ViewInfo("CheckBox", child1Node, 0, 0, 50, 20);
+        UiViewElementNode child2Node = createNode(rootNode, "include", true);
+        ViewInfo child2 = new ViewInfo("RelativeLayout", null /* layoutlib 4 */, 0, 20, 70, 25);
+        root.setChildren(Arrays.asList(child1, child2));
+        ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25);
+        child2.setChildren(Arrays.asList(child21));
+
+        CanvasViewInfo rootView = CanvasViewInfo.create(root);
+        assertNotNull(rootView);
+        assertEquals("LinearLayout", rootView.getName());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect());
+        assertNull(rootView.getParent());
+        assertSame(rootNode, rootView.getUiViewNode());
+        assertEquals(2, rootView.getChildren().size());
+
+        CanvasViewInfo childView1 = rootView.getChildren().get(0);
+        CanvasViewInfo includedView = rootView.getChildren().get(1);
+
+        assertEquals("CheckBox", childView1.getName());
+        assertSame(rootView, childView1.getParent());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect());
+        assertSame(childView1.getUiViewNode(), child1Node);
+
+        assertEquals("RelativeLayout", includedView.getName());
+        assertSame(rootView, includedView.getParent());
+        assertEquals(new Rectangle(10, 30, 69, 4), includedView.getAbsRect());
+        assertEquals(new Rectangle(10, 30, 69, 5), includedView.getSelectionRect());
+        assertSame(includedView.getUiViewNode(), child2Node);
+        assertEquals(0, includedView.getChildren().size());
+    }
+
+    public void testMerge() throws Exception {
+        // Test rendering of MULTIPLE included views or when there is no simple match
+        // between view info and ui element node children
+
+        UiViewElementNode rootNode = createNode("android.widget.LinearLayout", true);
+        ViewInfo root = new ViewInfo("LinearLayout", rootNode, 10, 10, 100, 100);
+        UiViewElementNode child1Node = createNode(rootNode, "android.widget.Button", false);
+        ViewInfo child1 = new ViewInfo("CheckBox", child1Node, 0, 0, 50, 20);
+        UiViewElementNode multiChildNode = createNode(rootNode, "foo", true);
+        ViewInfo child2 = new ViewInfo("RelativeLayout", null, 0, 20, 70, 25);
+        ViewInfo child3 = new ViewInfo("AbsoluteLayout", null, 10, 40, 50, 15);
+        root.setChildren(Arrays.asList(child1, child2, child3));
+        ViewInfo child21 = new ViewInfo("RadioButton", null, 0, 20, 70, 25);
+        child2.setChildren(Arrays.asList(child21));
+
+        CanvasViewInfo rootView = CanvasViewInfo.create(root);
+        assertNotNull(rootView);
+        assertEquals("LinearLayout", rootView.getName());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 89, 89), rootView.getSelectionRect());
+        assertNull(rootView.getParent());
+        assertSame(rootNode, rootView.getUiViewNode());
+        assertEquals(2, rootView.getChildren().size());
+
+        CanvasViewInfo childView1 = rootView.getChildren().get(0);
+        CanvasViewInfo includedView = rootView.getChildren().get(1);
+
+        assertEquals("CheckBox", childView1.getName());
+        assertSame(rootView, childView1.getParent());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getAbsRect());
+        assertEquals(new Rectangle(10, 10, 49, 19), childView1.getSelectionRect());
+        assertSame(childView1.getUiViewNode(), child1Node);
+
+        assertEquals("foo", includedView.getName());
+        assertSame(rootView, includedView.getParent());
+        assertEquals(new Rectangle(10, 30, 70, 5), includedView.getAbsRect());
+        assertEquals(new Rectangle(10, 30, 70, 5), includedView.getSelectionRect());
+        assertEquals(0, includedView.getChildren().size());
+        assertSame(multiChildNode, includedView.getUiViewNode());
+    }
+
+    /**
+     * Dumps out the given {@link ViewInfo} hierarchy to standard out.
+     * Useful during development.
+     *
+     * @param graphicalEditor the editor associated with this hierarchy
+     * @param root the root of the {@link ViewInfo} hierarchy
+     */
+    public static void dump(GraphicalEditorPart graphicalEditor, ViewInfo root) {
+        System.out.println("\n\nRendering:");
+        boolean supportsEmbedding = graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT);
+        System.out.println("Supports Embedded Layout=" + supportsEmbedding);
+        System.out.println("Rendering context=" + graphicalEditor.getIncludedWithin());
+        dump(root, 0);
+
+    }
+
+    /** Helper for {@link #dump(GraphicalEditorPart, ViewInfo)} */
+    private static void dump(ViewInfo info, int depth) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < depth; i++) {
+            sb.append("    ");
+        }
+        sb.append(info.getClassName());
+        sb.append(" [");
+        sb.append(info.getLeft());
+        sb.append(",");
+        sb.append(info.getTop());
+        sb.append(",");
+        sb.append(info.getRight());
+        sb.append(",");
+        sb.append(info.getBottom());
+        sb.append("] ");
+        Object cookie = info.getCookie();
+        if (cookie instanceof UiViewElementNode) {
+            sb.append(" ");
+            UiViewElementNode node = (UiViewElementNode) cookie;
+            sb.append("<");
+            sb.append(node.getXmlNode().getNodeName());
+            sb.append("> ");
+        } else if (cookie != null) {
+            sb.append(" cookie=" + cookie);
+        }
+
+        System.out.println(sb.toString());
+
+        for (ViewInfo child : info.getChildren()) {
+            dump(child, depth + 1);
+        }
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlayTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeOverlayTest.java
new file mode 100644 (file)
index 0000000..a96b1a3
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.editors.layout.gle2;
+
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import junit.framework.TestCase;
+
+public class IncludeOverlayTest extends TestCase {
+
+    public void testSubtractRectangles() throws Exception {
+        checkSubtract(new Rectangle(0, 0, 100, 80), Collections.<Rectangle> emptyList());
+
+        checkSubtract(new Rectangle(0, 0, 100, 80), Arrays.asList(new Rectangle(50, 50, 20, 20)));
+
+        checkSubtract(new Rectangle(0, 0, 100, 80), Arrays.asList(new Rectangle(50, 50, 20, 20),
+                new Rectangle(90, 90, 10, 10)));
+
+        checkSubtract(new Rectangle(0, 0, 100, 80), Arrays.asList(new Rectangle(50, 50, 20, 20),
+                new Rectangle(90, 90, 10, 10), new Rectangle(0, 0, 10, 10)));
+
+    }
+
+    private void checkSubtract(Rectangle rectangle, List<Rectangle> holes) {
+        Collection<Rectangle> result = IncludeOverlay.subtractRectangles(rectangle, holes);
+
+        // Do some Monte Carlo testing - pick random coordinates and check that if they
+        // are within one of the holes then they are not in the result list and vice versa
+        Random random = new Random(42L);
+        for (int i = 0; i < 1000; i++) {
+            int x = random.nextInt(rectangle.width);
+            int y = random.nextInt(rectangle.height);
+
+            boolean inHole = false;
+            for (Rectangle hole : holes) {
+                if (hole.contains(x, y)) {
+                    inHole = true;
+                }
+            }
+
+            boolean inResult = false;
+            for (Rectangle r : result) {
+                if (r.contains(x, y)) {
+                    inResult = true;
+                    break;
+                }
+            }
+
+            if (inHole == inResult) {
+                fail("Wrong result at (" + x + "," + y + ") for rectangle=" + rectangle
+                        + " and holes=" + holes + " where inHole=inResult="
+                        + inResult);
+            }
+        }
+    }
+}
index 2e9e205..08f191a 100755 (executable)
@@ -48,7 +48,7 @@ public class NodeFactoryTest extends TestCase {
         ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass");
         UiViewElementNode uiv = new UiViewElementNode(ved);
         ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120);
-        CanvasViewInfo cvi = new CanvasViewInfo(lvi);
+        CanvasViewInfo cvi = CanvasViewInfo.create(lvi);
 
         // Create a NodeProxy.
         NodeProxy proxy = m.create(cvi);
@@ -95,7 +95,7 @@ public class NodeFactoryTest extends TestCase {
         ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass");
         UiViewElementNode uiv = new UiViewElementNode(ved);
         ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120);
-        CanvasViewInfo cvi = new CanvasViewInfo(lvi);
+        CanvasViewInfo cvi = CanvasViewInfo.create(lvi);
 
         // NodeProxies are cached. Creating the same one twice returns the same proxy.
         NodeProxy proxy1 = m.create(cvi);
@@ -107,7 +107,7 @@ public class NodeFactoryTest extends TestCase {
         ViewElementDescriptor ved = new ViewElementDescriptor("xml", "com.example.MyJavaClass");
         UiViewElementNode uiv = new UiViewElementNode(ved);
         ViewInfo lvi = new ViewInfo("name", uiv, 10, 12, 110, 120);
-        CanvasViewInfo cvi = new CanvasViewInfo(lvi);
+        CanvasViewInfo cvi = CanvasViewInfo.create(lvi);
 
         // NodeProxies are cached. Creating the same one twice returns the same proxy.
         NodeProxy proxy1 = m.create(cvi);