ADT GLE: support multiple selection and alternate selection.

Change-Id: Ia7ce5fb2365898fdcae73eacbaf6d08b091d7586
diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java
index 69846eb..a0de610 100755
--- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java
+++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java
@@ -40,6 +40,10 @@
 import java.awt.image.BufferedImage;

 import java.awt.image.DataBufferInt;

 import java.awt.image.Raster;

+import java.util.ArrayList;

+import java.util.LinkedList;

+import java.util.List;

+import java.util.ListIterator;

 

 /**

  * Displays the image rendered by the {@link GraphicalEditorPart} and handles

@@ -51,46 +55,59 @@
  * TODO list:

  * - gray on error, keep select but disable d'n'd.

  * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.)

- * - handle selection (will need the model, aka the root node).

  * - handle drop target (from palette).

  * - handle drag'n'drop (internal, for moving/duplicating).

  * - handle context menu (depending on selection).

  * - selection synchronization with the outline (both ways).

- * - preserve selection during editor input change if applicable (e.g. when changing configuration.)

  */

 public class LayoutCanvas extends Canvas {

 

+    /**

+     * Margin around the rendered image. Should be enough space to display the layout

+     * width and height pseudo widgets.

+     */

     private static final int IMAGE_MARGIN = 5;

-    private static final int SELECTION_MARGIN = 2;

+    /**

+     * Minimal size of the selection, in case an empty view or layout is selected.

+     */

+    private static final int SELECTION_MIN_SIZE = 6;

 

     private ILayoutResult mLastValidResult;

+    private ViewInfo mLastValidViewInfoRoot;

 

     /** Current background image. Null when there's no image. */

     private Image mImage;

 

-    /** Current selected view info. Null when none is selected. */

-    private ILayoutViewInfo mSelectionViewInfo;

-    /** Current selection border rectangle. Null when there's no selection. */

-    private Rectangle mSelectionRect;

-    /** The name displayed over the selection, typically the widget class name. */

-    private String mSelectionName;

+    private final LinkedList<Selection> mSelections = new LinkedList<Selection>();

+

     /** Selection border color. Do not dispose, it's a system color. */

     private Color mSelectionFgColor;

+

     /** Selection name font. Do not dispose, it's a system font. */

     private Font mSelectionFont;

+

     /** Pixel height of the font displaying the selection name. Initially set to 0 and only

      * initialized in onPaint() when we have a GC. */

     private int mSelectionFontHeight;

 

     /** Current hover view info. Null when no mouse hover. */

-    private ILayoutViewInfo mHoverViewInfo;

+    private ViewInfo mHoverViewInfo;

+

     /** Current mouse hover border rectangle. Null when there's no mouse hover. */

     private Rectangle mHoverRect;

-    /** Hover border color. Do not dispose, it's a system color. */

+

+    /** Hover border color. Must be disposed, it's NOT a system color. */

     private Color mHoverFgColor;

 

-    private boolean mIsResultValid;

+    private AlternateSelection mAltSelection;

 

+    /**

+     * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}

+     * in which case it is also available in {@link #mLastValidResult}.

+     * 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.

+     */

+    private boolean mIsResultValid;

 

 

     public LayoutCanvas(Composite parent, int style) {

@@ -98,9 +115,8 @@
 

         Display d = getDisplay();

         mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);

-        mHoverFgColor = mSelectionFgColor;

-

-        mSelectionFont = d.getSystemFont();

+        mHoverFgColor     = new Color(d, 0xFF, 0x99, 0x00); // orange

+        mSelectionFont    = d.getSystemFont();

 

         addPaintListener(new PaintListener() {

             public void paintControl(PaintEvent e) {

@@ -129,6 +145,11 @@
         });

     }

 

+    @Override

+    public void dispose() {

+        super.dispose();

+    }

+

     /**

      * Sets the result of the layout rendering. The result object indicates if the layout

      * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.

@@ -148,16 +169,29 @@
 

         if (mIsResultValid && result != null) {

             mLastValidResult = result;

+            mLastValidViewInfoRoot = new ViewInfo(result.getRootView());

             setImage(result.getImage());

 

-            // Check if the selection is still the same (based on its key)

-            // and eventually recompute its bounds.

-            if (mSelectionViewInfo != null) {

-                ILayoutViewInfo vi = findViewInfoKey(

-                        mSelectionViewInfo.getViewKey(),

-                        result.getRootView());

-                setSelection(vi);

+            // Check if the selection is still the same (based on the object keys)

+            // and eventually recompute their bounds.

+            for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) {

+                Selection s = it.next();

+

+                // Check the if the selected object still exists

+                Object key = s.getViewInfo().getKey();

+                ViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot);

+

+                // Remove the previous selection -- if the selected object still exists

+                // we need to recompute its bounds in case it moved so we'll insert a new one

+                // at the same place.

+                it.remove();

+                if (vi != null) {

+                    it.add(new Selection(vi));

+                }

             }

+

+            // remove the current alternate selection views

+            mAltSelection = null;

         }

 

         redraw();

@@ -184,6 +218,9 @@
         mImage = new Image(getDisplay(), imageData);

     }

 

+    /**

+     * Paints the canvas in response to paint events.

+     */

     private void onPaint(PaintEvent e) {

         GC gc = e.gc;

 

@@ -212,19 +249,27 @@
             mSelectionFontHeight = fm.getHeight();

         }

 

-        if (mSelectionRect != null) {

-            gc.setForeground(mSelectionFgColor);

-            gc.setLineStyle(SWT.LINE_SOLID);

-            gc.drawRectangle(mSelectionRect);

+        for (Selection s : mSelections) {

+            drawSelection(gc, s);

+        }

+    }

 

-            if (mSelectionName != null) {

-                int x = mSelectionRect.x + 2;

-                int y = mSelectionRect.y - mSelectionFontHeight;

-                if (y < 0) {

-                    y = mSelectionRect.y + mSelectionRect.height;

-                }

-                gc.drawString(mSelectionName, x, y, true /*transparent*/);

+    private void drawSelection(GC gc, Selection s) {

+        Rectangle r = s.getRect();

+

+        gc.setForeground(mSelectionFgColor);

+        gc.setLineStyle(SWT.LINE_SOLID);

+        gc.drawRectangle(s.mRect);

+

+        String name = s.getName();

+

+        if (name != null) {

+            int xs = r.x + 2;

+            int ys = r.y - mSelectionFontHeight;

+            if (ys < 0) {

+                ys = r.y + r.height;

             }

+            gc.drawString(name, xs, ys, true /*transparent*/);

         }

     }

 

@@ -233,8 +278,8 @@
      */

     private void onMouseMove(MouseEvent e) {

         if (mLastValidResult != null) {

-            ILayoutViewInfo root = mLastValidResult.getRootView();

-            ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root);

+            ViewInfo root = mLastValidViewInfoRoot;

+            ViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root);

 

             // We don't hover on the root since it's not a widget per see and it is always there.

             if (vi == root) {

@@ -244,10 +289,12 @@
             boolean needsUpdate = vi != mHoverViewInfo;

             mHoverViewInfo = vi;

 

-            mHoverRect = vi == null ? null : getViewInfoRect(vi);

-            if (mHoverRect != null) {

-                mHoverRect.x += IMAGE_MARGIN;

-                mHoverRect.y += IMAGE_MARGIN;

+            if (vi == null) {

+                mHoverRect = null;

+            } else {

+                Rectangle r = vi.getSelectionRect();

+                mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN,

+                                           r.width, r.height);

             }

 

             if (needsUpdate) {

@@ -262,12 +309,125 @@
 

     /**

      * Performs selection on mouse up (not mouse down).

+     * <p/>

+     * Shift key is used to toggle in multi-selection.

+     * Alt key is used to cycle selection through objects at the same level than the one

+     * pointed at (i.e. click on an object then alt-click to cycle).

      */

     private void onMouseUp(MouseEvent e) {

         if (mLastValidResult != null) {

-            ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN,

-                    mLastValidResult.getRootView());

-            setSelection(vi);

+

+            boolean isShift = (e.stateMask & SWT.SHIFT) != 0;

+            boolean isAlt   = (e.stateMask & SWT.ALT)   != 0;

+

+            int x = e.x - IMAGE_MARGIN;

+            int y = e.y - IMAGE_MARGIN;

+            ViewInfo vi = findViewInfoAt(x, y, mLastValidViewInfoRoot);

+

+            if (isShift && !isAlt) {

+                // Case where shift is pressed: pointed object is toggled.

+

+                // reset alternate selection if any

+                mAltSelection = null;

+

+                // If nothing has been found at the cursor, assume it might be a user error

+                // and avoid clearing the existing selection.

+

+                if (vi != null) {

+                    // toggle this selection on-off: remove it if already selected

+                    if (deselect(vi)) {

+                        redraw();

+                        return;

+                    }

+

+                    // otherwise add it.

+                    mSelections.add(new Selection(vi));

+                    redraw();

+                }

+

+            } else if (isAlt) {

+                // Case where alt is pressed: select or cycle the object pointed at.

+

+                // Note: if shift and alt are pressed, shift is ignored. The alternate selection

+                // mechanism does not reset the current multiple selection unless they intersect.

+

+                // We need to remember the "origin" of the alternate selection, to be

+                // able to continue cycling through it later. If there's no alternate selection,

+                // create one. If there's one but not for the same origin object, create a new

+                // one too.

+                if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) {

+                    mAltSelection = new AlternateSelection(vi, findAltViewInfoAt(

+                                                    x, y, mLastValidViewInfoRoot, null));

+

+                    // deselect them all, in case they were partially selected

+                    deselectAll(mAltSelection.getAltViews());

+

+                    // select the current one

+                    ViewInfo vi2 = mAltSelection.getCurrent();

+                    if (vi2 != null) {

+                        mSelections.addFirst(new Selection(vi2));

+                    }

+                } else {

+                    // We're trying to cycle through the current alternate selection.

+                    // First remove the current object.

+                    ViewInfo vi2 = mAltSelection.getCurrent();

+                    deselect(vi2);

+

+                    // Now select the next one.

+                    vi2 = mAltSelection.getNext();

+                    if (vi2 != null) {

+                        mSelections.addFirst(new Selection(vi2));

+                    }

+                }

+                redraw();

+

+            } else {

+                // Case where no modifier is pressed: either select or reset the selection.

+

+                // reset alternate selection if any

+                mAltSelection = null;

+

+                // reset (multi)selection if any

+                if (mSelections.size() > 0) {

+                    if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {

+                        // Selection remains the same, don't touch it.

+                        return;

+                    }

+                    mSelections.clear();

+                }

+

+                if (vi != null) {

+                    mSelections.add(new Selection(vi));

+                }

+                redraw();

+            }

+        }

+    }

+

+    /** Deselects a view info. Returns true if the object was actually selected. */

+    private boolean deselect(ViewInfo viewInfo) {

+        if (viewInfo == null) {

+            return false;

+        }

+

+        for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) {

+            Selection s = it.next();

+            if (viewInfo == s.getViewInfo()) {

+                it.remove();

+                return true;

+            }

+        }

+

+        return false;

+    }

+

+    /** Deselects multiple view infos, */

+    private void deselectAll(List<ViewInfo> viewInfos) {

+        for (ListIterator<Selection> it = mSelections.listIterator(); it.hasNext(); ) {

+            Selection s = it.next();

+            if (viewInfos.contains(s.getViewInfo())) {

+                it.remove();

+            }

         }

     }

 

@@ -275,61 +435,20 @@
         // pass, not used yet.

     }

 

-    private void setSelection(ILayoutViewInfo viewInfo) {

-        boolean needsUpdate = viewInfo != mSelectionViewInfo;

-        mSelectionViewInfo = viewInfo;

-

-        mSelectionRect = viewInfo == null ? null : getViewInfoRect(viewInfo);

-        if (mSelectionRect != null) {

-            mSelectionRect.x += IMAGE_MARGIN;

-            mSelectionRect.y += IMAGE_MARGIN;

-        }

-

-        String name = viewInfo == null ? null : viewInfo.getName();

-        if (name != null) {

-            // The name is typically a fully-qualified class name. Let's make it a tad shorter.

-

-            if (name.startsWith("android.")) {                                      // $NON-NLS-1$

-                // For android classes, convert android.foo.Name to android...Name

-                int first = name.indexOf('.');

-                int last = name.lastIndexOf('.');

-                if (last > first) {

-                    name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$

-                }

-            } else {

-                // For custom non-android classes, it's best to keep the 2 first segments of

-                // the namespace, e.g. we want to get something like com.example...MyClass

-                int first = name.indexOf('.');

-                first = name.indexOf('.', first + 1);

-                int last = name.lastIndexOf('.');

-                if (last > first) {

-                    name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$

-                }

-            }

-        }

-        mSelectionName = name;

-

-        if (needsUpdate) {

-            redraw();

-        }

-    }

-

     /**

      * Tries to find a child with the same view key in the view info sub-tree.

      * Returns null if not found.

      */

-    private ILayoutViewInfo findViewInfoKey(Object viewKey, ILayoutViewInfo viewInfo) {

-        if (viewInfo.getViewKey() == viewKey) {

+    private ViewInfo findViewInfoKey(Object viewKey, ViewInfo viewInfo) {

+        if (viewInfo.getKey() == viewKey) {

             return viewInfo;

         }

 

         // try to find a matching child

-        if (viewInfo.getChildren() != null) {

-            for (ILayoutViewInfo child : viewInfo.getChildren()) {

-                ILayoutViewInfo v = findViewInfoKey(viewKey, child);

-                if (v != null) {

-                    return v;

-                }

+        for (ViewInfo child : viewInfo.getChildren()) {

+            ViewInfo v = findViewInfoKey(viewKey, child);

+            if (v != null) {

+                return v;

             }

         }

 

@@ -342,17 +461,15 @@
      *

      * Returns null if not found.

      */

-    private ILayoutViewInfo findViewInfoAt(int x, int y, ILayoutViewInfo viewInfo) {

-        Rectangle r = getViewInfoRect(viewInfo);

+    private ViewInfo findViewInfoAt(int x, int y, ViewInfo viewInfo) {

+        Rectangle r = viewInfo.getSelectionRect();

         if (r.contains(x, y)) {

 

             // try to find a matching child first

-            if (viewInfo.getChildren() != null) {

-                for (ILayoutViewInfo child : viewInfo.getChildren()) {

-                    ILayoutViewInfo v = findViewInfoAt(x, y, child);

-                    if (v != null) {

-                        return v;

-                    }

+            for (ViewInfo child : viewInfo.getChildren()) {

+                ViewInfo v = findViewInfoAt(x, y, child);

+                if (v != null) {

+                    return v;

                 }

             }

 

@@ -363,26 +480,295 @@
         return null;

     }

 

-    /**

-     * Returns the bounds of the view info as a rectangle.

-     * In case the view has a null width or null height, it is expanded using

-     * {@link #SELECTION_MARGIN}.

-     */

-    private Rectangle getViewInfoRect(ILayoutViewInfo viewInfo) {

-        int x = viewInfo.getLeft();

-        int y = viewInfo.getTop();

-        int w = viewInfo.getRight() - x;

-        int h = viewInfo.getBottom() - y;

+    private ArrayList<ViewInfo> findAltViewInfoAt(

+            int x, int y, ViewInfo parent, ArrayList<ViewInfo> outList) {

+        Rectangle r;

 

-        if (w == 0) {

-            x -= SELECTION_MARGIN;

-            w += 2 * SELECTION_MARGIN;

-        }

-        if (h == 0) {

-            y -= SELECTION_MARGIN;

-            h += 2* SELECTION_MARGIN;

+        if (outList == null) {

+            outList = new ArrayList<ViewInfo>();

+

+            // add the parent root only once

+            r = parent.getSelectionRect();

+            if (r.contains(x, y)) {

+                outList.add(parent);

+            }

         }

 

-        return new Rectangle(x, y, w, h);

+        if (parent.getChildren().size() > 0) {

+            // then add all children that match the position

+            for (ViewInfo child : parent.getChildren()) {

+                r = child.getSelectionRect();

+                if (r.contains(x, y)) {

+                    outList.add(child);

+                }

+            }

+

+            // finally recurse in the children

+            for (ViewInfo child : parent.getChildren()) {

+                r = child.getSelectionRect();

+                if (r.contains(x, y)) {

+                    findAltViewInfoAt(x, y, child, outList);

+                }

+            }

+        }

+

+        return outList;

     }

+

+    /**

+     * Maps a {@link ILayoutViewInfo} in a structure more adapted to our needs.

+     * The only large difference is that we keep both the original bounds of the view info

+     * and we pre-compute the selection bounds which are absolute to the rendered image (where

+     * as the original bounds are relative to the parent view.)

+     * <p/>

+     * Each view also know its parent, which should be handy later.

+     * <p/>

+     * We can't alter {@link ILayoutViewInfo} as it is part of the LayoutBridge and needs to

+     * have a fixed API.

+     */

+    private static class ViewInfo {

+        private final Rectangle mRealRect;

+        private final Rectangle mSelectionRect;

+        private final String mName;

+        private final Object mKey;

+        private final ViewInfo mParent;

+        private final ArrayList<ViewInfo> mChildren = new ArrayList<ViewInfo>();

+

+        /**

+         * Constructs a {@link ViewInfo} hierarchy based on a given {@link ILayoutViewInfo}

+         * hierarchy. This call is recursives and builds a full tree.

+         *

+         * @param viewInfo The root of the {@link ILayoutViewInfo} hierarchy.

+         */

+        public ViewInfo(ILayoutViewInfo viewInfo) {

+            this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/);

+        }

+

+        private ViewInfo(ILayoutViewInfo viewInfo, ViewInfo parent, int parentX, int parentY) {

+            mParent = parent;

+            mKey  = viewInfo.getViewKey();

+            mName = viewInfo.getName();

+

+            int x = viewInfo.getLeft();

+            int y = viewInfo.getTop();

+            int w = viewInfo.getRight() - x;

+            int h = viewInfo.getBottom() - y;

+

+            mRealRect = new Rectangle(x, y, w, h);

+

+            if (parent != null) {

+                x += parentX;

+                y += parentY;

+            }

+

+            if (viewInfo.getChildren() != null) {

+                for (ILayoutViewInfo child : viewInfo.getChildren()) {

+                    mChildren.add(new ViewInfo(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);

+        }

+

+        /** Returns the original {@link ILayoutResult} bounds, relative to the parent. */

+        public Rectangle getRealRect() {

+            return mRealRect;

+        }

+

+        /*

+        * Returns the absolute selection bounds of the view info as a rectangle.

+        * The selection bounds will always have a size greater or equal to

+        * {@link #SELECTION_MIN_SIZE}.

+        * The width/height is inclusive (i.e. width = right-left-1).

+        * This is in absolute "screen" coordinates (relative to the rendered bitmap).

+        */

+        public Rectangle getSelectionRect() {

+            return mSelectionRect;

+        }

+

+        /**

+         * Returns the view key. Could be null, although unlikely.

+         * @see ILayoutViewInfo#getViewKey()

+         */

+        public Object getKey() {

+            return mKey;

+        }

+

+        /**

+         * Returns the parent {@link ViewInfo}.

+         * It is null for the root and non-null for children.

+         */

+        public ViewInfo getParent() {

+            return mParent;

+        }

+

+        /**

+         * Returns the list of children of this {@link ViewInfo}.

+         * The list is never null. It can be empty.

+         * By contract, this.getChildren().get(0..n-1).getParent() == this.

+         */

+        public ArrayList<ViewInfo> getChildren() {

+            return mChildren;

+        }

+

+        /**

+         * Returns the name of the {@link ViewInfo}.

+         * Could be null, although unlikely.

+         * Experience shows this is the full qualified Java name of the View.

+         * @see ILayoutViewInfo#getName()

+         */

+        public String getName() {

+            return mName;

+        }

+    }

+

+    /**

+     * Represents one selection.

+     */

+    private static class Selection {

+        /** Current selected view info. Cannot be null. */

+        private final ViewInfo mViewInfo;

+

+        /** Current selection border rectangle. Cannot be null. */

+        private final Rectangle mRect;

+

+        /** The name displayed over the selection, typically the widget class name. Can be null. */

+        private final String mName;

+

+        /**

+         * Creates a new {@link Selection} object.

+         * @param viewInfo The view info being selected. Must not be null.

+         */

+        public Selection(ViewInfo viewInfo) {

+

+            assert viewInfo != null;

+

+            mViewInfo = viewInfo;

+

+            if (viewInfo == null) {

+                mRect = null;

+            } else {

+                Rectangle r = viewInfo.getSelectionRect();

+                mRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height);

+            }

+

+            String name = viewInfo == null ? null : viewInfo.getName();

+            if (name != null) {

+                // The name is typically a fully-qualified class name. Let's make it a tad shorter.

+

+                if (name.startsWith("android.")) {                                      // $NON-NLS-1$

+                    // For android classes, convert android.foo.Name to android...Name

+                    int first = name.indexOf('.');

+                    int last = name.lastIndexOf('.');

+                    if (last > first) {

+                        name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$

+                    }

+                } else {

+                    // For custom non-android classes, it's best to keep the 2 first segments of

+                    // the namespace, e.g. we want to get something like com.example...MyClass

+                    int first = name.indexOf('.');

+                    first = name.indexOf('.', first + 1);

+                    int last = name.lastIndexOf('.');

+                    if (last > first) {

+                        name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$

+                    }

+                }

+            }

+

+            mName = name;

+        }

+

+        /**

+         * Returns the selected view info. Cannot be null.

+         */

+        public ViewInfo getViewInfo() {

+            return mViewInfo;

+        }

+

+        /**

+         * Returns the selection border rectangle.

+         * Cannot be null.

+         */

+        public Rectangle getRect() {

+            return mRect;

+        }

+

+        /**

+         * The name displayed over the selection, typically the widget class name.

+         * Can be null.

+         */

+        public String getName() {

+            return mName;

+        }

+    }

+

+    /**

+     * Information for the current alternate selection, i.e. the possible selected items

+     * that are located at the same x/y as the original view, either sibling or parents.

+     */

+    private static class AlternateSelection {

+        private final ViewInfo mOriginatingView;

+        private final List<ViewInfo> mAltViews;

+        private int mIndex;

+

+        /**

+         * Creates a new alternate selection based on the given originating view and the

+         * given list of alternate views. Both cannot be null.

+         */

+        public AlternateSelection(ViewInfo originatingView, List<ViewInfo> altViews) {

+            assert originatingView != null;

+            assert altViews != null;

+            mOriginatingView = originatingView;

+            mAltViews = altViews;

+            mIndex = altViews.size() - 1;

+        }

+

+        /** Returns the list of alternate views. Cannot be null. */

+        public List<ViewInfo> getAltViews() {

+            return mAltViews;

+        }

+

+        /** Returns the originating view. Cannot be null. */

+        public ViewInfo getOriginatingView() {

+            return mOriginatingView;

+        }

+

+        /**

+         * Returns the current alternate view to select.

+         * Initially this is the top-most view.

+         */

+        public ViewInfo getCurrent() {

+            return mIndex >= 0 ? mAltViews.get(mIndex) : null;

+        }

+

+        /**

+         * Changes the current view to be the next one and then returns it.

+         * This loops through the alternate views.

+         */

+        public ViewInfo getNext() {

+            if (mIndex == 0) {

+                mIndex = mAltViews.size() - 1;

+            } else if (mIndex > 0) {

+                mIndex--;

+            }

+

+            return getCurrent();

+        }

+    }

+

+

 }