OSDN Git Service

Add gesture support, marquee selection, and refactoring
authorTor Norbye <tnorbye@google.com>
Tue, 26 Oct 2010 15:54:15 +0000 (08:54 -0700)
committerTor Norbye <tnorbye@google.com>
Fri, 5 Nov 2010 00:26:21 +0000 (17:26 -0700)
This checkin adds support for gestures and overlays. Gestures are
sessions of mouse/keyboard activity, and this is documented in the
javadoc for the new Gesture class. Overlays are units of graphics, and
these are documented in the Overlay javadoc. The gesture architecture
lets us isolate the logic for each different type of operation
(marquee, resize, move, etC), and with associated overlays we don't
attempt to for example paint drag feedback during a resize operation,
etc.

The checkin also adds marquee selection (as a second gesture, in
addition to the existing drag & drop based move gesture), along with
some associated changes in how the root view is treated.

As part of isolating the mouse handling and painting related to
gestures, painting etc., I also refactored the code quite a bit.
LayoutCanvas which used to be a large class has been split into a
number of new classes, one for each area of responsibility:

- The mouse listener and drag & drop code has been moved into a
  GestureManager. (A lot of the drop handling code also came from the
  CanvasDropListener class.)

- Code related to maintaining the set of rendered views, and
  performing searches in the views, has been moved into a
  ViewHierarchy class.

- Code related to selection has been moved into a SelectionManager.

- Various individual painting pieces (outline, hover, etc) have been
  moved into individual Overlay classes such as OutlineOverlay,
  HoverOverlay, SelectionOverlay, etc. This also moved associated
  resource allocation and cleanup into the overlays.

- New coordinate classes, ControlPoint and LayoutPoint, are used
  instead of ints and plain Points to make it really clear which
  methods require coordinates in the layout (such as the
  ViewHieararchy search methods) and which ones require coordinates in
  the canvas control (such as paint methods). There are factory methods
  to automatically construct the right kind of coordinate from
  different types of mouse events, as well as methods to convert
  between the two.

I also tweaked the visual appearance of selection a bit more, and
some other misc cleanup.

Change-Id: I666aabdcd36720bebe406b68237e8966d985fb8f

38 files changed:
eclipse/dictionary.txt
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/Pair.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasSelection.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/ClipboardSupport.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GCWrapper.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GlobalCanvasDragInfo.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/HoverOverlay.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvasViewer.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java [moved from eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasDropListener.java with 90% similarity, mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlinePage2.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SimpleXmlTransfer.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/NodeProxy.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPointTest.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPointTest.java [new file with mode: 0644]
eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java [new file with mode: 0644]

index 260b8ce..e82a6dc 100644 (file)
@@ -20,6 +20,7 @@ callbacks
 checkbox
 classpath
 clipboard
+clipboards
 codebase
 codename
 codenames
@@ -56,6 +57,7 @@ instanceof
 int
 javadoc
 layoutlib
+leaky
 lifecycle
 linestyle
 linux
index 15c3ba9..950f6b9 100644 (file)
@@ -23,6 +23,9 @@ package com.android.ide.common.layout;
  * classes using this Pair by a more dedicated data structure (so we don't have
  * to pass around generic signatures as is currently done, though at least the
  * construction is helped a bit by the {@link #of} factory method.
+ *
+ * @param <S> The type of the first value
+ * @param <T> The type of the second value
  */
 class Pair<S,T> {
     private final S mFirst;
@@ -85,4 +88,4 @@ class Pair<S,T> {
             return false;
         return true;
     }
-}
\ No newline at end of file
+}
index c78a523..7ac8354 100755 (executable)
@@ -609,7 +609,8 @@ public class RelativeLayoutRule extends BaseLayout {
                     }
 
                     for (String it : data.getCurr().getAttr()) {
-                        newChild.setAttribute(ANDROID_URI, "layout_" + it, id != null ? id : "true");
+                        newChild.setAttribute(ANDROID_URI,
+                                "layout_" + it, id != null ? id : "true");
                     }
 
                     addInnerElements(newChild, element, idMap);
index 117138e..1e745b3 100644 (file)
@@ -37,6 +37,7 @@ import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.jface.action.IAction;
 import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.BadLocationException;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.source.ISourceViewer;
 import org.eclipse.swt.widgets.Display;
@@ -66,6 +67,7 @@ import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
 import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
@@ -81,6 +83,7 @@ import java.net.URL;
  * Derived classes must implement createFormPages to create the forms before the
  * source editor. This can be a no-op if desired.
  */
+@SuppressWarnings("restriction") // Uses XML model, which has no non-restricted replacement yet
 public abstract class AndroidXmlEditor extends FormEditor implements IResourceChangeListener {
 
     /** Preference name for the current page of this file */
@@ -940,6 +943,38 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
     }
 
     /**
+     * Get the XML text directly from the editor.
+     *
+     * @param xmlNode The node whose XML text we want to obtain.
+     * @return The XML representation of the {@link Node}.
+     */
+    public String getXmlText(Node xmlNode) {
+        String data = null;
+        IStructuredModel model = getModelForRead();
+        try {
+            IStructuredDocument document = getStructuredDocument();
+            if (xmlNode instanceof NodeContainer) {
+                // The easy way to get the source of an SSE XML node.
+                data = ((NodeContainer) xmlNode).getSource();
+            } else  if (xmlNode instanceof IndexedRegion && document != null) {
+                // Try harder.
+                IndexedRegion region = (IndexedRegion) xmlNode;
+                int start = region.getStartOffset();
+                int end = region.getEndOffset();
+
+                if (end > start) {
+                    data = document.get(start, end - start);
+                }
+            }
+        } catch (BadLocationException e) {
+            // the region offset was invalid. ignore.
+        } finally {
+            model.releaseFromRead();
+        }
+        return data;
+    }
+
+    /**
      * Listen to changes in the underlying XML model in the structured editor.
      */
     private class XmlModelStateListener implements IModelStateListener {
index 3875896..c23a4d4 100644 (file)
@@ -534,7 +534,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
      * Helper method that returns a {@link ViewElementDescriptor} for the requested FQCN.
      * Will return null if we can't find that FQCN or we lack the editor/data/descriptors info.
      */
-    public ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
+    public ViewElementDescriptor getFqcnViewDescriptor(String fqcn) {
         ViewElementDescriptor desc = null;
 
         AndroidTargetData data = getTargetData();
index 4c29c5f..cd927ec 100755 (executable)
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
 import com.android.ide.common.api.INode;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
 import com.android.sdklib.SdkConstants;
 
 import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Represents one selection in {@link LayoutCanvas}.
@@ -70,6 +76,7 @@ import org.eclipse.swt.graphics.Rectangle;
     /**
      * Returns true when this selection item represents the root, the top level
      * layout element in the editor.
+     * @return True if and only if this element is at the root of the hierarchy
      */
     public boolean isRoot() {
         return mNodeProxy.getParent() == null;
@@ -123,7 +130,7 @@ import org.eclipse.swt.graphics.Rectangle;
         if (mNodeProxy != null) {
             INode parent = mNodeProxy.getParent();
             if (parent instanceof NodeProxy) {
-                gre.callOnChildSelected(gcWrapper, (NodeProxy)parent, mNodeProxy);
+                gre.callOnChildSelected(gcWrapper, (NodeProxy) parent, mNodeProxy);
             }
         }
     }
@@ -174,4 +181,47 @@ import org.eclipse.swt.graphics.Rectangle;
 
         return name;
     }
+
+    /**
+     * Gets the XML text from the given selection for a text transfer.
+     * The returned string can be empty but not null.
+     */
+    /* package */ static String getAsText(LayoutCanvas canvas, List<CanvasSelection> selection) {
+        StringBuilder sb = new StringBuilder();
+
+        LayoutEditor layoutEditor = canvas.getLayoutEditor();
+        for (CanvasSelection cs : selection) {
+            CanvasViewInfo vi = cs.getViewInfo();
+            UiViewElementNode key = vi.getUiViewKey();
+            Node node = key.getXmlNode();
+            String t = layoutEditor.getXmlText(node);
+            if (t != null) {
+                if (sb.length() > 0) {
+                    sb.append('\n');
+                }
+                sb.append(t);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Returns elements representing the given selection of canvas items.
+     *
+     * @param items Items to wrap in elements
+     * @return An array of wrapper elements. Never null.
+     */
+    /* package */ static SimpleElement[] getAsElements(List<CanvasSelection> items) {
+        ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
+
+        for (CanvasSelection cs : items) {
+            CanvasViewInfo vi = cs.getViewInfo();
+
+            SimpleElement e = vi.toSimpleElement();
+            elements.add(e);
+        }
+
+        return elements.toArray(new SimpleElement[elements.size()]);
+    }
 }
index f4dce0f..9714b6c 100755 (executable)
 
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
+import com.android.ide.common.api.Rect;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
 import com.android.layoutlib.api.ILayoutResult;
 import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
 
@@ -69,7 +73,8 @@ public class CanvasViewInfo implements IPropertySource {
         this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/);
     }
 
-    private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent, int parentX, int parentY) {
+    private CanvasViewInfo(ILayoutViewInfo viewInfo, CanvasViewInfo parent,
+            int parentX, int parentY) {
         mParent = parent;
         mName = viewInfo.getName();
 
@@ -258,4 +263,65 @@ public class CanvasViewInfo implements IPropertySource {
 
         return null;
     }
+
+    /**
+     * Returns true iff this view info corresponds to a root element.
+     *
+     * @return True iff this is a root view info.
+     */
+    public boolean isRoot() {
+        // Select the visual element -- unless it's the root.
+        // The root element is the one whose GRAND parent
+        // is null (because the parent will be a -document-
+        // node).
+        return mUiViewKey == null || mUiViewKey.getUiParent() == null ||
+            mUiViewKey.getUiParent().getUiParent() == null;
+    }
+
+    /**
+     * Returns the info represented as a {@link SimpleElement}.
+     *
+     * @return A {@link SimpleElement} wrapping this info.
+     */
+    /* package */ SimpleElement toSimpleElement() {
+
+        UiViewElementNode uiNode = getUiViewKey();
+
+        String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
+        String parentFqcn = null;
+        Rect bounds = new Rect(getAbsRect());
+        Rect parentBounds = null;
+
+        UiElementNode uiParent = uiNode.getUiParent();
+        if (uiParent != null) {
+            parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
+        }
+        if (getParent() != null) {
+            parentBounds = new Rect(getParent().getAbsRect());
+        }
+
+        SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
+
+        for (UiAttributeNode attr : uiNode.getUiAttributes()) {
+            String value = attr.getCurrentValue();
+            if (value != null && value.length() > 0) {
+                AttributeDescriptor attrDesc = attr.getDescriptor();
+                SimpleAttribute a = new SimpleAttribute(
+                        attrDesc.getNamespaceUri(),
+                        attrDesc.getXmlLocalName(),
+                        value);
+                e.addAttribute(a);
+            }
+        }
+
+        for (CanvasViewInfo childVi : getChildren()) {
+            SimpleElement e2 = childVi.toSimpleElement();
+            if (e2 != null) {
+                e.addInnerElement(e2);
+            }
+        }
+
+        return e;
+    }
+
 }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
new file mode 100644 (file)
index 0000000..00b2518
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ * 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.api.IDragElement;
+import com.android.ide.common.api.IDragElement.IDragAttribute;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.widgets.Composite;
+
+import java.util.List;
+
+/**
+ * The {@link ClipboardSupport} class manages the native clipboard, providing operations
+ * to copy, cut and paste view items, and can answer whether the clipboard contains
+ * a transferable we care about.
+ */
+public class ClipboardSupport {
+    private static final boolean DEBUG = false;
+
+    /** SWT clipboard instance. */
+    private Clipboard mClipboard;
+    private LayoutCanvas mCanvas;
+
+    /**
+     * Constructs a new {@link ClipboardSupport} tied to the given
+     * {@link LayoutCanvas}.
+     *
+     * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
+     * @param parent The parent widget in the SWT hierarchy of the canvas.
+     */
+    public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
+        this.mCanvas = canvas;
+
+        mClipboard = new Clipboard(parent.getDisplay());
+    }
+
+    /**
+     * Frees up any resources held by the {@link ClipboardSupport}.
+     */
+    public void dispose() {
+        if (mClipboard != null) {
+            mClipboard.dispose();
+            mClipboard = null;
+        }
+    }
+
+    /**
+     * Perform the "Copy" action, either from the Edit menu or from the context
+     * menu.
+     * <p/>
+     * This sanitizes the selection, so it must be a copy. It then inserts the
+     * selection both as text and as {@link SimpleElement}s in the clipboard.
+     *
+     * @param selection A list of selection items to add to the clipboard;
+     *            <b>this should be a copy already - this method will not make a
+     *            copy</b>
+     */
+    public void copySelectionToClipboard(List<CanvasSelection> selection) {
+        SelectionManager.sanitize(selection);
+
+        if (selection.isEmpty()) {
+            return;
+        }
+
+        Object[] data = new Object[] {
+                CanvasSelection.getAsElements(selection),
+                CanvasSelection.getAsText(mCanvas, selection)
+        };
+
+        Transfer[] types = new Transfer[] {
+                SimpleXmlTransfer.getInstance(),
+                TextTransfer.getInstance()
+        };
+
+        mClipboard.setContents(data, types);
+    }
+
+    /**
+     * Perform the "Cut" action, either from the Edit menu or from the context
+     * menu.
+     * <p/>
+     * This sanitizes the selection, so it must be a copy. It uses the
+     * {@link #copySelectionToClipboard(List)} method to copy the selection to
+     * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
+     * delete the selection with a "Cut" verb for the title.
+     *
+     * @param selection A list of selection items to add to the clipboard;
+     *            <b>this should be a copy already - this method will not make a
+     *            copy</b>
+     */
+    public void cutSelectionToClipboard(List<CanvasSelection> selection) {
+        copySelectionToClipboard(selection);
+        deleteSelection(
+                mCanvas.getCutLabel(),
+                selection);
+    }
+
+    /**
+     * Deletes the given selection.
+     *
+     * @param verb A translated verb for the action. Will be used for the
+     *            undo/redo title. Typically this should be
+     *            {@link Action#getText()} for either the cut or the delete
+     *            actions in the canvas.
+     * @param selection The selection. Must not be null. Can be empty, in which
+     *            case nothing happens. The selection list will be sanitized so
+     *            the caller should pass in a copy.
+     */
+    public void deleteSelection(String verb, final List<CanvasSelection> selection) {
+        SelectionManager.sanitize(selection);
+
+        if (selection.isEmpty()) {
+            return;
+        }
+
+        // If all selected items have the same *kind* of parent, display that in the undo title.
+        String title = null;
+        for (CanvasSelection cs : selection) {
+            CanvasViewInfo vi = cs.getViewInfo();
+            if (vi != null && vi.getParent() != null) {
+                if (title == null) {
+                    title = vi.getParent().getName();
+                } else if (!title.equals(vi.getParent().getName())) {
+                    // More than one kind of parent selected.
+                    title = null;
+                    break;
+                }
+            }
+        }
+
+        if (title != null) {
+            // Typically the name is an FQCN. Just get the last segment.
+            int pos = title.lastIndexOf('.');
+            if (pos > 0 && pos < title.length() - 1) {
+                title = title.substring(pos + 1);
+            }
+        }
+        boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
+        if (title == null) {
+            title = String.format(
+                        multiple ? "%1$s elements" : "%1$s element",
+                        verb);
+        } else {
+            title = String.format(
+                        multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
+                        verb, title);
+        }
+
+        // Implementation note: we don't clear the internal selection after removing
+        // the elements. An update XML model event should happen when the model gets released
+        // which will trigger a recompute of the layout, thus reloading the model thus
+        // resetting the selection.
+        mCanvas.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() {
+            public void run() {
+                for (CanvasSelection cs : selection) {
+                    CanvasViewInfo vi = cs.getViewInfo();
+                    // You can't delete the root element
+                    if (vi != null && !vi.isRoot()) {
+                        UiViewElementNode ui = vi.getUiViewKey();
+                        if (ui != null) {
+                            ui.deleteXmlNode();
+                        }
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Perform the "Paste" action, either from the Edit menu or from the context
+     * menu.
+     *
+     * @param selection A list of selection items to add to the clipboard;
+     *            <b>this should be a copy already - this method will not make a
+     *            copy</b>
+     */
+    public void pasteSelection(List<CanvasSelection> selection) {
+
+        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+        SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
+
+        if (pasted == null || pasted.length == 0) {
+            return;
+        }
+
+        CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
+        if (lastRoot == null) {
+            // Pasting in an empty document. Only paste the first element.
+            pasteInEmptyDocument(pasted[0]);
+            return;
+        }
+
+        // Otherwise use the current selection, if any, as a guide where to paste
+        // using the first selected element only. If there's no selection use
+        // the root as the insertion point.
+        SelectionManager.sanitize(selection);
+        CanvasViewInfo target = lastRoot;
+        if (selection.size() > 0) {
+            CanvasSelection cs = selection.get(0);
+            target = cs.getViewInfo();
+        }
+
+        NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
+
+        mCanvas.getRulesEngine().callOnPaste(targetNode, pasted);
+    }
+
+    /**
+     * Paste a new root into an empty XML layout.
+     * <p/>
+     * In case of error (unknown FQCN, document not empty), silently do nothing.
+     * In case of success, the new element will have some default attributes set (xmlns:android,
+     * layout_width and height). The edit is wrapped in a proper undo.
+     * <p/>
+     * Implementation is similar to {@link #createDocumentRoot(String)} except we also
+     * copy all the attributes and inner elements recursively.
+     */
+    private void pasteInEmptyDocument(final IDragElement pastedElement) {
+        String rootFqcn = pastedElement.getFqcn();
+
+        // Need a valid empty document to create the new root
+        final LayoutEditor layoutEditor = mCanvas.getLayoutEditor();
+        final UiDocumentNode uiDoc = layoutEditor.getUiRootNode();
+        if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
+            debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
+            return;
+        }
+
+        // Find the view descriptor matching our FQCN
+        final ViewElementDescriptor viewDesc = layoutEditor.getFqcnViewDescriptor(rootFqcn);
+        if (viewDesc == null) {
+            // TODO this could happen if pasting a custom view not known in this project
+            debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
+            return;
+        }
+
+        // Get the last segment of the FQCN for the undo title
+        String title = rootFqcn;
+        int pos = title.lastIndexOf('.');
+        if (pos > 0 && pos < title.length() - 1) {
+            title = title.substring(pos + 1);
+        }
+        title = String.format("Paste root %1$s in document", title);
+
+        layoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
+            public void run() {
+                UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
+
+                // A root node requires the Android XMLNS
+                uiNew.setAttributeValue(
+                        "android", //$NON-NLS-1$
+                        XmlnsAttributeDescriptor.XMLNS_URI,
+                        SdkConstants.NS_RESOURCES,
+                        true /*override*/);
+
+                // Copy all the attributes from the pasted element
+                for (IDragAttribute attr : pastedElement.getAttributes()) {
+                    uiNew.setAttributeValue(
+                            attr.getName(),
+                            attr.getUri(),
+                            attr.getValue(),
+                            true /*override*/);
+                }
+
+                // Adjust the attributes, adding the default layout_width/height
+                // only if they are not present (the original element should have
+                // them though.)
+                DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
+
+                uiNew.createXmlNode();
+
+                // Now process all children
+                for (IDragElement childElement : pastedElement.getInnerElements()) {
+                    addChild(uiNew, childElement);
+                }
+            }
+
+            private void addChild(UiElementNode uiParent, IDragElement childElement) {
+                String childFqcn = childElement.getFqcn();
+                final ViewElementDescriptor childDesc =
+                    layoutEditor.getFqcnViewDescriptor(childFqcn);
+                if (childDesc == null) {
+                    // TODO this could happen if pasting a custom view
+                    debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
+                    return;
+                }
+
+                UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
+
+                // Copy all the attributes from the pasted element
+                for (IDragAttribute attr : childElement.getAttributes()) {
+                    uiChild.setAttributeValue(
+                            attr.getName(),
+                            attr.getUri(),
+                            attr.getValue(),
+                            true /*override*/);
+                }
+
+                // Adjust the attributes, adding the default layout_width/height
+                // only if they are not present (the original element should have
+                // them though.)
+                DescriptorsUtils.setDefaultLayoutAttributes(
+                        uiChild, false /*updateLayout*/);
+
+                uiChild.createXmlNode();
+
+                // Now process all grand children
+                for (IDragElement grandChildElement : childElement.getInnerElements()) {
+                    addChild(uiChild, grandChildElement);
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns true if we have a a simple xml transfer data object on the
+     * clipboard.
+     *
+     * @return True if and only if the clipboard contains one of XML element
+     *         objects.
+     */
+    public boolean hasSxtOnClipboard() {
+        // The paste operation is only available if we can paste our custom type.
+        // We do not currently support pasting random text (e.g. XML). Maybe later.
+        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
+        for (TransferData td : mClipboard.getAvailableTypes()) {
+            if (sxt.isSupportedType(td)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private void debugPrintf(String message, Object... params) {
+        if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
+    }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPoint.java
new file mode 100644 (file)
index 0000000..e90371f
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * 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.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link ControlPoint} is a coordinate in the canvas control which corresponds
+ * exactly to (0,0) at the top left of the canvas. It is unaffected by canvas
+ * zooming.
+ */
+public final class ControlPoint {
+    /** Containing canvas which the point is relative to. */
+    private final LayoutCanvas mCanvas;
+
+    /** The X coordinate of the mouse coordinate. */
+    public final int x;
+
+    /** The Y coordinate of the mouse coordinate. */
+    public final int y;
+
+    /**
+     * Constructs a new {@link ControlPoint} from the given event. The event
+     * must be from a {@link MouseListener} associated with the
+     * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+     * {@link MouseEvent#y} fields are relative to the canvas.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param event The mouse event to construct the {@link ControlPoint}
+     *            from.
+     * @return A {@link ControlPoint} which corresponds to the given
+     *         {@link MouseEvent}.
+     */
+    public static ControlPoint create(LayoutCanvas canvas, MouseEvent event) {
+        // The mouse event coordinates should already be relative to the canvas
+        // widget.
+        assert event.widget == canvas : event.widget;
+        return new ControlPoint(canvas, event.x, event.y);
+    }
+
+    /**
+     * Constructs a new {@link ControlPoint} from the given event. The event
+     * must be from a {@link DragSourceListener} associated with the
+     * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+     * {@link DragSourceEvent#y} fields are relative to the canvas.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param event The mouse event to construct the {@link ControlPoint}
+     *            from.
+     * @return A {@link ControlPoint} which corresponds to the given
+     *         {@link DragSourceEvent}.
+     */
+    public static ControlPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+        // The drag source event coordinates should already be relative to the
+        // canvas widget.
+        return new ControlPoint(canvas, event.x, event.y);
+    }
+
+    /**
+     * Constructs a new {@link ControlPoint} from the given event.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param event The mouse event to construct the {@link ControlPoint}
+     *            from.
+     * @return A {@link ControlPoint} which corresponds to the given
+     *         {@link DropTargetEvent}.
+     */
+    public static ControlPoint create(LayoutCanvas canvas, DropTargetEvent event) {
+        // The drop target events are always relative to the display, so we must
+        // first convert them to be canvas relative.
+        org.eclipse.swt.graphics.Point p = canvas.toControl(event.x, event.y);
+        return new ControlPoint(canvas, p.x, p.y);
+    }
+
+    /**
+     * Constructs a new {@link ControlPoint} from the given x,y coordinates,
+     * which must be relative to the given {@link LayoutCanvas}.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param x The mouse event x coordinate relative to the canvas
+     * @param y The mouse event x coordinate relative to the canvas
+     * @return A {@link ControlPoint} which corresponds to the given
+     *         coordinates.
+     */
+    public static ControlPoint create(LayoutCanvas canvas, int x, int y) {
+        return new ControlPoint(canvas, x, y);
+    }
+
+    /**
+     * Constructs a new canvas control coordinate with the given X and Y
+     * coordinates. This is private; use one of the factory methods
+     * {@link #create(LayoutCanvas, MouseEvent)},
+     * {@link #create(LayoutCanvas, DragSourceEvent)} or
+     * {@link #create(LayoutCanvas, DropTargetEvent)} instead.
+     *
+     * @param canvas The canvas which contains this coordinate
+     * @param x The mouse x coordinate
+     * @param y The mouse y coordinate
+     */
+    private ControlPoint(LayoutCanvas canvas, int x, int y) {
+        this.mCanvas = canvas;
+        this.x = x;
+        this.y = y;
+    }
+
+    /**
+     * Returns the equivalent {@link LayoutPoint} to this
+     * {@link ControlPoint}.
+     *
+     * @return The equivalent {@link LayoutPoint} to this
+     *         {@link ControlPoint}.
+     */
+    public LayoutPoint toLayout() {
+        int lx = mCanvas.getHorizontalTransform().inverseTranslate(x);
+        int ly = mCanvas.getVerticalTransform().inverseTranslate(y);
+
+        return LayoutPoint.create(mCanvas, lx, ly);
+    }
+
+    @Override
+    public String toString() {
+        return "ControlPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + x;
+        result = prime * result + y;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ControlPoint other = (ControlPoint) obj;
+        if (x != other.x)
+            return false;
+        if (y != other.y)
+            return false;
+        if (mCanvas != other.mCanvas) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DropGesture.java
new file mode 100644 (file)
index 0000000..bb3be7f
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+
+/**
+ * A {@link DropGesture} is a {@link Gesture} which deals with drag and drop, so
+ * it has additional hooks for indicating whether the current position is
+ * "valid", and in general gets access to the system drag and drop data
+ * structures. See the {@link Gesture} documentation for more details on whether
+ * you should choose a plain {@link Gesture} or a {@link DropGesture}.
+ */
+public abstract class DropGesture extends Gesture {
+    /**
+     * The cursor has entered the drop target boundaries.
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#dragEnter(DropTargetEvent)
+     */
+    public void dragEnter(DropTargetEvent event) {
+    }
+
+    /**
+     * The cursor is moving over the drop target.
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#dragOver(DropTargetEvent)
+     */
+    public void dragOver(DropTargetEvent event) {
+    }
+
+    /**
+     * The operation being performed has changed (usually due to the user
+     * changing the selected modifier key(s) while dragging).
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#dragOperationChanged(DropTargetEvent)
+     */
+    public void dragOperationChanged(DropTargetEvent event) {
+    }
+
+    /**
+     * The cursor has left the drop target boundaries OR the drop has been
+     * canceled OR the data is about to be dropped.
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#dragLeave(DropTargetEvent)
+     */
+    public void dragLeave(DropTargetEvent event) {
+    }
+
+    /**
+     * The drop is about to be performed. The drop target is given a last chance
+     * to change the nature of the drop.
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#dropAccept(DropTargetEvent)
+     */
+    public void dropAccept(DropTargetEvent event) {
+    }
+
+    /**
+     * The data is being dropped. The data field contains java format of the
+     * data being dropped.
+     *
+     * @param event The {@link DropTargetEvent} for this drag and drop event
+     * @see DropTargetListener#drop(DropTargetEvent)
+     */
+    public void drop(final DropTargetEvent event) {
+    }
+}
index fb7ccfa..66f2327 100755 (executable)
@@ -215,7 +215,7 @@ import java.util.regex.Pattern;
             final TreeMap<String, ArrayList<MenuAction>> outActionsMap,
             final TreeMap<String, MenuAction.Group> outGroupsMap) {
         int maxMenuSelection = 0;
-        for (CanvasSelection selection : mCanvas.getCanvasSelections()) {
+        for (CanvasSelection selection : mCanvas.getSelectionManager().getSelections()) {
             List<MenuAction> viewActions = null;
             if (selection != null) {
                 CanvasViewInfo vi = selection.getViewInfo();
index 23b4cbe..fd41a0f 100755 (executable)
@@ -38,7 +38,7 @@ import java.util.Map;
 /**
  * Wraps an SWT {@link GC} into an {@link IGraphics} interface so that {@link IViewRule} objects
  * can directly draw on the canvas.
- * </p>
+ * <p/>
  * The actual wrapped GC object is only non-null during the context of a paint operation.
  */
 public class GCWrapper implements IGraphics {
@@ -212,6 +212,9 @@ public class GCWrapper implements IGraphics {
         case LINE_DASHDOTDOT:
             swtStyle = SWT.LINE_DASHDOTDOT;
             break;
+        default:
+            assert false : style;
+            break;
         }
 
         if (swtStyle != 0) {
@@ -422,12 +425,12 @@ public class GCWrapper implements IGraphics {
         mCurrentStyle = swtStyle;
     }
 
-    /** Use the stroke alpha for subsequent drawing operations */
+    /** Uses the stroke alpha for subsequent drawing operations. */
     private void useStrokeAlpha() {
         mGc.setAlpha(mCurrentStyle.getStrokeAlpha());
     }
 
-    /** Use the fill alpha for subsequent drawing operations */
+    /** Uses the fill alpha for subsequent drawing operations. */
     private void useFillAlpha() {
         mGc.setAlpha(mCurrentStyle.getFillAlpha());
     }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Gesture.java
new file mode 100644 (file)
index 0000000..c65655a
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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.events.KeyEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A gesture is a mouse or keyboard driven user operation, such as a
+ * swipe-select or a resize. It can be thought of as a session, since it is
+ * initiated, updated during user manipulation, and finally completed or
+ * canceled. A gesture is associated with a single undo transaction (although
+ * some gestures don't actually edit anything, such as a selection), and a
+ * gesture can have a number of graphics {@link Overlay}s which are added and
+ * cleaned up on behalf of the gesture by the system.
+ * <p/>
+ * Gestures are typically mouse oriented. If a mouse wishes to integrate
+ * with the native drag &amp; drop support, it should also implement
+ * the {@link DropGesture} interface, which is a sub interface of this
+ * {@link Gesture} interface. There are pros and cons to using native drag
+ * &amp; drop, so various gestures will differ in whether they use it.
+ * In particular, you should use drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Show a native drag &amp; drop cursor
+ * <li> Copy or move data, especially if this applies outside the canvas
+ *    control window or even the application itself
+ * </ul>
+ * You might want to avoid using native drag &amp; drop if your gesture should:
+ * <ul>
+ * <li> Continue updating itself even when the mouse cursor leaves the
+ *    canvas window (in a drag &amp; gesture, as soon as you leave the canvas
+ *    the drag source is no longer informed of mouse updates, whereas a regular
+ *    mouse listener is)
+ * <li> Respond to modifier keys (for example, if toggling the Shift key
+ *    should constrain motion as is common during resizing, and so on)
+ * <li> Use no special cursor (for example, during a marquee selection gesture we
+ *     don't want a native drag &amp; drop cursor)
+ *  </ul>
+ * <p/>
+ * Examples of gestures:
+ * <ul>
+ * <li>Move (dragging to reorder or change hierarchy of views or change visual
+ * layout attributes)
+ * <li>Marquee (swiping out a rectangle to make a selection)
+ * <li>Resize (dragging some edge or corner of a widget to change its size, for
+ * example to some new fixed size, or to "attach" it to some other edge.)
+ * <li>Inline Editing (editing the text of some text-oriented widget like a
+ * label or a button)
+ * <li>Link (associate two or more widgets in some way, such as an
+ *   "is required" widget linked to a text field)
+ * </ul>
+ */
+public abstract class Gesture {
+    /** Start mouse coordinate, in control coordinates. */
+    protected ControlPoint mStart;
+
+    /** Initial SWT mask when the gesture started. */
+    protected int mStartMask;
+
+    /**
+     * Returns a list of overlays, from bottom to top (where the later overlays
+     * are painted on top of earlier ones if they overlap).
+     *
+     * @return A list of overlays to paint for this gesture, if applicable.
+     *         Should not be null, but can be empty.
+     */
+    public List<Overlay> createOverlays() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Handles initialization of this gesture. Called when the gesture is
+     * starting.
+     *
+     * @param pos The most recent mouse coordinate applicable to this
+     *            gesture, relative to the canvas control.
+     * @param startMask The initial SWT mask for the gesture, if known, or
+     *            otherwise 0.
+     */
+    public void begin(ControlPoint pos, int startMask) {
+        this.mStart = pos;
+        this.mStartMask = startMask;
+    }
+
+    /**
+     * Handles updating of the gesture state for a new mouse position.
+     *
+     * @param pos The most recent mouse coordinate applicable to this
+     *            gesture, relative to the canvas control.
+     */
+    public void update(ControlPoint pos) {
+    }
+
+    /**
+     * Handles termination of the gesture. This method is called when the
+     * gesture has terminated (either through successful completion, or because
+     * it was canceled).
+     *
+     * @param pos The most recent mouse coordinate applicable to this
+     *            gesture, relative to the canvas control.
+     * @param canceled True if the gesture was canceled, and false otherwise.
+     */
+    public void end(ControlPoint pos, boolean canceled) {
+    }
+
+    /**
+     * Handles a key press during the gesture. May be called repeatedly when the
+     * user is holding the key for several seconds.
+     *
+     * @param event The SWT event for the key press,
+     */
+    public void keyPressed(KeyEvent event) {
+    }
+
+    /**
+     * Handles a key release during the gesture.
+     *
+     * @param event The SWT event for the key release,
+     */
+    public void keyReleased(KeyEvent event) {
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java
new file mode 100644 (file)
index 0000000..2bba581
--- /dev/null
@@ -0,0 +1,599 @@
+/*
+ * 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.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DragSource;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.TypedEvent;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link GestureManager} is is the central manager of gestures; it is responsible
+ * for recognizing when particular gestures should begin and terminate. It
+ * listens to the drag, mouse and keyboard systems to find out when to start
+ * gestures and in order to update the gestures along the way.
+ */
+public class GestureManager {
+    /** The canvas which owns this GestureManager. */
+    private final LayoutCanvas mCanvas;
+
+    /** The currently executing gesture, or null. */
+    private Gesture mCurrentGesture;
+
+    /** A listener for drop target events. */
+    private final DropTargetListener mDropListener = new CanvasDropListener();
+
+    /** A listener for drag source events. */
+    private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener();
+
+    /**
+     * The list of overlays associated with {@link #mCurrentGesture}. Will be
+     * null before it has been initialized lazily by the paint routine (the
+     * initialized value can never be null, but it can be an empty collection).
+     */
+    private List<Overlay> mOverlays;
+
+    /**
+     * Most recently seen mouse position (x coordinate). We keep a copy of this
+     * value since we sometimes need to know it when we aren't told about the
+     * mouse position (such as when a keystroke is received, such as an arrow
+     * key in order to tweak the current drop position)
+     */
+    protected int mLastMouseX;
+
+    /**
+     * Most recently seen mouse position (y coordinate). We keep a copy of this
+     * value since we sometimes need to know it when we aren't told about the
+     * mouse position (such as when a keystroke is received, such as an arrow
+     * key in order to tweak the current drop position)
+     */
+    protected int mLastMouseY;
+
+    /**
+     * Most recently seen mouse mask. We keep a copy of this since in some
+     * scenarios (such as on a drag gesture) we don't get access to it.
+     */
+    protected int mLastStateMask;
+
+    /**
+     * Listener for mouse motion, click and keyboard events.
+     */
+    private Listener mListener;
+
+    /**
+     * When we the drag leaves, we don't know if that's the last we'll see of
+     * this drag or if it's just temporarily outside the canvas and it will
+     * return. We want to restore it if it comes back. This is also necessary
+     * because even on a drop we'll receive a
+     * {@link DropTargetListener#dragLeave} right before the drop, and we need
+     * to restore it in the drop. Therefore, when we lose a {@link DropGesture}
+     * to a {@link DropTargetListener#dragLeave}, we store a reference to the
+     * current gesture as a {@link #mZombieGesture}, since the gesture is dead
+     * but might be brought back to life if we see a subsequent
+     * {@link DropTargetListener#dragEnter} before another gesture begins.
+     */
+    private DropGesture mZombieGesture;
+
+    /**
+     * Constructs a new {@link GestureManager} for the given
+     * {@link LayoutCanvas}.
+     *
+     * @param canvas The canvas which controls this {@link GestureManager}
+     */
+    public GestureManager(LayoutCanvas canvas) {
+        this.mCanvas = canvas;
+    }
+
+    /**
+     * Returns the canvas associated with this GestureManager.
+     *
+     * @return The {@link LayoutCanvas} associated with this GestureManager.
+     *         Never null.
+     */
+    public LayoutCanvas getCanvas() {
+        return mCanvas;
+    }
+
+    /**
+     * Returns the current gesture, if one is in progress, and otherwise returns
+     * null.
+     *
+     * @return The current gesture or null.
+     */
+    public Gesture getCurrentGesture() {
+        return mCurrentGesture;
+    }
+
+    /**
+     * Paints the overlays associated with the current gesture, if any.
+     *
+     * @param gc The graphics object to paint into.
+     */
+    public void paint(GC gc) {
+        if (mCurrentGesture == null) {
+            return;
+        }
+
+        if (mOverlays == null) {
+            mOverlays = mCurrentGesture.createOverlays();
+            Device device = gc.getDevice();
+            for (Overlay overlay : mOverlays) {
+                overlay.create(device);
+            }
+        }
+        for (Overlay overlay : mOverlays) {
+            overlay.paint(gc);
+        }
+    }
+
+    /**
+     * Returns the {@link DropTargetListener} used by the GestureManager. This
+     * is a bit leaky, but the Outline is reusing all this code... This should
+     * be separated out.
+     */
+    /* package */DropTargetListener getDropTargetListener() {
+        return mDropListener;
+    }
+
+    /**
+     * Returns the {@link DragSourceListener} used by the GestureManager. This
+     * is a bit leaky, but the Outline is reusing all this code... This should
+     * be separated out.
+     */
+    /* package */DragSourceListener getDragSourceListener() {
+        return mDragSourceListener;
+    }
+
+    /**
+     * Registers all the listeners needed by the {@link GestureManager}.
+     *
+     * @param dragSource The drag source in the {@link LayoutCanvas} to listen
+     *            to.
+     * @param dropTarget The drop target in the {@link LayoutCanvas} to listen
+     *            to.
+     */
+    public void registerListeners(DragSource dragSource, DropTarget dropTarget) {
+        assert mListener == null;
+        mListener = new Listener();
+        mCanvas.addMouseMoveListener(mListener);
+        mCanvas.addMouseListener(mListener);
+        mCanvas.addKeyListener(mListener);
+
+        if (dragSource != null) {
+            dragSource.addDragListener(mDragSourceListener);
+        }
+        if (dropTarget != null) {
+            dropTarget.addDropListener(mDropListener);
+        }
+    }
+
+    /**
+     * Unregisters all the listeners previously registered by
+     * {@link #registerListeners}.
+     *
+     * @param dragSource The drag source in the {@link LayoutCanvas} to stop
+     *            listening to.
+     * @param dropTarget The drop target in the {@link LayoutCanvas} to stop
+     *            listening to.
+     */
+    public void unregisterListeners(DragSource dragSource, DropTarget dropTarget) {
+        if (mListener != null) {
+            mCanvas.removeMouseMoveListener(mListener);
+            mCanvas.removeMouseListener(mListener);
+            mCanvas.removeKeyListener(mListener);
+            mListener = null;
+        }
+
+        if (dragSource != null) {
+            dragSource.removeDragListener(mDragSourceListener);
+        }
+        if (dropTarget != null) {
+            dropTarget.removeDropListener(mDropListener);
+        }
+    }
+
+    /**
+     * Starts the given gesture.
+     *
+     * @param mousePos The most recent mouse coordinate applicable to the new
+     *            gesture, in control coordinates.
+     * @param gesture The gesture to initiate
+     */
+    private void startGesture(ControlPoint mousePos, Gesture gesture, int mask) {
+        if (mCurrentGesture != null) {
+            finishGesture(mousePos, true);
+            assert mCurrentGesture == null;
+        }
+
+        if (gesture != null) {
+            mCurrentGesture = gesture;
+            mCurrentGesture.begin(mousePos, mask);
+        }
+    }
+
+    /**
+     * Updates the current gesture, if any, for the given event.
+     *
+     * @param mousePos The most recent mouse coordinate applicable to the new
+     *            gesture, in control coordinates.
+     * @param event The event corresponding to this update. May be null. Don't
+     *            make any assumptions about the type of this event - for
+     *            example, it may not always be a MouseEvent, it could be a
+     *            DragSourceEvent, etc.
+     */
+    private void updateMouse(ControlPoint mousePos, TypedEvent event) {
+        if (mCurrentGesture != null) {
+            mCurrentGesture.update(mousePos);
+        }
+    }
+
+    /**
+     * Finish the given gesture, either from successful completion or from
+     * cancellation.
+     *
+     * @param mousePos The most recent mouse coordinate applicable to the new
+     *            gesture, in control coordinates.
+     * @param canceled True if and only if the gesture was canceled.
+     */
+    private void finishGesture(ControlPoint mousePos, boolean canceled) {
+        if (mCurrentGesture != null) {
+            mCurrentGesture.end(mousePos, canceled);
+            if (mOverlays != null) {
+                for (Overlay overlay : mOverlays) {
+                    overlay.dispose();
+                }
+                mOverlays = null;
+            }
+            mCurrentGesture = null;
+            mZombieGesture = null;
+            mLastStateMask = 0;
+        }
+    }
+
+    /**
+     * Helper class which implements the {@link MouseMoveListener},
+     * {@link MouseListener} and {@link KeyListener} interfaces.
+     */
+    private class Listener implements MouseMoveListener, MouseListener, KeyListener {
+
+        // --- MouseMoveListener ---
+
+        public void mouseMove(MouseEvent e) {
+            mLastMouseX = e.x;
+            mLastMouseY = e.y;
+            mLastStateMask = e.stateMask;
+
+            if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+                if (mCurrentGesture != null) {
+                    ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
+                    updateMouse(controlPoint, e);
+                    mCanvas.redraw();
+                }
+            } else {
+                mCanvas.hover(e);
+            }
+        }
+
+        // --- MouseListener ---
+
+        public void mouseUp(MouseEvent e) {
+            if (mCurrentGesture == null) {
+                // Just a click, select
+                mCanvas.getSelectionManager().select(e);
+            }
+            finishGesture(ControlPoint.create(mCanvas, e), false);
+            mCanvas.redraw();
+        }
+
+        public void mouseDown(MouseEvent e) {
+            mLastMouseX = e.x;
+            mLastMouseY = e.y;
+            mLastStateMask = e.stateMask;
+
+            // Not yet used. Should be, for Mac and Linux.
+        }
+
+        public void mouseDoubleClick(MouseEvent e) {
+            mCanvas.showXml(e);
+        }
+
+        // --- KeyListener ---
+
+        public void keyPressed(KeyEvent e) {
+            if (mCurrentGesture != null) {
+                mCurrentGesture.keyPressed(e);
+            } else {
+                if (e.keyCode == SWT.ESC) {
+                    // It appears that SWT does NOT (on the Mac) pass any
+                    // key strokes other than modifier keys when the mouse
+                    // button is pressed!!
+                    ControlPoint controlPoint = ControlPoint.create(mCanvas,
+                            mLastMouseX, mLastMouseY);
+                    finishGesture(controlPoint, true);
+                    return;
+                }
+            }
+        }
+
+        public void keyReleased(KeyEvent e) {
+            if (mCurrentGesture != null) {
+                mCurrentGesture.keyReleased(e);
+            }
+        }
+
+    }
+
+    /** Listener for Drag &amp; Drop events. */
+    private class CanvasDropListener implements DropTargetListener {
+        public CanvasDropListener() {
+        }
+
+        /**
+         * The cursor has entered the drop target boundaries. {@inheritDoc}
+         */
+        public void dragEnter(DropTargetEvent event) {
+            if (mCurrentGesture == null) {
+                Gesture newGesture = mZombieGesture;
+                if (newGesture == null) {
+                    newGesture = new MoveGesture(mCanvas);
+                } else {
+                    mZombieGesture = null;
+                }
+                startGesture(ControlPoint.create(mCanvas, event),
+                        newGesture, 0);
+            }
+
+            if (mCurrentGesture instanceof DropGesture) {
+                ((DropGesture) mCurrentGesture).dragEnter(event);
+            }
+        }
+
+        /**
+         * The cursor is moving over the drop target. {@inheritDoc}
+         */
+        public void dragOver(DropTargetEvent event) {
+            if (mCurrentGesture instanceof DropGesture) {
+                ((DropGesture) mCurrentGesture).dragOver(event);
+            }
+        }
+
+        /**
+         * The cursor has left the drop target boundaries OR data is about to be
+         * dropped. {@inheritDoc}
+         */
+        public void dragLeave(DropTargetEvent event) {
+            if (mCurrentGesture instanceof DropGesture) {
+                DropGesture dropGesture = (DropGesture) mCurrentGesture;
+                dropGesture.dragLeave(event);
+                finishGesture(ControlPoint.create(mCanvas, event), true);
+                mZombieGesture = dropGesture;
+            }
+        }
+
+        /**
+         * The drop is about to be performed. The drop target is given a last
+         * chance to change the nature of the drop. {@inheritDoc}
+         */
+        public void dropAccept(DropTargetEvent event) {
+            Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+            if (gesture instanceof DropGesture) {
+                ((DropGesture) gesture).dropAccept(event);
+            }
+        }
+
+        /**
+         * The data is being dropped. {@inheritDoc}
+         */
+        public void drop(final DropTargetEvent event) {
+            // See if we had a gesture just prior to the drop (we receive a dragLeave
+            // right before the drop which we don't know whether means the cursor has
+            // left the canvas for good or just before a drop)
+            Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture;
+            mZombieGesture = null;
+
+            if (gesture instanceof DropGesture) {
+                ((DropGesture) gesture).drop(event);
+
+                finishGesture(ControlPoint.create(mCanvas, event), true);
+            }
+        }
+
+        /**
+         * The operation being performed has changed (e.g. modifier key).
+         * {@inheritDoc}
+         */
+        public void dragOperationChanged(DropTargetEvent event) {
+            if (mCurrentGesture instanceof DropGesture) {
+                ((DropGesture) mCurrentGesture).dragOperationChanged(event);
+            }
+        }
+    }
+
+    /**
+     * Our canvas {@link DragSourceListener}. Handles drag being started and
+     * finished and generating the drag data.
+     */
+    private class CanvasDragSourceListener implements DragSourceListener {
+
+        /**
+         * The current selection being dragged. This may be a subset of the
+         * canvas selection due to the "sanitize" pass. Can be empty but never
+         * null.
+         */
+        private final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
+
+        private SimpleElement[] mDragElements;
+
+        /**
+         * The user has begun the actions required to drag the widget.
+         * <p/>
+         * Initiate a drag only if there is one or more item selected. If
+         * there's none, try to auto-select the one under the cursor.
+         * {@inheritDoc}
+         */
+        public void dragStart(DragSourceEvent e) {
+            // We need a selection (simple or multiple) to do any transfer.
+            // If there's a selection *and* the cursor is over this selection,
+            // use all the currently selected elements.
+            // If there is no selection or the cursor is not over a selected
+            // element, *change* the selection to match the element under the
+            // cursor and use that. If nothing can be selected, abort the drag
+            // operation.
+
+            List<CanvasSelection> selections = mCanvas.getSelectionManager().getSelections();
+            mDragSelection.clear();
+
+            if (!selections.isEmpty()) {
+                // Is the cursor on top of a selected element?
+                LayoutPoint p = LayoutPoint.create(mCanvas, e);
+
+                boolean insideSelection = false;
+
+                for (CanvasSelection cs : selections) {
+                    if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) {
+                        insideSelection = true;
+                        break;
+                    }
+                }
+
+                if (!insideSelection) {
+                    CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+                    if (vi != null && !vi.isRoot()) {
+                        mCanvas.getSelectionManager().selectSingle(vi);
+                        insideSelection = true;
+                    }
+                }
+
+                if (insideSelection) {
+                    // We should now have a proper selection that matches the
+                    // cursor. Let's use this one. We make a copy of it since
+                    // the "sanitize" pass below might remove some of the
+                    // selected objects.
+                    if (selections.size() == 1) {
+                        // You are dragging just one element - this might or
+                        // might not be the root, but if it's the root that is
+                        // fine since we will let you drag the root if it is the
+                        // only thing you are dragging.
+                        mDragSelection.addAll(selections);
+                    } else {
+                        // Only drag non-root items.
+                        for (CanvasSelection cs : selections) {
+                            if (!cs.isRoot()) {
+                                mDragSelection.add(cs);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // If you are dragging a non-selected item, select it
+            if (mDragSelection.isEmpty()) {
+                LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+                CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+                if (vi != null && !vi.isRoot()) {
+                    mCanvas.getSelectionManager().selectSingle(vi);
+                    mDragSelection.addAll(selections);
+                }
+            }
+
+            SelectionManager.sanitize(mDragSelection);
+
+            e.doit = !mDragSelection.isEmpty();
+            if (e.doit) {
+                mDragElements = CanvasSelection.getAsElements(mDragSelection);
+                GlobalCanvasDragInfo.getInstance().startDrag(mDragElements,
+                        mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
+                        mCanvas, new Runnable() {
+                            public void run() {
+                                mCanvas.getClipboardSupport().deleteSelection("Remove",
+                                        mDragSelection);
+                            }
+                        });
+            }
+
+            // If you drag on the -background-, we make that into a marquee
+            // selection
+            if (!e.doit || (mDragSelection.size() == 1 && mDragSelection.get(0).isRoot())) {
+                boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0;
+                startGesture(ControlPoint.create(mCanvas, e),
+                        new MarqueeGesture(mCanvas, toggle), mLastStateMask);
+                e.detail = DND.DROP_NONE;
+                e.doit = false;
+            } else {
+                // Otherwise, the drag means you are moving something
+                startGesture(ControlPoint.create(mCanvas, e), new MoveGesture(mCanvas), 0);
+            }
+
+            // No hover during drag (since no mouse over events are delivered
+            // during a drag to keep the hovers up to date anyway)
+            mCanvas.clearHover();
+
+            mCanvas.redraw();
+        }
+
+        /**
+         * Callback invoked when data is needed for the event, typically right
+         * before drop. The drop side decides what type of transfer to use and
+         * this side must now provide the adequate data. {@inheritDoc}
+         */
+        public void dragSetData(DragSourceEvent e) {
+            if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
+                e.data = CanvasSelection.getAsText(mCanvas, mDragSelection);
+                return;
+            }
+
+            if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
+                e.data = mDragElements;
+                return;
+            }
+
+            // otherwise we failed
+            e.detail = DND.DROP_NONE;
+            e.doit = false;
+        }
+
+        /**
+         * Callback invoked when the drop has been finished either way. On a
+         * successful move, remove the originating elements.
+         */
+        public void dragFinished(DragSourceEvent e) {
+            // Clear the selection
+            mDragSelection.clear();
+            mDragElements = null;
+            GlobalCanvasDragInfo.getInstance().stopDrag();
+
+            finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE);
+            mCanvas.redraw();
+        }
+    }
+}
index d49ff40..330e113 100755 (executable)
@@ -31,10 +31,10 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
  * off a canvas or its palette and then set back to null when the drag'n'drop is finished.
  * <p/>
  * Note that when a drag starts in one instance of Eclipse and the dragOver/drop is done
- * in a <em>separate</em> instance of Eclipse, the tragged FQCN won't be registered here
+ * in a <em>separate</em> instance of Eclipse, the dragged FQCN won't be registered here
  * and will be null.
  */
-class GlobalCanvasDragInfo {
+final class GlobalCanvasDragInfo {
 
     private static final GlobalCanvasDragInfo sInstance = new GlobalCanvasDragInfo();
 
index 253ceed..a0fbac6 100755 (executable)
@@ -462,7 +462,7 @@ public class GraphicalEditorPart extends EditorPart
      * @param xmlNode The Node whose element we want to select
      */
     public void select(Node xmlNode) {
-        mCanvasViewer.getCanvas().select(xmlNode);
+        mCanvasViewer.getCanvas().getSelectionManager().select(xmlNode);
     }
 
     /**
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/HoverOverlay.java
new file mode 100644 (file)
index 0000000..9a39427
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link HoverOverlay} paints an optional hover on top of the layout,
+ * highlighting the currently hovered view.
+ */
+public class HoverOverlay extends Overlay {
+    /** Hover border color. Must be disposed, it's NOT a system color. */
+    private Color mHoverStrokeColor;
+
+    /** Hover fill color. Must be disposed, it's NOT a system color. */
+    private Color mHoverFillColor;
+
+    /** Vertical scaling & scrollbar information. */
+    private ScaleInfo mVScale;
+
+    /** Horizontal scaling & scrollbar information. */
+    private ScaleInfo mHScale;
+
+    /**
+     * Current mouse hover border rectangle. Null when there's no mouse hover.
+     * The rectangle coordinates do not take account of the translation, which
+     * must be applied to the rectangle when drawing.
+     */
+    private Rectangle mHoverRect;
+
+    /**
+     * Constructs a new {@link HoverOverlay} linked to the given view hierarchy.
+     *
+     * @param hScale The {@link ScaleInfo} to use to transfer horizontal layout
+     *            coordinates to screen coordinates.
+     * @param vScale The {@link ScaleInfo} to use to transfer vertical layout
+     *            coordinates to screen coordinates.
+     */
+    public HoverOverlay(ScaleInfo hScale, ScaleInfo vScale) {
+        super();
+        this.mHScale = hScale;
+        this.mVScale = vScale;
+    }
+
+    @Override
+    public void create(Device device) {
+        if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
+            mHoverStrokeColor = new Color(device, SwtDrawingStyle.HOVER.getStrokeColor());
+        }
+        if (SwtDrawingStyle.HOVER.getFillColor() != null) {
+            mHoverFillColor = new Color(device, SwtDrawingStyle.HOVER.getFillColor());
+        }
+    }
+
+    @Override
+    public void dispose() {
+        if (mHoverStrokeColor != null) {
+            mHoverStrokeColor.dispose();
+            mHoverStrokeColor = null;
+        }
+
+        if (mHoverFillColor != null) {
+            mHoverFillColor.dispose();
+            mHoverFillColor = null;
+        }
+    }
+
+    /**
+     * Sets the hover rectangle. The coordinates of the rectangle are in layout
+     * coordinates. The recipient is will own this rectangle.
+     * <p/>
+     * TODO: Consider switching input arguments to two {@link LayoutPoint}s so
+     * we don't have ambiguity about the coordinate system of these input
+     * parameters.
+     * <p/>
+     *
+     * @param x The top left x coordinate, in layout coordinates, of the hover.
+     * @param y The top left y coordinate, in layout coordinates, of the hover.
+     * @param w The width of the hover (in layout coordinates).
+     * @param h The height of the hover (in layout coordinates).
+     */
+    public void setHover(int x, int y, int w, int h) {
+        mHoverRect = new Rectangle(x, y, w, h);
+    }
+
+    /**
+     * Removes the hover for the next paint.
+     */
+    public void clearHover() {
+        mHoverRect = null;
+    }
+
+    @Override
+    public void paint(GC gc) {
+        if (mHoverRect != null) {
+            // Translate the hover rectangle (in canvas coordinates) to control
+            // coordinates
+            int x = mHScale.translate(mHoverRect.x);
+            int y = mVScale.translate(mHoverRect.y);
+            int w = mHScale.scale(mHoverRect.width);
+            int h = mVScale.scale(mHoverRect.height);
+
+            if (mHoverStrokeColor != null) {
+                int oldAlpha = gc.getAlpha();
+                gc.setForeground(mHoverStrokeColor);
+                gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle());
+                gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha());
+                gc.drawRectangle(x, y, w, h);
+                gc.setAlpha(oldAlpha);
+            }
+
+            if (mHoverFillColor != null) {
+                int oldAlpha = gc.getAlpha();
+                gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha());
+                gc.setBackground(mHoverFillColor);
+                gc.fillRectangle(x, y, w, h);
+                gc.setAlpha(oldAlpha);
+            }
+        }
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageOverlay.java
new file mode 100644 (file)
index 0000000..4817eb8
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * 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.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+
+/**
+ * The {@link ImageOverlay} class renders an image as an overlay.
+ */
+public class ImageOverlay extends Overlay {
+    /** Current background image. Null when there's no image. */
+    private Image mImage;
+
+    /** The associated {@link LayoutCanvas}. */
+    private LayoutCanvas mCanvas;
+
+    /** Vertical scaling & scrollbar information. */
+    private ScaleInfo mVScale;
+
+    /** Horizontal scaling & scrollbar information. */
+    private ScaleInfo mHScale;
+
+    /**
+     * Constructs an {@link ImageOverlay} tied to the given canvas.
+     *
+     * @param canvas The {@link LayoutCanvas} to paint the overlay over.
+     * @param hScale The horizontal scale information.
+     * @param vScale The vertical scale information.
+     */
+    public ImageOverlay(LayoutCanvas canvas, ScaleInfo hScale, ScaleInfo vScale) {
+        this.mCanvas = canvas;
+        this.mHScale = hScale;
+        this.mVScale = vScale;
+    }
+
+    @Override
+    public void create(Device device) {
+        super.create(device);
+    }
+
+    @Override
+    public void dispose() {
+        if (mImage != null) {
+            mImage.dispose();
+            mImage = null;
+        }
+    }
+
+    /**
+     * Sets the image to be drawn as an overlay from the passed in AWT
+     * {@link BufferedImage} (which will be converted to an SWT image).
+     * <p/>
+     * The image <b>can</b> be null, which is the case when we are dealing with
+     * an empty document.
+     *
+     * @param awtImage The AWT image to be rendered as an SWT image.
+     * @return The corresponding SWT image, or null.
+     */
+    public Image setImage(BufferedImage awtImage) {
+        if (mImage != null) {
+            mImage.dispose();
+        }
+        if (awtImage == null) {
+            mImage = null;
+
+        } else {
+            int width = awtImage.getWidth();
+            int height = awtImage.getHeight();
+
+            Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
+            int[] imageDataBuffer = ((DataBufferInt) raster.getDataBuffer()).getData();
+
+            ImageData imageData = new ImageData(width, height, 32, new PaletteData(0x00FF0000,
+                    0x0000FF00, 0x000000FF));
+
+            imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+            mImage = new Image(mCanvas.getDisplay(), imageData);
+        }
+
+        return mImage;
+    }
+
+    @Override
+    public void paint(GC gc) {
+        if (mImage != null) {
+            boolean valid = mCanvas.getViewHierarchy().isValid();
+            if (!valid) {
+                gc_setAlpha(gc, 128); // half-transparent
+            }
+
+            ScaleInfo hi = mHScale;
+            ScaleInfo vi = mVScale;
+
+            // we only anti-alias when reducing the image size.
+            int oldAlias = -2;
+            if (hi.getScale() < 1.0) {
+                oldAlias = gc_setAntialias(gc, SWT.ON);
+            }
+
+            gc.drawImage(
+                    mImage,
+                    0,                      // srcX
+                    0,                      // srcY
+                    hi.getImgSize(),        // srcWidth
+                    vi.getImgSize(),        // srcHeight
+                    hi.translate(0),        // destX
+                    vi.translate(0),        // destY
+                    hi.getScalledImgSize(), // destWidth
+                    vi.getScalledImgSize());  // destHeight
+
+            if (oldAlias != -2) {
+                gc_setAntialias(gc, oldAlias);
+            }
+
+            if (!valid) {
+                gc_setAlpha(gc, 255); // opaque
+            }
+        }
+    }
+
+    /**
+     * Sets the alpha for the given GC.
+     * <p/>
+     * Alpha may not work on all platforms and may fail with an exception, which
+     * is hidden here (false is returned in that case).
+     *
+     * @param gc the GC to change
+     * @param alpha the new alpha, 0 for transparent, 255 for opaque.
+     * @return True if the operation worked, false if it failed with an
+     *         exception.
+     * @see GC#setAlpha(int)
+     */
+    private boolean gc_setAlpha(GC gc, int alpha) {
+        try {
+            gc.setAlpha(alpha);
+            return true;
+        } catch (SWTException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Sets the non-text antialias flag for the given GC.
+     * <p/>
+     * Antialias may not work on all platforms and may fail with an exception,
+     * which is hidden here (-2 is returned in that case).
+     *
+     * @param gc the GC to change
+     * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
+     * @return The previous aliasing mode if the operation worked, or -2 if it
+     *         failed with an exception.
+     * @see GC#setAntialias(int)
+     */
+    private int gc_setAntialias(GC gc, int alias) {
+        try {
+            int old = gc.getAntialias();
+            gc.setAntialias(alias);
+            return old;
+        } catch (SWTException e) {
+            return -2;
+        }
+    }
+
+}
index ebb053a..c97013b 100755 (executable)
 
 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
 
-import com.android.ide.common.api.IDragElement;
 import com.android.ide.common.api.INode;
 import com.android.ide.common.api.Point;
-import com.android.ide.common.api.Rect;
-import com.android.ide.common.api.IDragElement.IDragAttribute;
 import com.android.ide.eclipse.adt.AdtPlugin;
-import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
-import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
 import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
-import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
-import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
 import com.android.layoutlib.api.ILayoutResult;
-import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
 import com.android.sdklib.SdkConstants;
 
-import org.eclipse.core.runtime.ListenerList;
-import org.eclipse.gef.ui.parts.TreeViewer;
 import org.eclipse.jface.action.Action;
 import org.eclipse.jface.action.ActionContributionItem;
 import org.eclipse.jface.action.IAction;
@@ -48,79 +38,39 @@ import org.eclipse.jface.action.IContributionItem;
 import org.eclipse.jface.action.IMenuManager;
 import org.eclipse.jface.action.MenuManager;
 import org.eclipse.jface.action.Separator;
-import org.eclipse.jface.text.BadLocationException;
-import org.eclipse.jface.util.SafeRunnable;
-import org.eclipse.jface.viewers.ISelection;
-import org.eclipse.jface.viewers.ISelectionChangedListener;
-import org.eclipse.jface.viewers.ISelectionProvider;
-import org.eclipse.jface.viewers.ITreeSelection;
-import org.eclipse.jface.viewers.SelectionChangedEvent;
-import org.eclipse.jface.viewers.TreePath;
-import org.eclipse.jface.viewers.TreeSelection;
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.SWTException;
-import org.eclipse.swt.dnd.Clipboard;
 import org.eclipse.swt.dnd.DND;
 import org.eclipse.swt.dnd.DragSource;
-import org.eclipse.swt.dnd.DragSourceEvent;
 import org.eclipse.swt.dnd.DragSourceListener;
 import org.eclipse.swt.dnd.DropTarget;
 import org.eclipse.swt.dnd.DropTargetListener;
 import org.eclipse.swt.dnd.TextTransfer;
 import org.eclipse.swt.dnd.Transfer;
-import org.eclipse.swt.dnd.TransferData;
 import org.eclipse.swt.events.ControlAdapter;
 import org.eclipse.swt.events.ControlEvent;
 import org.eclipse.swt.events.KeyEvent;
 import org.eclipse.swt.events.KeyListener;
 import org.eclipse.swt.events.MouseEvent;
-import org.eclipse.swt.events.MouseListener;
-import org.eclipse.swt.events.MouseMoveListener;
 import org.eclipse.swt.events.PaintEvent;
 import org.eclipse.swt.events.PaintListener;
-import org.eclipse.swt.events.SelectionAdapter;
-import org.eclipse.swt.events.SelectionEvent;
-import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.graphics.ImageData;
-import org.eclipse.swt.graphics.PaletteData;
 import org.eclipse.swt.graphics.Rectangle;
 import org.eclipse.swt.widgets.Canvas;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Menu;
-import org.eclipse.swt.widgets.ScrollBar;
 import org.eclipse.ui.IActionBars;
 import org.eclipse.ui.actions.ActionFactory;
 import org.eclipse.ui.actions.ContributionItemFactory;
 import org.eclipse.ui.actions.TextActionHandler;
 import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
 import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
-import org.eclipse.ui.internal.registry.ViewDescriptor;
 import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
-import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
-import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
-import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
-import org.eclipse.wst.xml.core.internal.document.NodeContainer;
 import org.w3c.dom.Node;
 
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.Raster;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Set;
-
 /**
  * Displays the image rendered by the {@link GraphicalEditorPart} and handles
  * the interaction with the widgets.
@@ -129,25 +79,15 @@ import java.util.Set;
  * actually uses the {@link LayoutCanvasViewer}, which is a JFace viewer wrapper
  * around this control.
  * <p/>
- * This class implements {@link ISelectionProvider} so that it can delegate
- * the selection provider from the {@link LayoutCanvasViewer}.
- * <p/>
- * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
- * control so that it can invoke its own fireSelectionChanged when the control's
- * selection changes.
+ * The LayoutCanvas contains the painting logic for the canvas. Selection,
+ * clipboard, view management etc. is handled in separate helper classes.
  *
  * @since GLE2
  */
-/*
- * TODO list:
- * - gray on error, keep select but disable d'n'd.
- * - context menu: enum clear, flag values, toggles as tri-states
- * - context menu: impl custom layout width/height
- * - properly handle custom views
- */
-class LayoutCanvas extends Canvas implements ISelectionProvider {
+@SuppressWarnings("restriction") // For WorkBench "Show In" support
+class LayoutCanvas extends Canvas {
 
-    private final static boolean DEBUG = false;
+    private static final boolean DEBUG = false;
 
     /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_";
 
@@ -157,43 +97,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     /** The Rules Engine, associated with the current project. */
     private RulesEngine mRulesEngine;
 
-    /** SWT clipboard instance. */
-    private Clipboard mClipboard;
-
-    /**
-     * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)}
-     * with a valid layout.
-     * <p/>
-     * This <em>can</em> be null to indicate we're dealing with an empty document with
-     * no root node. Null here does not mean the result was invalid, merely that the XML
-     * had no content to display -- we need to treat an empty document as valid so that
-     * we can drop new items in it.
-     */
-    private CanvasViewInfo mLastValidViewInfoRoot;
-
-    /**
-     * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}.
-     * <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.
-     * <p/>
-     * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
-     * valid since it is an acceptable drop target.
-     */
-    private boolean mIsResultValid;
-
-    /** Current background image. Null when there's no image. */
-    private Image mImage;
-
-    /** The current selection list. The list is never null, however it can be empty. */
-    private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
-
-    /** An unmodifiable view of {@link #mSelections}. */
-    private List<CanvasSelection> mUnmodifiableSelection;
-
-    /** CanvasSelection border color. Do not dispose, it's a system color. */
-    private Color mSelectionFgColor;
-
     /** GC wrapper given to the IViewRule methods. The GC itself is only defined in the
      *  context of {@link #onPaint(PaintEvent)}; otherwise it is null. */
     private GCWrapper mGCWrapper;
@@ -204,36 +107,12 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     /** Current hover view info. Null when no mouse hover. */
     private CanvasViewInfo mHoverViewInfo;
 
-    /** Current mouse hover border rectangle. Null when there's no mouse hover.
-     * The rectangle coordinates do not take account of the translation, which must
-     * be applied to the rectangle when drawing.
-     */
-    private Rectangle mHoverRect;
-
-    /** Hover border color. Must be disposed, it's NOT a system color. */
-    private Color mHoverStrokeColor;
-
-    /** Hover fill color. Must be disposed, it's NOT a system color. */
-    private Color mHoverFillColor;
-
-    /** Outline color. Must be disposed, it's NOT a system color. */
-    private Color mOutlineColor;
-
-    /**
-     * The <em>current</em> alternate selection, if any, which changes when the Alt key is
-     * used during a selection. Can be null.
-     */
-    private CanvasAlternateSelection mAltSelection;
-
     /** When true, always display the outline of all views. */
     private boolean mShowOutline;
 
     /** Drop target associated with this composite. */
     private DropTarget mDropTarget;
 
-    /** Drop listener, with feedback from current drop */
-    private CanvasDropListener mDropListener;
-
     /** Factory that can create {@link INode} proxies. */
     private final NodeFactory mNodeFactory = new NodeFactory();
 
@@ -246,9 +125,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     /** Drag source associated with this canvas. */
     private DragSource mDragSource;
 
-    /** List of clients listening to selection changes. */
-    private final ListenerList mSelectionListeners = new ListenerList();
-
     /**
      * The current Outline Page, to set its model.
      * It isn't possible to call OutlinePage2.dispose() in this.dispose().
@@ -258,10 +134,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
      **/
     private OutlinePage2 mOutlinePage;
 
-    /** Barrier set when updating the selection to prevent from recursively
-     * invoking ourselves. */
-    private boolean mInsideUpdateSelection;
-
     /** Delete action for the Edit or context menu. */
     private Action mDeleteAction;
 
@@ -280,8 +152,34 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     /** Root of the context menu. */
     private MenuManager mMenuManager;
 
-    private CanvasDragSourceListener mDragSourceListener;
+    /** The view hierarchy associated with this canvas. */
+    private final ViewHierarchy mViewHierarchy = new ViewHierarchy(this);
+
+    /** The selection in the canvas. */
+    private final SelectionManager mSelectionManager = new SelectionManager(this);
+
+    /** The overlay which paints the optional outline. */
+    private OutlineOverlay mOutlineOverlay;
+
+    /** The overlay which paints the mouse hover. */
+    private HoverOverlay mHoverOverlay;
 
+    /** The overlay which paints the selection. */
+    private SelectionOverlay mSelectionOverlay;
+
+    /** The overlay which paints the rendered layout image. */
+    private ImageOverlay mImageOverlay;
+
+    /**
+     * Gesture Manager responsible for identifying mouse, keyboard and drag and
+     * drop events.
+     */
+    private final GestureManager mGestureManager = new GestureManager(this);
+
+    /**
+     * Native clipboard support.
+     */
+    private ClipboardSupport mClipboardSupport;
 
     public LayoutCanvas(LayoutEditor layoutEditor,
             RulesEngine rulesEngine,
@@ -291,25 +189,25 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         mLayoutEditor = layoutEditor;
         mRulesEngine = rulesEngine;
 
-        mClipboard = new Clipboard(parent.getDisplay());
-
-        mHScale = new ScaleInfo(getHorizontalBar());
-        mVScale = new ScaleInfo(getVerticalBar());
+        mClipboardSupport = new ClipboardSupport(this, parent);
+        mHScale = new ScaleInfo(this, getHorizontalBar());
+        mVScale = new ScaleInfo(this, getVerticalBar());
 
         mGCWrapper = new GCWrapper(mHScale, mVScale);
 
-        Display d = getDisplay();
-        mSelectionFgColor = new Color(d, SwtDrawingStyle.SELECTION.getStrokeColor());
-        if (SwtDrawingStyle.HOVER.getStrokeColor() != null) {
-            mHoverStrokeColor = new Color(d, SwtDrawingStyle.HOVER.getStrokeColor());
-        }
-        if (SwtDrawingStyle.HOVER.getFillColor() != null) {
-            mHoverFillColor = new Color(d, SwtDrawingStyle.HOVER.getFillColor());
-        }
-        mOutlineColor = new Color(d, SwtDrawingStyle.OUTLINE.getStrokeColor());
+        Display display = getDisplay();
+        mFont = display.getSystemFont();
 
-        mFont = d.getSystemFont();
+        // --- Set up graphic overlays
+        // mOutlineOverlay is initialized lazily
+        mHoverOverlay = new HoverOverlay(mHScale, mVScale);
+        mHoverOverlay.create(display);
+        mSelectionOverlay = new SelectionOverlay();
+        mSelectionOverlay.create(display);
+        mImageOverlay = new ImageOverlay(this, mHScale, mVScale);
+        mImageOverlay.create(display);
 
+        // --- Set up listeners
         addPaintListener(new PaintListener() {
             public void paintControl(PaintEvent e) {
                 onPaint(e);
@@ -325,26 +223,6 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
             }
         });
 
-        addMouseMoveListener(new MouseMoveListener() {
-            public void mouseMove(MouseEvent e) {
-                onMouseMove(e);
-            }
-        });
-
-        addMouseListener(new MouseListener() {
-            public void mouseUp(MouseEvent e) {
-                onMouseUp(e);
-            }
-
-            public void mouseDown(MouseEvent e) {
-                onMouseDown(e);
-            }
-
-            public void mouseDoubleClick(MouseEvent e) {
-                onDoubleClick(e);
-            }
-        });
-
         addKeyListener(new KeyListener() {
 
             public void keyPressed(KeyEvent e) {
@@ -365,11 +243,14 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         // --- setup drag'n'drop ---
         // DND Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
 
-        mDropListener = new CanvasDropListener(this);
-        mDropTarget = createDropTarget(this, mDropListener);
+        mDropTarget = createDropTarget(this);
+        mDragSource = createDragSource(this);
+        mGestureManager.registerListeners(mDragSource, mDropTarget);
 
-        mDragSourceListener = new CanvasDragSourceListener();
-        mDragSource = createDragSource(this, mDragSourceListener);
+        if (mLayoutEditor == null) {
+            // TODO: In another CL we should use EasyMock/objgen to provide an editor.
+            return; // Unit test
+        }
 
         // --- setup context menu ---
         setupGlobalActionHandlers();
@@ -387,25 +268,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     public void dispose() {
         super.dispose();
 
-        if (mSelectionFgColor != null) {
-            mSelectionFgColor.dispose();
-            mSelectionFgColor = null;
-        }
-
-        if (mOutlineColor != null) {
-            mOutlineColor.dispose();
-            mOutlineColor = null;
-        }
-
-        if (mHoverStrokeColor != null) {
-            mHoverStrokeColor.dispose();
-            mHoverStrokeColor = null;
-        }
-
-        if (mHoverFillColor != null) {
-            mHoverFillColor.dispose();
-            mHoverFillColor = null;
-        }
+        mGestureManager.unregisterListeners(mDragSource, mDropTarget);
 
         if (mDropTarget != null) {
             mDropTarget.dispose();
@@ -422,41 +285,35 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
             mDragSource = null;
         }
 
-        if (mClipboard != null) {
-            mClipboard.dispose();
-            mClipboard = null;
-        }
-
-        if (mImage != null) {
-            mImage.dispose();
-            mImage = null;
+        if (mClipboardSupport != null) {
+            mClipboardSupport.dispose();
+            mClipboardSupport = null;
         }
 
         if (mGCWrapper != null) {
             mGCWrapper.dispose();
             mGCWrapper = null;
         }
-    }
 
-    /**
-     * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
-     * {@link ILayoutResult}.
-     * <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.
-     * <p/>
-     * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
-     * valid since it is an acceptable drop target.
-    */
-    /* package */ boolean isResultValid() {
-        return mIsResultValid;
-    }
+        if (mOutlineOverlay != null) {
+            mOutlineOverlay.dispose();
+            mOutlineOverlay = null;
+        }
 
-    /**
-     * Returns true if the last valid content of the canvas represents an empty document.
-     */
-    /* package */ boolean isEmptyDocument() {
-        return mLastValidViewInfoRoot == null;
+        if (mHoverOverlay != null) {
+            mHoverOverlay.dispose();
+            mHoverOverlay = null;
+        }
+
+        if (mSelectionOverlay != null) {
+            mSelectionOverlay.dispose();
+            mSelectionOverlay = null;
+        }
+
+        if (mImageOverlay != null) {
+            mImageOverlay.dispose();
+            mImageOverlay = null;
+        }
     }
 
     /** Returns the Rules Engine, associated with the current project. */
@@ -482,7 +339,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
      * This is used by {@link OutlinePage2} to delegate drag source events.
      */
     /* package */ DragSourceListener getDragListener() {
-        return mDragSourceListener;
+        return mGestureManager.getDragSourceListener();
     }
 
     /**
@@ -490,21 +347,16 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
      * This is used by {@link OutlinePage2} to delegate drop target events.
      */
     /* package */ DropTargetListener getDropListener() {
-        return mDropListener;
+        return mGestureManager.getDropTargetListener();
     }
 
     /**
-     * Returns the native {@link CanvasSelection} list.
+     * Returns the GCWrapper used to paint view rules.
      *
-     * @return An immutable list of {@link CanvasSelection}. Can be empty but not null.
-     * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer}
-     *                      compatible {@link ISelection}.
+     * @return The GCWrapper used to paint view rules
      */
-    /* package */ List<CanvasSelection> getCanvasSelections() {
-        if (mUnmodifiableSelection == null) {
-            mUnmodifiableSelection = Collections.unmodifiableList(mSelections);
-        }
-        return mUnmodifiableSelection;
+    /* package */ GCWrapper getGcWrapper() {
+        return mGCWrapper;
     }
 
     /**
@@ -515,6 +367,58 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     }
 
     /**
+     * Returns the horizontal {@link ScaleInfo} transform object, which can map
+     * a layout point into a control point.
+     *
+     * @return A {@link ScaleInfo} for mapping between layout and control
+     *         coordinates in the horizontal dimension.
+     */
+    /* package */ ScaleInfo getHorizontalTransform() {
+        return mHScale;
+    }
+
+    /**
+     * Returns the vertical {@link ScaleInfo} transform object, which can map a
+     * layout point into a control point.
+     *
+     * @return A {@link ScaleInfo} for mapping between layout and control
+     *         coordinates in the vertical dimension.
+     */
+    /* package */ ScaleInfo getVerticalTransform() {
+        return mVScale;
+    }
+
+    /**
+     * Returns the {@link SelectionManager} associated with this canvas.
+     *
+     * @return The {@link SelectionManager} holding the selection for this
+     *         canvas. Never null.
+     */
+    public SelectionManager getSelectionManager() {
+        return mSelectionManager;
+    }
+
+    /**
+     * Returns the {@link ViewHierarchy} object associated with this canvas,
+     * holding the most recent rendered view of the scene, if valid.
+     *
+     * @return The {@link ViewHierarchy} object associated with this canvas.
+     *         Never null.
+     */
+    public ViewHierarchy getViewHierarchy() {
+        return mViewHierarchy;
+    }
+
+    /**
+     * Returns the {@link ClipboardSupport} object associated with this canvas.
+     *
+     * @return The {@link ClipboardSupport} object for this canvas. Null only after dispose.
+     */
+    public ClipboardSupport getClipboardSupport() {
+        return mClipboardSupport;
+    }
+
+    /**
      * 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.
      *
@@ -526,47 +430,17 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
      */
     /* package */ void setResult(ILayoutResult result) {
         // disable any hover
-        mHoverRect = null;
-
-        mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
+        clearHover();
 
-        if (mIsResultValid && result != null) {
-            ILayoutViewInfo root = result.getRootView();
-            if (root == null) {
-                mLastValidViewInfoRoot = null;
-            } else {
-                mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
-            }
-            setImage(result.getImage());
-
-            updateNodeProxies(mLastValidViewInfoRoot);
-            mOutlinePage.setModel(mLastValidViewInfoRoot);
-
-            // Check if the selection is still the same (based on the object keys)
-            // and eventually recompute their bounds.
-            for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
-                CanvasSelection s = it.next();
-
-                // Check if the selected object still exists
-                Object key = s.getViewInfo().getUiViewKey();
-                CanvasViewInfo 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 CanvasSelection(vi, mRulesEngine, mNodeFactory));
-                }
-            }
-            fireSelectionChanged();
+        mViewHierarchy.setResult(result);
+        if (mViewHierarchy.isValid() && result != null) {
+            Image image = mImageOverlay.setImage(result.getImage());
 
-            // remove the current alternate selection views
-            mAltSelection = null;
+            mOutlinePage.setModel(mViewHierarchy.getRoot());
 
-            if (mImage != null) {
-                mHScale.setSize(mImage.getImageData().width, getClientArea().width);
-                mVScale.setSize(mImage.getImageData().height, getClientArea().height);
+            if (image != null) {
+                mHScale.setSize(image.getImageData().width, getClientArea().width);
+                mVScale.setSize(image.getImageData().height, getClientArea().height);
             }
 
             // Pre-load the android.view.View rule in the Rules Engine. Doing it here means
@@ -596,157 +470,19 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     }
 
     /**
-     * Transforms a point, expressed in SWT display coordinates
-     * (e.g. from a Drag'n'Drop {@link Event}, not local {@link Control} coordinates)
-     * into the canvas' image coordinates according to the current zoom and scroll.
-     *
-     * @param displayX X in SWT display coordinates
-     * @param displayY Y in SWT display coordinates
-     * @return A new {@link Point} in canvas coordinates
-     */
-    /* package */ Point displayToCanvasPoint(int displayX, int displayY) {
-        // convert screen coordinates to local SWT control coordinates
-        org.eclipse.swt.graphics.Point p = this.toControl(displayX, displayY);
-
-        int x = mHScale.inverseTranslate(p.x);
-        int y = mVScale.inverseTranslate(p.y);
-        return new Point(x, y);
-    }
-
-    /**
-     * Transforms a point, expressed in canvas coordinates, into "client" coordinates
-     * relative to the control (and not relative to the display.)
+     * Transforms a point, expressed in layout coordinates, into "client" coordinates
+     * relative to the control (and not relative to the display).
      *
      * @param canvasX X in the canvas coordinates
      * @param canvasY Y in the canvas coordinates
      * @return A new {@link Point} in control client coordinates (not display coordinates)
      */
-    /* package */ Point canvasToControlPoint(int canvasX, int canvasY) {
+    /* package */ Point layoutToControlPoint(int canvasX, int canvasY) {
         int x = mHScale.translate(canvasX);
         int y = mVScale.translate(canvasY);
         return new Point(x, y);
     }
 
-    //----
-    // Implementation of ISelectionProvider
-
-    /**
-     * Returns a {@link TreeSelection} compatible with a TreeViewer
-     * where each {@link TreePath} item is actually a {@link CanvasViewInfo}.
-     */
-    public ISelection getSelection() {
-        if (mSelections.isEmpty()) {
-            return TreeSelection.EMPTY;
-        }
-
-        ArrayList<TreePath> paths = new ArrayList<TreePath>();
-
-        for (CanvasSelection cs : mSelections) {
-            CanvasViewInfo vi = cs.getViewInfo();
-            if (vi != null) {
-                ArrayList<Object> segments = new ArrayList<Object>();
-                while (vi != null) {
-                    segments.add(0, vi);
-                    vi = vi.getParent();
-                }
-                paths.add(new TreePath(segments.toArray()));
-            }
-        }
-
-        return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
-    }
-
-    /**
-     * Sets the selection. It must be an {@link ITreeSelection} where each segment
-     * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
-     * as an empty selection.
-     * <p/>
-     * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
-     * in response to an <em>outside</em> selection (compatible with ours) that has
-     * changed. Typically it means the outline selection has changed and we're
-     * synchronizing ours to match.
-     */
-    public void setSelection(ISelection selection) {
-        if (mInsideUpdateSelection) {
-            return;
-        }
-
-        try {
-            mInsideUpdateSelection = true;
-
-            if (selection == null) {
-                selection = TreeSelection.EMPTY;
-            }
-
-            if (selection instanceof ITreeSelection) {
-                ITreeSelection treeSel = (ITreeSelection) selection;
-
-                if (treeSel.isEmpty()) {
-                    // Clear existing selection, if any
-                    if (!mSelections.isEmpty()) {
-                        mSelections.clear();
-                        mAltSelection = null;
-                        redraw();
-                    }
-                    return;
-                }
-
-                boolean changed = false;
-
-                // Create a list of all currently selected view infos
-                Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
-                for (CanvasSelection cs : mSelections) {
-                    oldSelected.add(cs.getViewInfo());
-                }
-
-                // Go thru new selection and take care of selecting new items
-                // or marking those which are the same as in the current selection
-                for (TreePath path : treeSel.getPaths()) {
-                    Object seg = path.getLastSegment();
-                    if (seg instanceof CanvasViewInfo) {
-                        CanvasViewInfo newVi = (CanvasViewInfo) seg;
-                        if (oldSelected.contains(newVi)) {
-                            // This view info is already selected. Remove it from the
-                            // oldSelected list so that we don't de-select it later.
-                            oldSelected.remove(newVi);
-                        } else {
-                            // This view info is not already selected. Select it now.
-
-                            // reset alternate selection if any
-                            mAltSelection = null;
-                            // otherwise add it.
-                            mSelections.add(
-                                    new CanvasSelection(newVi, mRulesEngine, mNodeFactory));
-                            changed = true;
-                        }
-                    }
-                }
-
-                // De-select old selected items that are not in the new one
-                for (CanvasViewInfo vi : oldSelected) {
-                    deselect(vi);
-                    changed = true;
-                }
-
-                if (changed) {
-                    redraw();
-                    updateMenuActions();
-                }
-
-            }
-        } finally {
-            mInsideUpdateSelection = false;
-        }
-    }
-
-    public void addSelectionChangedListener(ISelectionChangedListener listener) {
-        mSelectionListeners.add(listener);
-    }
-
-    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
-        mSelectionListeners.remove(listener);
-    }
-
     /**
      * Returns the action for the context menu corresponding to the given action id.
      * <p/>
@@ -776,1165 +512,152 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         return null;
     }
 
-    //---
+    //---------------
 
     /**
-     * Helper class to convert between control pixel coordinates and canvas coordinates.
-     * Takes care of the zooming and offset of the canvas.
+     * Paints the canvas in response to paint events.
      */
-    private class ScaleInfo implements ICanvasTransform {
-        /** Canvas image size (original, before zoom), in pixels */
-        private int mImgSize;
-
-        /** Client size, in pixels */
-        private int mClientSize;
-
-        /** Left-top offset in client pixel coordinates */
-        private int mTranslate;
-
-        /** Scaling factor, > 0 */
-        private double mScale;
-
-        /** Scrollbar widget */
-        ScrollBar mScrollbar;
-
-        public ScaleInfo(ScrollBar scrollbar) {
-            mScrollbar = scrollbar;
-            mScale = 1.0;
-            mTranslate = 0;
+    private void onPaint(PaintEvent e) {
+        GC gc = e.gc;
+        gc.setFont(mFont);
+        mGCWrapper.setGC(gc);
+        try {
+            mImageOverlay.paint(gc);
 
-            mScrollbar.addSelectionListener(new SelectionAdapter() {
-                @Override
-                public void widgetSelected(SelectionEvent e) {
-                    // User requested scrolling. Changes translation and redraw canvas.
-                    mTranslate = mScrollbar.getSelection();
-                    redraw();
+            if (mShowOutline) {
+                if (mOutlineOverlay == null) {
+                    mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale);
+                    mOutlineOverlay.create(getDisplay());
                 }
-            });
-        }
-
-        /**
-         * Sets the new scaling factor. Recomputes scrollbars.
-         * @param scale Scaling factor, > 0.
-         */
-        public void setScale(double scale) {
-            if (mScale != scale) {
-                mScale = scale;
-                resizeScrollbar();
+                mOutlineOverlay.paint(gc);
             }
-        }
-
-        /** Returns current scaling factor. */
-        public double getScale() {
-            return mScale;
-        }
 
-        /** Returns Canvas image size (original, before zoom), in pixels. */
-        public int getImgSize() {
-            return mImgSize;
-        }
+            mHoverOverlay.paint(gc);
+            mSelectionOverlay.paint(mSelectionManager, gc, mGCWrapper, mRulesEngine);
+            mGestureManager.paint(gc);
 
-        /** Returns the scaled image size in pixels. */
-        public int getScalledImgSize() {
-            return (int) (mImgSize * mScale);
+        } finally {
+            mGCWrapper.setGC(null);
         }
+    }
 
-        /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
-        public void setSize(int imgSize, int clientSize) {
-            mImgSize = imgSize;
-            setClientSize(clientSize);
-        }
+    /**
+     * Clears the hover.
+     */
+    /* package */ void clearHover() {
+        mHoverOverlay.clearHover();
+    }
 
-        /** Changes the size of the client size. Recomputes scrollbars. */
-        public void setClientSize(int clientSize) {
-            mClientSize = clientSize;
-            resizeScrollbar();
+    /**
+     * Hover on top of a known child.
+     */
+    /* package */ void hover(MouseEvent e) {
+        // Check if a button is pressed; no hovers during drags
+        if ((e.stateMask & SWT.BUTTON_MASK) != 0) {
+            clearHover();
+            return;
         }
 
-        private void resizeScrollbar() {
-            // scaled image size
-            int sx = (int) (mImgSize * mScale);
-
-            // actual client area is always reduced by the margins
-            int cx = mClientSize - 2 * IMAGE_MARGIN;
+        LayoutPoint p = ControlPoint.create(this, e).toLayout();
+        CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
 
-            if (sx < cx) {
-                mScrollbar.setEnabled(false);
-            } else {
-                mScrollbar.setEnabled(true);
-
-                // max scroll value is the scaled image size
-                // thumb value is the actual viewable area out of the scaled img size
-                mScrollbar.setMaximum(sx);
-                mScrollbar.setThumb(cx);
-            }
+        // We don't hover on the root since it's not a widget per see and it is always there.
+        if (vi != null && vi.isRoot()) {
+            vi = null;
         }
 
-        public int translate(int canvasX) {
-            return IMAGE_MARGIN - mTranslate + (int)(mScale * canvasX);
-        }
+        boolean needsUpdate = vi != mHoverViewInfo;
+        mHoverViewInfo = vi;
 
-        public int scale(int canwasW) {
-            return (int)(mScale * canwasW);
+        if (vi == null) {
+            clearHover();
+        } else {
+            Rectangle r = vi.getSelectionRect();
+            mHoverOverlay.setHover(r.x, r.y, r.width, r.height);
         }
 
-        public int inverseTranslate(int screenX) {
-            return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
+        if (needsUpdate) {
+            redraw();
         }
     }
 
     /**
-     * Creates or updates the node proxy for this canvas view info.
-     * <p/>
-     * Since proxies are reused, this will update the bounds of an existing proxy when the
-     * canvas is refreshed and a view changes position or size.
-     * <p/>
-     * This is a recursive call that updates the whole hierarchy starting at the given
-     * view info.
+     * Show the XML element corresponding to the point under the mouse event
+     * (unless it's a root).
+     *
+     * @param e A mouse event pointing on the screen whose underlying XML
+     *            element we want to view
      */
-    private void updateNodeProxies(CanvasViewInfo vi) {
-        if (vi == null) {
+    public void showXml(MouseEvent e) {
+        // Warp to the text editor and show the corresponding XML for the
+        // double-clicked widget
+        LayoutPoint p = ControlPoint.create(this, e).toLayout();
+        CanvasViewInfo vi = mViewHierarchy.findViewInfoAt(p);
+        if (vi == null || vi.isRoot()) {
             return;
         }
 
-        UiViewElementNode key = vi.getUiViewKey();
-
-        if (key != null) {
-            mNodeFactory.create(vi);
-        }
-
-        for (CanvasViewInfo child : vi.getChildren()) {
-            updateNodeProxies(child);
+        Node xmlNode = vi.getXmlNode();
+        if (xmlNode != null) {
+            boolean found = mLayoutEditor.show(xmlNode);
+            if (!found) {
+                getDisplay().beep();
+            }
         }
     }
 
+    //---------------
+
     /**
-     * Sets the image of the last *successful* rendering.
-     * Converts the AWT image into an SWT image.
+     * Helper to create the drag source for the given control.
      * <p/>
-     * The image *can* be null, which is the case when we are dealing with an empty document.
+     * This is static with package-access so that {@link OutlinePage2} can also
+     * create an exact copy of the source with the same attributes.
      */
-    private void setImage(BufferedImage awtImage) {
-        if (mImage != null) {
-            mImage.dispose();
-        }
-        if (awtImage == null) {
-            mImage = null;
-
-        } else {
-            int width = awtImage.getWidth();
-            int height = awtImage.getHeight();
-
-            Raster raster = awtImage.getData(new java.awt.Rectangle(width, height));
-            int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
-
-            ImageData imageData = new ImageData(width, height, 32,
-                    new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
-
-            imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
-
-            mImage = new Image(getDisplay(), imageData);
-        }
+    /* package */static DragSource createDragSource(Control control) {
+        DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
+        source.setTransfer(new Transfer[] {
+                TextTransfer.getInstance(),
+                SimpleXmlTransfer.getInstance()
+        });
+        return source;
     }
 
     /**
-     * Sets the alpha for the given GC.
+     * Helper to create the drop target for the given control.
      * <p/>
-     * Alpha may not work on all platforms and may fail with an exception, which is
-     * hidden here (false is returned in that case).
-     *
-     * @param gc the GC to change
-     * @param alpha the new alpha, 0 for transparent, 255 for opaque.
-     * @return True if the operation worked, false if it failed with an exception.
-     *
-     * @see GC#setAlpha(int)
+     * This is static with package-access so that {@link OutlinePage2} can also
+     * create an exact copy of the drop target with the same attributes.
      */
-    private boolean gc_setAlpha(GC gc, int alpha) {
-        try {
-            gc.setAlpha(alpha);
-            return true;
-        } catch (SWTException e) {
-            return false;
-        }
+    /* package */static DropTarget createDropTarget(Control control) {
+        DropTarget dropTarget = new DropTarget(
+                control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
+        dropTarget.setTransfer(new Transfer[] {
+            SimpleXmlTransfer.getInstance()
+        });
+        return dropTarget;
     }
 
+    //---------------
+
     /**
-     * Sets the non-text antialias flag for the given GC.
+     * Invoked by the constructor to add our cut/copy/paste/delete/select-all
+     * handlers in the global action handlers of this editor's site.
      * <p/>
-     * Antialias may not work on all platforms and may fail with an exception, which is
-     * hidden here (-2 is returned in that case).
-     *
-     * @param gc the GC to change
-     * @param alias One of {@link SWT#DEFAULT}, {@link SWT#ON}, {@link SWT#OFF}.
-     * @return The previous aliasing mode if the operation worked,
-     *         or -2 if it failed with an exception.
-     *
-     * @see GC#setAntialias(int)
+     * This will enable the menu items under the global Edit menu and make them
+     * invoke our actions as needed. As a benefit, the corresponding shortcut
+     * accelerators will do what one would expect.
      */
-    private int gc_setAntialias(GC gc, int alias) {
-        try {
-            int old = gc.getAntialias();
-            gc.setAntialias(alias);
-            return old;
-        } catch (SWTException e) {
-            return -2;
-        }
-    }
-
-    /**
-     * Paints the canvas in response to paint events.
-     */
-    private void onPaint(PaintEvent e) {
-        GC gc = e.gc;
-        gc.setFont(mFont);
-        mGCWrapper.setGC(gc);
-        try {
-
-            if (mImage != null) {
-                if (!mIsResultValid) {
-                    gc_setAlpha(gc, 128);  // half-transparent
-                }
-
-                ScaleInfo hi = mHScale;
-                ScaleInfo vi = mVScale;
-
-                // we only anti-alias when reducing the image size.
-                int oldAlias = -2;
-                if (hi.getScale() < 1.0) {
-                    oldAlias = gc_setAntialias(gc, SWT.ON);
-                }
-
-                gc.drawImage(mImage,
-                        0,                          // srcX
-                        0,                          // srcY
-                        hi.getImgSize(),            // srcWidth
-                        vi.getImgSize(),            // srcHeight
-                        hi.translate(0),            // destX
-                        vi.translate(0),            // destY
-                        hi.getScalledImgSize(),     // destWidth
-                        vi.getScalledImgSize()      // destHeight
-                        );
-
-                if (oldAlias != -2) {
-                    gc_setAntialias(gc, oldAlias);
-                }
-
-                if (!mIsResultValid) {
-                    gc_setAlpha(gc, 255);  // opaque
-                }
-            }
-
-            if (mShowOutline && mLastValidViewInfoRoot != null) {
-                gc.setForeground(mOutlineColor);
-                gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
-                int oldAlpha = gc.getAlpha();
-                gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
-                drawOutline(gc, mLastValidViewInfoRoot);
-                gc.setAlpha(oldAlpha);
-            }
-
-            if (mHoverRect != null) {
-                int x = mHScale.translate(mHoverRect.x);
-                int y = mVScale.translate(mHoverRect.y);
-                int w = mHScale.scale(mHoverRect.width);
-                int h = mVScale.scale(mHoverRect.height);
-
-                if (mHoverStrokeColor != null) {
-                    int oldAlpha = gc.getAlpha();
-                    gc.setForeground(mHoverStrokeColor);
-                    gc.setLineStyle(SwtDrawingStyle.HOVER.getLineStyle());
-                    gc.setAlpha(SwtDrawingStyle.HOVER.getStrokeAlpha());
-                    gc.drawRectangle(x, y, w, h);
-                    gc.setAlpha(oldAlpha);
-                }
-
-                if (mHoverFillColor != null) {
-                    int oldAlpha = gc.getAlpha();
-                    gc.setAlpha(SwtDrawingStyle.HOVER.getFillAlpha());
-                    gc.setBackground(mHoverFillColor);
-                    gc.fillRectangle(x, y, w, h);
-                    gc.setAlpha(oldAlpha);
-                }
-            }
-
-            int n = mSelections.size();
-            if (n > 0) {
-                boolean isMultipleSelection = n > 1;
-
-                if (n == 1) {
-                    gc.setForeground(mSelectionFgColor);
-                    mSelections.get(0).paintParentSelection(mRulesEngine, mGCWrapper);
-                }
-
-                for (CanvasSelection s : mSelections) {
-                    gc.setForeground(mSelectionFgColor);
-                    s.paintSelection(mRulesEngine, mGCWrapper, isMultipleSelection);
-                }
-            }
-
-            if (mDropListener != null) {
-                mDropListener.paintFeedback(mGCWrapper);
-            }
-
-        } finally {
-            mGCWrapper.setGC(null);
-        }
-    }
-
-    private void drawOutline(GC gc, CanvasViewInfo info) {
-
-        Rectangle r = info.getAbsRect();
-
-        int x = mHScale.translate(r.x);
-        int y = mVScale.translate(r.y);
-        int w = mHScale.scale(r.width);
-        int h = mVScale.scale(r.height);
-
-        // Add +1 to the width and +1 to the height such that when you have a
-        // series of boxes (in say a LinearLayout), instead of the bottom of one
-        // box and the top of the next box being -adjacent-, they -overlap-.
-        // This makes the outline nicer visually since you don't get
-        // "double thickness" lines for all adjacent boxes.
-        gc.drawRectangle(x, y, w + 1, h + 1);
-
-        for (CanvasViewInfo vi : info.getChildren()) {
-            drawOutline(gc, vi);
-        }
-    }
-
-    /**
-     * Hover on top of a known child.
-     */
-    private void onMouseMove(MouseEvent e) {
-        CanvasViewInfo root = mLastValidViewInfoRoot;
-
-        int x = mHScale.inverseTranslate(e.x);
-        int y = mVScale.inverseTranslate(e.y);
-
-        CanvasViewInfo vi = findViewInfoAt(x, y);
-
-        // We don't hover on the root since it's not a widget per see and it is always there.
-        if (vi == root) {
-            vi = null;
-        }
-
-        boolean needsUpdate = vi != mHoverViewInfo;
-        mHoverViewInfo = vi;
-
-        if (vi == null) {
-            mHoverRect = null;
-        } else {
-            Rectangle r = vi.getSelectionRect();
-            mHoverRect = new Rectangle(r.x, r.y, r.width, r.height);
-        }
-
-        if (needsUpdate) {
-            redraw();
-        }
-    }
-
-    private void onMouseDown(MouseEvent e) {
-        // Pass, not used yet. We do everything on mouse up.
-    }
-
-    /**
-     * 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) {
-
-        boolean isShift = (e.stateMask & SWT.SHIFT) != 0;
-        boolean isAlt   = (e.stateMask & SWT.ALT)   != 0;
-
-        int x = mHScale.inverseTranslate(e.x);
-        int y = mVScale.inverseTranslate(e.y);
-
-        if (e.button == 3) {
-            // Right click button is used to display a context menu.
-            // If there's an existing selection and the click is anywhere in this selection
-            // and there are no modifiers being used, we don't want to change the selection.
-            // Otherwise we select the item under the cursor.
-
-            if (!isAlt && !isShift) {
-                for (CanvasSelection cs : mSelections) {
-                    if (cs.getRect().contains(x, y)) {
-                        // The cursor is inside the selection. Don't change anything.
-                        return;
-                    }
-                }
-            }
-
-        } else if (e.button != 1) {
-            // Click was done with something else than the left button for normal selection
-            // or the right button for context menu.
-            // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
-            // anything, so let's not change the selection.
-            return;
-        }
-
-        CanvasViewInfo vi = findViewInfoAt(x, y);
-
-        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 CanvasSelection(vi, mRulesEngine, mNodeFactory));
-                fireSelectionChanged();
-                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 CanvasAlternateSelection(
-                        vi, findAltViewInfoAt(x, y, mLastValidViewInfoRoot));
-
-                // deselect them all, in case they were partially selected
-                deselectAll(mAltSelection.getAltViews());
-
-                // select the current one
-                CanvasViewInfo vi2 = mAltSelection.getCurrent();
-                if (vi2 != null) {
-                    mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
-                    fireSelectionChanged();
-                }
-            } else {
-                // We're trying to cycle through the current alternate selection.
-                // First remove the current object.
-                CanvasViewInfo vi2 = mAltSelection.getCurrent();
-                deselect(vi2);
-
-                // Now select the next one.
-                vi2 = mAltSelection.getNext();
-                if (vi2 != null) {
-                    mSelections.addFirst(new CanvasSelection(vi2, mRulesEngine, mNodeFactory));
-                    fireSelectionChanged();
-                }
-            }
-            redraw();
-
-        } else {
-            // Case where no modifier is pressed: either select or reset the selection.
-
-            selectSingle(vi);
-        }
-    }
-
-    /**
-     * Removes all the currently selected item and only select the given item.
-     * Issues a {@link #redraw()} if the selection changes.
-     *
-     * @param vi The new selected item if non-null. Selection becomes empty if null.
-     */
-    private void selectSingle(CanvasViewInfo vi) {
-        // reset alternate selection if any
-        mAltSelection = null;
-
-        // reset (multi)selection if any
-        if (!mSelections.isEmpty()) {
-            if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
-                // CanvasSelection remains the same, don't touch it.
-                return;
-            }
-            mSelections.clear();
-        }
-
-        if (vi != null) {
-            mSelections.add(new CanvasSelection(vi, mRulesEngine, mNodeFactory));
-        }
-        fireSelectionChanged();
-        redraw();
-    }
-
-    /**
-     * Selects the given set of {@link CanvasViewInfo}s. This is similar to
-     * {@link #selectSingle} but allows you to make a multi-selection. Issues a
-     * {@link #redraw()}.
-     *
-     * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
-     *            selected, or null or empty to clear the selection.
-     */
-    /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
-        // reset alternate selection if any
-        mAltSelection = null;
-
-        mSelections.clear();
-        if (viewInfos != null) {
-            for (CanvasViewInfo viewInfo : viewInfos) {
-                mSelections.add(new CanvasSelection(viewInfo, mRulesEngine, mNodeFactory));
-            }
-        }
-
-        fireSelectionChanged();
-        redraw();
-    }
-
-    /**
-     * Select the visual element corresponding to the given XML node
-     * @param xmlNode The Node whose element we want to select
-     */
-    public void select(Node xmlNode) {
-        CanvasViewInfo vi = findViewInfoFor(xmlNode);
-        if (vi != null) {
-            // Select the visual element -- unless it's the root.
-            // The root element is the one whose GRAND parent
-            // is null (because the parent will be a -document-
-            // node).
-            UiViewElementNode key = vi.getUiViewKey();
-            if (key != null && key.getUiParent() != null &&
-                    key.getUiParent().getUiParent() != null) {
-                selectSingle(vi);
-            }
-        }
-    }
-
-    /**
-     * Deselects a view info.
-     * Returns true if the object was actually selected.
-     * Callers are responsible for calling redraw() and updateOulineSelection() after.
-     */
-    private boolean deselect(CanvasViewInfo canvasViewInfo) {
-        if (canvasViewInfo == null) {
-            return false;
-        }
-
-        for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
-            CanvasSelection s = it.next();
-            if (canvasViewInfo == s.getViewInfo()) {
-                it.remove();
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Deselects multiple view infos.
-     * Callers are responsible for calling redraw() and updateOulineSelection() after.
-     */
-    private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
-        for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
-            CanvasSelection s = it.next();
-            if (canvasViewInfos.contains(s.getViewInfo())) {
-                it.remove();
-            }
-        }
-    }
-
-    private void onDoubleClick(MouseEvent e) {
-        // Warp to the text editor and show the corresponding XML for the
-        // double-clicked widget
-        int x = mHScale.inverseTranslate(e.x);
-        int y = mVScale.inverseTranslate(e.y);
-        CanvasViewInfo vi = findViewInfoAt(x, y);
-        if (vi == null) {
-            return;
-        }
-
-        Node xmlNode = vi.getXmlNode();
-        if (xmlNode != null) {
-            boolean found = mLayoutEditor.show(xmlNode);
-            if (!found) {
-                getDisplay().beep();
-            }
-        }
-    }
-
-    /**
-     * Tries to find a child with the same view key in the view info sub-tree.
-     * Returns null if not found.
-     */
-    private CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
-        if (canvasViewInfo == null) {
-            return null;
-        }
-        if (canvasViewInfo.getUiViewKey() == viewKey) {
-            return canvasViewInfo;
-        }
-
-        // try to find a matching child
-        for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
-            CanvasViewInfo v = findViewInfoKey(viewKey, child);
-            if (v != null) {
-                return v;
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
-     * node, or null if it cannot be found.
-     *
-     * @param node The node we want to find a corresponding
-     *            {@link CanvasViewInfo} for.
-     * @return The {@link CanvasViewInfo} corresponding to the given node, or
-     *         null if no match was found.
-     */
-    /* package */ CanvasViewInfo findViewInfoFor(Node node) {
-        if (mLastValidViewInfoRoot != null) {
-            return findViewInfoForNode(node, mLastValidViewInfoRoot);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Tries to find a child with the same view XML node in the view info sub-tree.
-     * Returns null if not found.
-     */
-    private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) {
-        if (canvasViewInfo == null) {
-            return null;
-        }
-        if (canvasViewInfo.getXmlNode() == xmlNode) {
-            return canvasViewInfo;
-        }
-
-        // Try to find a matching child
-        for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
-            CanvasViewInfo v = findViewInfoForNode(xmlNode, child);
-            if (v != null) {
-                return v;
-            }
-        }
-
-        return null;
-    }
-
-
-    /**
-     * Tries to find the inner most child matching the given x,y coordinates
-     * in the view info sub-tree, starting at the last know view info root.
-     * This uses the potentially-expanded selection bounds.
-     * <p/>
-     * Returns null if not found or if there's no view info root.
-     */
-    /* package */ CanvasViewInfo findViewInfoAt(int x, int y) {
-        if (mLastValidViewInfoRoot == null) {
-            return null;
-        } else {
-            return findViewInfoAt_Recursive(x, y, mLastValidViewInfoRoot);
-        }
-    }
-
-    /**
-     * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
-     * <p/>
-     * Tries to find the inner most child matching the given x,y coordinates in the view
-     * info sub-tree. This uses the potentially-expanded selection bounds.
-     *
-     * Returns null if not found.
-     */
-    private CanvasViewInfo findViewInfoAt_Recursive(int x, int y, CanvasViewInfo canvasViewInfo) {
-        if (canvasViewInfo == null) {
-            return null;
-        }
-        Rectangle r = canvasViewInfo.getSelectionRect();
-        if (r.contains(x, y)) {
-
-            // try to find a matching child first
-            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
-                CanvasViewInfo v = findViewInfoAt_Recursive(x, y, child);
-                if (v != null) {
-                    return v;
-                }
-            }
-
-            // if no children matched, this is the view that we're looking for
-            return canvasViewInfo;
-        }
-
-        return null;
-    }
-
-    /**
-     * Returns a list of all the possible alternatives for a given view at the given
-     * position. This is used to build and manage the "alternate" selection that cycles
-     * around the parents or children of the currently selected element.
-     */
-    private List<CanvasViewInfo> findAltViewInfoAt(int x, int y, CanvasViewInfo parent) {
-        return findAltViewInfoAt_Recursive(x, y, parent, null);
-    }
-
-    /**
-     * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
-     * Please don't use directly.
-     */
-    private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
-            int x, int y, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
-        Rectangle r;
-
-        if (outList == null) {
-            outList = new ArrayList<CanvasViewInfo>();
-
-            if (parent != null) {
-                // add the parent root only once
-                r = parent.getSelectionRect();
-                if (r.contains(x, y)) {
-                    outList.add(parent);
-                }
-            }
-        }
-
-        if (parent != null && !parent.getChildren().isEmpty()) {
-            // then add all children that match the position
-            for (CanvasViewInfo child : parent.getChildren()) {
-                r = child.getSelectionRect();
-                if (r.contains(x, y)) {
-                    outList.add(child);
-                }
-            }
-
-            // finally recurse in the children
-            for (CanvasViewInfo child : parent.getChildren()) {
-                r = child.getSelectionRect();
-                if (r.contains(x, y)) {
-                    findAltViewInfoAt_Recursive(x, y, child, outList);
-                }
-            }
-        }
-
-        return outList;
-    }
-
-    /**
-     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
-     * node, or null if it cannot be found.
-     *
-     * @param node The node we want to find a corresponding
-     *            {@link CanvasViewInfo} for.
-     * @return The {@link CanvasViewInfo} corresponding to the given node, or
-     *         null if no match was found.
-     */
-    /* package */ CanvasViewInfo findViewInfoFor(INode node) {
-        if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) {
-            return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Used by {@link #onSelectAll()} to add all current view infos to the selection list.
-     *
-     * @param canvasViewInfo The root to add. This info and all its children will be added to the
-     *                 selection list.
-     */
-    private void selectAllViewInfos(CanvasViewInfo canvasViewInfo) {
-        if (canvasViewInfo != null) {
-            mSelections.add(new CanvasSelection(canvasViewInfo, mRulesEngine, mNodeFactory));
-            for (CanvasViewInfo vi : canvasViewInfo.getChildren()) {
-                selectAllViewInfos(vi);
-            }
-        }
-    }
-
-    /**
-     * Notifies listeners that the selection has changed.
-     */
-    private void fireSelectionChanged() {
-        if (mInsideUpdateSelection) {
-            return;
-        }
-        try {
-            mInsideUpdateSelection = true;
-
-            final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
-
-            SafeRunnable.run(new SafeRunnable() {
-                public void run() {
-                    for (Object listener : mSelectionListeners.getListeners()) {
-                        ((ISelectionChangedListener)listener).selectionChanged(event);
-                    }
-                }
-            });
-
-            // Update menu actions that depend on the selection
-            updateMenuActions();
-
-        } finally {
-            mInsideUpdateSelection = false;
-        }
-    }
-
-
-    //---------------
-
-    /**
-     * Helper to create the drag source for the given control.
-     * <p/>
-     * This is static with package-access so that {@link OutlinePage2} can also
-     * create an exact copy of the source with the same attributes.
-     */
-    /* package */ static DragSource createDragSource(
-            Control control,
-            DragSourceListener dragSourceListener) {
-        DragSource source = new DragSource(control, DND.DROP_COPY | DND.DROP_MOVE);
-        source.setTransfer(new Transfer[] {
-                TextTransfer.getInstance(),
-                SimpleXmlTransfer.getInstance()
-            } );
-        source.addDragListener(dragSourceListener);
-        return source;
-    }
-
-    /**
-     * Helper to create the drop target for the given control.
-     * <p/>
-     * This is static with package-access so that {@link OutlinePage2} can also
-     * create an exact copy of the drop target with the same attributes.
-     */
-    /* package */ static DropTarget createDropTarget(
-            Control control,
-            DropTargetListener dropListener) {
-        DropTarget dropTarget = new DropTarget(
-                control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT);
-        dropTarget.setTransfer(new Transfer[] { SimpleXmlTransfer.getInstance() } );
-        dropTarget.addDropListener(dropListener);
-        return dropTarget;
-    }
-
-    /**
-     * Our canvas {@link DragSourceListener}. Handles drag being started and finished
-     * and generating the drag data.
-     */
-    private class CanvasDragSourceListener implements DragSourceListener {
-
-        /**
-         * The current selection being dragged.
-         * This may be a subset of the canvas selection due to the "sanitize" pass.
-         * Can be empty but never null.
-         */
-        private final ArrayList<CanvasSelection> mDragSelection = new ArrayList<CanvasSelection>();
-        private SimpleElement[] mDragElements;
-
-        /**
-         * The user has begun the actions required to drag the widget.
-         * <p/>
-         * Initiate a drag only if there is one or more item selected.
-         * If there's none, try to auto-select the one under the cursor.
-         *
-         * {@inheritDoc}
-         */
-        public void dragStart(DragSourceEvent e) {
-            // We need a selection (simple or multiple) to do any transfer.
-            // If there's a selection *and* the cursor is over this selection, use all the
-            // currently selected elements.
-            // If there is no selection or the cursor is not over a selected element, *change*
-            // the selection to match the element under the cursor and use that.
-            // If nothing can be selected, abort the drag operation.
-
-            mDragSelection.clear();
-
-            if (!mSelections.isEmpty()) {
-                // Is the cursor on top of a selected element?
-                int x = mHScale.inverseTranslate(e.x);
-                int y = mVScale.inverseTranslate(e.y);
-
-                boolean insideSelection = false;
-
-                for (CanvasSelection cs : mSelections) {
-                    if (!cs.isRoot() && cs.getRect().contains(x, y)) {
-                        insideSelection = true;
-                        break;
-                    }
-                }
-
-                if (!insideSelection) {
-                    CanvasViewInfo vi = findViewInfoAt(x, y);
-                    if (vi != null) {
-                        selectSingle(vi);
-                        insideSelection = true;
-                    }
-                }
-
-                if (insideSelection) {
-                    // We should now have a proper selection that matches the cursor.
-                    // Let's use this one. We make a copy of it since the "sanitize" pass
-                    // below might remove some of the selected objects.
-                    if (mSelections.size() == 1) {
-                        // You are dragging just one element - this might or might not be
-                        // the root, but if it's the root that is fine since we will let you
-                        // drag the root if it is the only thing you are dragging.
-                        mDragSelection.addAll(mSelections);
-                    } else {
-                        // Only drag non-root items.
-                        for (CanvasSelection cs : mSelections) {
-                            if (!cs.isRoot()) {
-                                mDragSelection.add(cs);
-                            }
-                        }
-                    }
-                }
-            }
-
-            // If you are dragging a non-selected item, select it
-            if (mDragSelection.isEmpty()) {
-                int x = mHScale.inverseTranslate(e.x);
-                int y = mVScale.inverseTranslate(e.y);
-                CanvasViewInfo vi = findViewInfoAt(x, y);
-                if (vi != null) {
-                    selectSingle(vi);
-                    mDragSelection.addAll(mSelections);
-                }
-            }
-
-            sanitizeSelection(mDragSelection);
-
-            e.doit = !mDragSelection.isEmpty();
-            if (e.doit) {
-                mDragElements = getSelectionAsElements(mDragSelection);
-                GlobalCanvasDragInfo.getInstance().startDrag(
-                        mDragElements,
-                        mDragSelection.toArray(new CanvasSelection[mDragSelection.size()]),
-                        LayoutCanvas.this,
-                        new Runnable() {
-                            public void run() {
-                                deleteSelection("Remove", mDragSelection);
-                            }
-                        }
-                );
-            }
-        }
-
-        /**
-         * Callback invoked when data is needed for the event, typically right before drop.
-         * The drop side decides what type of transfer to use and this side must now provide
-         * the adequate data.
-         *
-         * {@inheritDoc}
-         */
-        public void dragSetData(DragSourceEvent e) {
-            if (TextTransfer.getInstance().isSupportedType(e.dataType)) {
-                e.data = getSelectionAsText(mDragSelection);
-                return;
-            }
-
-            if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) {
-                e.data = mDragElements;
-                return;
-            }
-
-            // otherwise we failed
-            e.detail = DND.DROP_NONE;
-            e.doit = false;
-        }
-
-        /**
-         * Callback invoked when the drop has been finished either way.
-         * On a successful move, remove the originating elements.
-         */
-        public void dragFinished(DragSourceEvent e) {
-            // Clear the selection
-            mDragSelection.clear();
-            mDragElements = null;
-            GlobalCanvasDragInfo.getInstance().stopDrag();
-        }
-    }
-
-    /**
-     * Sanitizes the selection for a copy/cut or drag operation.
-     * <p/>
-     * Sanitizes the list to make sure all elements have a valid XML attached to it,
-     * that is remove element that have no XML to avoid having to make repeated such
-     * checks in various places after.
-     * <p/>
-     * In case of multiple selection, we also need to remove all children when their
-     * parent is already selected since parents will always be added with all their
-     * children.
-     * <p/>
-     *
-     * @param selection The selection list to be sanitized <b>in-place</b>.
-     *      The <code>selection</code> argument should not be {@link #mSelections} -- the
-     *      given list is going to be altered and we should never alter the user-made selection.
-     *      Instead the caller should provide its own copy.
-     */
-    private void sanitizeSelection(List<CanvasSelection> selection) {
-        if (selection.isEmpty()) {
-            return;
-        }
-
-        for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) {
-            CanvasSelection cs = it.next();
-            CanvasViewInfo vi = cs.getViewInfo();
-            UiViewElementNode key = vi == null ? null : vi.getUiViewKey();
-            Node node = key == null ? null : key.getXmlNode();
-            if (node == null) {
-                // Missing ViewInfo or view key or XML, discard this.
-                it.remove();
-                continue;
-            }
-
-            if (vi != null) {
-                for (Iterator<CanvasSelection> it2 = selection.iterator();
-                     it2.hasNext(); ) {
-                    CanvasSelection cs2 = it2.next();
-                    if (cs != cs2) {
-                        CanvasViewInfo vi2 = cs2.getViewInfo();
-                        if (vi.isParent(vi2)) {
-                            // vi2 is a parent for vi. Remove vi.
-                            it.remove();
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Get the XML text from the given selection for a text transfer.
-     * The returned string can be empty but not null.
-     */
-    private String getSelectionAsText(List<CanvasSelection> selection) {
-        StringBuilder sb = new StringBuilder();
-
-        for (CanvasSelection cs : selection) {
-            CanvasViewInfo vi = cs.getViewInfo();
-            UiViewElementNode key = vi.getUiViewKey();
-            Node node = key.getXmlNode();
-            String t = getXmlTextFromEditor(mLayoutEditor, node);
-            if (t != null) {
-                if (sb.length() > 0) {
-                    sb.append('\n');
-                }
-                sb.append(t);
-            }
-        }
-
-        return sb.toString();
-    }
-
-    /**
-     * Get the XML text directly from the editor.
-     */
-    private String getXmlTextFromEditor(AndroidXmlEditor editor, Node xml_node) {
-        String data = null;
-        IStructuredModel model = editor.getModelForRead();
-        try {
-            IStructuredDocument sse_doc = editor.getStructuredDocument();
-            if (xml_node instanceof NodeContainer) {
-                // The easy way to get the source of an SSE XML node.
-                data = ((NodeContainer) xml_node).getSource();
-            } else  if (xml_node instanceof IndexedRegion && sse_doc != null) {
-                // Try harder.
-                IndexedRegion region = (IndexedRegion) xml_node;
-                int start = region.getStartOffset();
-                int end = region.getEndOffset();
-
-                if (end > start) {
-                    data = sse_doc.get(start, end - start);
-                }
-            }
-        } catch (BadLocationException e) {
-            // the region offset was invalid. ignore.
-        } finally {
-            model.releaseFromRead();
-        }
-        return data;
-    }
-
-    private SimpleElement[] getSelectionAsElements(List<CanvasSelection> mDragSelection) {
-        ArrayList<SimpleElement> elements = new ArrayList<SimpleElement>();
-
-        for (CanvasSelection cs : mDragSelection) {
-            CanvasViewInfo vi = cs.getViewInfo();
-
-            SimpleElement e = transformToSimpleElement(vi);
-            elements.add(e);
-        }
-
-        return elements.toArray(new SimpleElement[elements.size()]);
-    }
-
-    private SimpleElement transformToSimpleElement(CanvasViewInfo vi) {
-
-        UiViewElementNode uiNode = vi.getUiViewKey();
-
-        String fqcn = SimpleXmlTransfer.getFqcn(uiNode.getDescriptor());
-        String parentFqcn = null;
-        Rect bounds = new Rect(vi.getAbsRect());
-        Rect parentBounds = null;
-
-        UiElementNode uiParent = uiNode.getUiParent();
-        if (uiParent != null) {
-            parentFqcn = SimpleXmlTransfer.getFqcn(uiParent.getDescriptor());
-        }
-        if (vi.getParent() != null) {
-            parentBounds = new Rect(vi.getParent().getAbsRect());
-        }
-
-        SimpleElement e = new SimpleElement(fqcn, parentFqcn, bounds, parentBounds);
-
-        for (UiAttributeNode attr : uiNode.getUiAttributes()) {
-            String value = attr.getCurrentValue();
-            if (value != null && value.length() > 0) {
-                AttributeDescriptor attrDesc = attr.getDescriptor();
-                SimpleAttribute a = new SimpleAttribute(
-                        attrDesc.getNamespaceUri(),
-                        attrDesc.getXmlLocalName(),
-                        value);
-                e.addAttribute(a);
-            }
-        }
-
-        for (CanvasViewInfo childVi : vi.getChildren()) {
-            SimpleElement e2 = transformToSimpleElement(childVi);
-            if (e2 != null) {
-                e.addInnerElement(e2);
-            }
-        }
-
-        return e;
-    }
-
-    //---------------
-
-    /**
-     * Invoked by the constructor to add our cut/copy/paste/delete/select-all
-     * handlers in the global action handlers of this editor's site.
-     * <p/>
-     * This will enable the menu items under the global Edit menu and make them
-     * invoke our actions as needed. As a benefit, the corresponding shortcut
-     * accelerators will do what one would expect.
-     */
-    private void setupGlobalActionHandlers() {
-        // Get the global action bar for this editor (i.e. the menu bar)
-        IActionBars actionBars = mLayoutEditor.getEditorSite().getActionBars();
+    private void setupGlobalActionHandlers() {
+        // Get the global action bar for this editor (i.e. the menu bar)
+        IActionBars actionBars = mLayoutEditor.getEditorSite().getActionBars();
 
         TextActionHandler tah = new TextActionHandler(actionBars);
 
         mCutAction = new Action() {
             @Override
             public void run() {
-                cutSelectionToClipboard(new ArrayList<CanvasSelection>(mSelections));
+                mClipboardSupport.cutSelectionToClipboard(mSelectionManager.getSnapshot());
             }
         };
 
@@ -1944,7 +667,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         mCopyAction = new Action() {
             @Override
             public void run() {
-                copySelectionToClipboard(new ArrayList<CanvasSelection>(mSelections));
+                mClipboardSupport.copySelectionToClipboard(mSelectionManager.getSnapshot());
             }
         };
 
@@ -1954,7 +677,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         mPasteAction = new Action() {
             @Override
             public void run() {
-                pasteSelection(new ArrayList<CanvasSelection>(mSelections));
+                mClipboardSupport.pasteSelection(mSelectionManager.getSnapshot());
             }
         };
 
@@ -1964,9 +687,9 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         mDeleteAction = new Action() {
             @Override
             public void run() {
-                deleteSelection(
-                        mDeleteAction.getText(), // verb "Delete" from the DELETE action's title
-                        new ArrayList<CanvasSelection>(mSelections));
+                mClipboardSupport.deleteSelection(
+                        getDeleteLabel(),
+                        mSelectionManager.getSnapshot());
             }
         };
 
@@ -1976,7 +699,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         mSelectAllAction = new Action() {
             @Override
             public void run() {
-                onSelectAll();
+                mSelectionManager.selectAll();
             }
         };
 
@@ -1984,11 +707,21 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL);
     }
 
-    /** Update menu actions that depends on the selection. */
-    private void updateMenuActions() {
+    /* package */ String getCutLabel() {
+        return mCutAction.getText();
+    }
 
-        boolean hasSelection = !mSelections.isEmpty();
+    /* package */ String getDeleteLabel() {
+        // verb "Delete" from the DELETE action's title
+        return mDeleteAction.getText();
+    }
 
+    /**
+     * Updates menu actions that depends on the selection.
+     *
+     * @param hasSelection True iff we have a non-empty selection
+     */
+    /* package */ void updateMenuActions(boolean hasSelection) {
         mCutAction.setEnabled(hasSelection);
         mCopyAction.setEnabled(hasSelection);
         mDeleteAction.setEnabled(hasSelection);
@@ -1996,14 +729,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
 
         // The paste operation is only available if we can paste our custom type.
         // We do not currently support pasting random text (e.g. XML). Maybe later.
-        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
-        boolean hasSxt = false;
-        for (TransferData td : mClipboard.getAvailableTypes()) {
-            if (sxt.isSupportedType(td)) {
-                hasSxt = true;
-                break;
-            }
-        }
+        boolean hasSxt = mClipboardSupport.hasSxtOnClipboard();
         mPasteAction.setEnabled(hasSxt);
     }
 
@@ -2085,7 +811,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         // Create a "Show In" sub-menu and automatically populate it using standard
         // actions contributed by the workbench.
         String showInLabel = IDEWorkbenchMessages.Workbench_showIn;
-        MenuManager showInSubMenu= new MenuManager(showInLabel);
+        MenuManager showInSubMenu = new MenuManager(showInLabel);
         showInSubMenu.add(
                 ContributionItemFactory.VIEWS_SHOW_IN.create(
                         mLayoutEditor.getSite().getWorkbenchWindow()));
@@ -2093,290 +819,26 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     }
 
     /**
-     * Invoked by {@link #mSelectAllAction}. It clears the selection and then
-     * selects everything (all views and all their children).
-     */
-    private void onSelectAll() {
-        // First clear the current selection, if any.
-        mSelections.clear();
-        mAltSelection = null;
-
-        // Now select everything if there's a valid layout
-        if (mIsResultValid && mLastValidViewInfoRoot != null) {
-            selectAllViewInfos(mLastValidViewInfoRoot);
-            redraw();
-        }
-
-        fireSelectionChanged();
-    }
-
-    /**
-     * Perform the "Copy" action, either from the Edit menu or from the context menu.
-     * Invoked by {@link #mCopyAction}.
-     * <p/>
-     * This sanitizes the selection, so it must be a copy. It then inserts the selection
-     * both as text and as {@link SimpleElement}s in the clipboard.
-     */
-    private void copySelectionToClipboard(List<CanvasSelection> selection) {
-        sanitizeSelection(selection);
-
-        if (selection.isEmpty()) {
-            return;
-        }
-
-        Object[] data = new Object[] {
-                getSelectionAsElements(selection),
-                getSelectionAsText(selection)
-        };
-
-        Transfer[] types = new Transfer[] {
-                SimpleXmlTransfer.getInstance(),
-                TextTransfer.getInstance()
-        };
-
-        mClipboard.setContents(data, types);
-    }
-
-    /**
-     * Perform the "Cut" action, either from the Edit menu or from the context menu.
-     * Invoked by {@link #mCutAction}.
-     * <p/>
-     * This sanitizes the selection, so it must be a copy.
-     * It uses the {@link #copySelectionToClipboard(List)} method to copy the selection
-     * to the clipboard.
-     * Finally it uses {@link #deleteSelection(String, List)} to delete the selection
-     * with a "Cut" verb for the title.
+     * Deletes the selection. Equivalent to pressing the Delete key.
      */
-    private void cutSelectionToClipboard(List<CanvasSelection> selection) {
-        copySelectionToClipboard(selection);
-        deleteSelection(
-                mCutAction.getText(), // verb "Cut" from the CUT action's title
-                selection);
-    }
-
-    /**
-     * Deletes the given selection.
-     * <p/>
-     * This can either be invoked directly by {@link #mDeleteAction}, or as
-     * an implementation detail as part of {@link #mCutAction} or also when removing
-     * the elements after a successful "MOVE" drag'n'drop.
-     *
-     * @param verb A translated verb for the action. Will be used for the undo/redo title.
-     *   Typically this should be {@link Action#getText()} for either
-     *   {@link #mCutAction} or {@link #mDeleteAction}.
-     * @param selection The selection. Must not be null. Can be empty, in which case nothing
-     *   happens. The selection list will be sanitized so the caller should give a copy of
-     *   {@link #mSelections}, directly or indirectly.
-     */
-    private void deleteSelection(String verb, final List<CanvasSelection> selection) {
-        sanitizeSelection(selection);
-
-        if (selection.isEmpty()) {
-            return;
-        }
-
-        // If all selected items have the same *kind* of parent, display that in the undo title.
-        String title = null;
-        for (CanvasSelection cs : selection) {
-            CanvasViewInfo vi = cs.getViewInfo();
-            if (vi != null && vi.getParent() != null) {
-                if (title == null) {
-                    title = vi.getParent().getName();
-                } else if (!title.equals(vi.getParent().getName())) {
-                    // More than one kind of parent selected.
-                    title = null;
-                    break;
-                }
-            }
-        }
-
-        if (title != null) {
-            // Typically the name is an FQCN. Just get the last segment.
-            int pos = title.lastIndexOf('.');
-            if (pos > 0 && pos < title.length() - 1) {
-                title = title.substring(pos + 1);
-            }
-        }
-        boolean multiple = mSelections.size() > 1;
-        if (title == null) {
-            title = String.format(
-                        multiple ? "%1$s elements" : "%1$s element",
-                        verb);
-        } else {
-            title = String.format(
-                        multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
-                        verb, title);
-        }
-
-        // Implementation note: we don't clear the internal selection after removing
-        // the elements. An update XML model event should happen when the model gets released
-        // which will trigger a recompute of the layout, thus reloading the model thus
-        // resetting the selection.
-        mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
-            public void run() {
-                for (CanvasSelection cs : selection) {
-                    CanvasViewInfo vi = cs.getViewInfo();
-                    if (vi != null) {
-                        UiViewElementNode ui = vi.getUiViewKey();
-                        if (ui != null) {
-                            ui.deleteXmlNode();
-                        }
-                    }
-                }
-            }
-        });
-    }
-
-    /**
-     * Perform the "Paste" action, either from the Edit menu or from the context menu.
-     */
-    private void pasteSelection(List<CanvasSelection> selection) {
-
-        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
-        SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
-
-        if (pasted == null || pasted.length == 0) {
-            return;
-        }
-
-        if (mLastValidViewInfoRoot == null) {
-            // Pasting in an empty document. Only paste the first element.
-            pasteInEmptyDocument(pasted[0]);
-            return;
-        }
-
-        // Otherwise use the current selection, if any, as a guide where to paste
-        // using the first selected element only. If there's no selection use
-        // the root as the insertion point.
-        sanitizeSelection(selection);
-        CanvasViewInfo target = mLastValidViewInfoRoot;
-        if (selection.size() > 0) {
-            CanvasSelection cs = selection.get(0);
-            target = cs.getViewInfo();
-        }
-
-        NodeProxy targetNode = mNodeFactory.create(target);
-
-        getRulesEngine().callOnPaste(targetNode, pasted);
-    }
-
-    /**
-     * Paste a new root into an empty XML layout.
-     * <p/>
-     * In case of error (unknown FQCN, document not empty), silently do nothing.
-     * In case of success, the new element will have some default attributes set (xmlns:android,
-     * layout_width and height). The edit is wrapped in a proper undo.
-     * <p/>
-     * Implementation is similar to {@link #createDocumentRoot(String)} except we also
-     * copy all the attributes and inner elements recursively.
-     */
-    private void pasteInEmptyDocument(final IDragElement pastedElement) {
-        String rootFqcn = pastedElement.getFqcn();
-
-        // Need a valid empty document to create the new root
-        final UiDocumentNode uiDoc = mLayoutEditor.getUiRootNode();
-        if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
-            debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
-            return;
-        }
-
-        // Find the view descriptor matching our FQCN
-        final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescritor(rootFqcn);
-        if (viewDesc == null) {
-            // TODO this could happen if pasting a custom view not known in this project
-            debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
-            return;
-        }
-
-        // Get the last segment of the FQCN for the undo title
-        String title = rootFqcn;
-        int pos = title.lastIndexOf('.');
-        if (pos > 0 && pos < title.length() - 1) {
-            title = title.substring(pos + 1);
-        }
-        title = String.format("Paste root %1$s in document", title);
-
-        mLayoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
-            public void run() {
-                UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
-
-                // A root node requires the Android XMLNS
-                uiNew.setAttributeValue(
-                        "android",
-                        XmlnsAttributeDescriptor.XMLNS_URI,
-                        SdkConstants.NS_RESOURCES,
-                        true /*override*/);
-
-                // Copy all the attributes from the pasted element
-                for (IDragAttribute attr : pastedElement.getAttributes()) {
-                    uiNew.setAttributeValue(
-                            attr.getName(),
-                            attr.getUri(),
-                            attr.getValue(),
-                            true /*override*/);
-                }
-
-                // Adjust the attributes, adding the default layout_width/height
-                // only if they are not present (the original element should have
-                // them though.)
-                DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
-
-                uiNew.createXmlNode();
-
-                // Now process all children
-                for (IDragElement childElement : pastedElement.getInnerElements()) {
-                    addChild(uiNew, childElement);
-                }
-            }
-
-            private void addChild(UiElementNode uiParent, IDragElement childElement) {
-                String childFqcn = childElement.getFqcn();
-                final ViewElementDescriptor childDesc =
-                    mLayoutEditor.getFqcnViewDescritor(childFqcn);
-                if (childDesc == null) {
-                    // TODO this could happen if pasting a custom view
-                    debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
-                    return;
-                }
-
-                UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
-
-                // Copy all the attributes from the pasted element
-                for (IDragAttribute attr : childElement.getAttributes()) {
-                    uiChild.setAttributeValue(
-                            attr.getName(),
-                            attr.getUri(),
-                            attr.getValue(),
-                            true /*override*/);
-                }
-
-                // Adjust the attributes, adding the default layout_width/height
-                // only if they are not present (the original element should have
-                // them though.)
-                DescriptorsUtils.setDefaultLayoutAttributes(
-                        uiChild, false /*updateLayout*/);
-
-                uiChild.createXmlNode();
-
-                // Now process all grand children
-                for (IDragElement grandChildElement : childElement.getInnerElements()) {
-                    addChild(uiChild, grandChildElement);
-                }
-            }
-        });
+    /* package */ void delete() {
+        mDeleteAction.run();
     }
 
     /**
      * Add new root in an existing empty XML layout.
      * <p/>
      * In case of error (unknown FQCN, document not empty), silently do nothing.
-     * In case of success, the new element will have some default attributes set (xmlns:android,
-     * layout_width and height). The edit is wrapped in a proper undo.
+     * In case of success, the new element will have some default attributes set
+     * (xmlns:android, layout_width and height). The edit is wrapped in a proper
+     * undo.
      * <p/>
-     * This is invoked by {@link CanvasDropListener#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
+     * This is invoked by
+     * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}.
      *
-     * @param rootFqcn A non-null non-empty FQCN that must match an existing {@link ViewDescriptor}
-     *   to add as root to the current empty XML document.
+     * @param rootFqcn A non-null non-empty FQCN that must match an existing
+     *            {@link ViewElementDescriptor} to add as root to the current
+     *            empty XML document.
      */
     /* package */ void createDocumentRoot(String rootFqcn) {
 
@@ -2388,7 +850,7 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
         }
 
         // Find the view descriptor matching our FQCN
-        final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescritor(rootFqcn);
+        final ViewElementDescriptor viewDesc = mLayoutEditor.getFqcnViewDescriptor(rootFqcn);
         if (viewDesc == null) {
             // TODO this could happen if dropping a custom view not known in this project
             debugPrintf("Failed to add document root, unknown FQCN %1$s", rootFqcn);
@@ -2423,7 +885,9 @@ class LayoutCanvas extends Canvas implements ISelectionProvider {
     }
 
     private void debugPrintf(String message, Object... params) {
-        if (DEBUG) AdtPlugin.printToConsole("Canvas", String.format(message, params));
+        if (DEBUG) {
+            AdtPlugin.printToConsole("Canvas", String.format(message, params));
+        }
     }
 
 }
index f509c19..f1b4247 100755 (executable)
@@ -52,13 +52,15 @@ class LayoutCanvasViewer extends Viewer {
         mLayoutEditor = layoutEditor;
         mCanvas = new LayoutCanvas(layoutEditor, rulesEngine, parent, style);
 
-        mCanvas.addSelectionChangedListener(new ISelectionChangedListener() {
-            public void selectionChanged(SelectionChangedEvent event) {
-                fireSelectionChanged(event);
-            }
-        });
+        mCanvas.getSelectionManager().addSelectionChangedListener(mSelectionListener);
     }
 
+    private ISelectionChangedListener mSelectionListener = new ISelectionChangedListener() {
+        public void selectionChanged(SelectionChangedEvent event) {
+            fireSelectionChanged(event);
+        }
+    };
+
     @Override
     public Control getControl() {
         return mCanvas;
@@ -70,6 +72,7 @@ class LayoutCanvasViewer extends Viewer {
      * have it already casted in the right type.
      * <p/>
      * This can never be null.
+     * @return The underlying {@link LayoutCanvas}.
      */
     public LayoutCanvas getCanvas() {
         return mCanvas;
@@ -96,7 +99,7 @@ class LayoutCanvasViewer extends Viewer {
      */
     @Override
     public ISelection getSelection() {
-        return mCanvas.getSelection();
+        return mCanvas.getSelectionManager().getSelection();
     }
 
     /**
@@ -106,7 +109,7 @@ class LayoutCanvasViewer extends Viewer {
      */
     @Override
     public void setSelection(ISelection selection, boolean reveal) {
-        mCanvas.setSelection(selection);
+        mCanvas.getSelectionManager().setSelection(selection);
     }
 
     /** Unused. Refreshing is done solely by the owning {@link LayoutEditor}. */
@@ -120,5 +123,8 @@ class LayoutCanvasViewer extends Viewer {
             mCanvas.dispose();
             mCanvas = null;
         }
+        if (mSelectionListener != null) {
+            mCanvas.getSelectionManager().removeSelectionChangedListener(mSelectionListener);
+        }
     }
 }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPoint.java
new file mode 100644 (file)
index 0000000..77ea8da
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+
+/**
+ * A {@link LayoutPoint} is a coordinate in the Android canvas (in other words,
+ * it may differ from the canvas control mouse coordinate because the canvas may
+ * be zoomed and scrolled.)
+ */
+public final class LayoutPoint {
+    /** Containing canvas which the point is relative to. */
+    private final LayoutCanvas mCanvas;
+
+    /** The X coordinate of the canvas coordinate. */
+    public final int x;
+
+    /** The Y coordinate of the canvas coordinate. */
+    public final int y;
+
+    /**
+     * Constructs a new {@link LayoutPoint} from the given event. The event
+     * must be from a {@link MouseListener} associated with the
+     * {@link LayoutCanvas} such that the {@link MouseEvent#x} and
+     * {@link MouseEvent#y} fields are relative to the canvas.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param event The mouse event to construct the {@link LayoutPoint}
+     *            from.
+     * @return A {@link LayoutPoint} which corresponds to the given
+     *         {@link MouseEvent}.
+     */
+    public static LayoutPoint create(LayoutCanvas canvas, MouseEvent event) {
+        // The mouse event coordinates should already be relative to the canvas
+        // widget.
+        assert event.widget == canvas : event.widget;
+        return ControlPoint.create(canvas, event).toLayout();
+    }
+
+    /**
+     * Constructs a new {@link LayoutPoint} from the given event. The event
+     * must be from a {@link DragSourceListener} associated with the
+     * {@link LayoutCanvas} such that the {@link DragSourceEvent#x} and
+     * {@link DragSourceEvent#y} fields are relative to the canvas.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param event The mouse event to construct the {@link LayoutPoint}
+     *            from.
+     * @return A {@link LayoutPoint} which corresponds to the given
+     *         {@link DragSourceEvent}.
+     */
+    public static LayoutPoint create(LayoutCanvas canvas, DragSourceEvent event) {
+        // The drag source event coordinates should already be relative to the
+        // canvas widget.
+        return ControlPoint.create(canvas, event).toLayout();
+    }
+
+    /**
+     * Constructs a new {@link LayoutPoint} from the given x,y coordinates.
+     *
+     * @param canvas The {@link LayoutCanvas} this point is within.
+     * @param x The mouse event x coordinate relative to the canvas
+     * @param y The mouse event x coordinate relative to the canvas
+     * @return A {@link LayoutPoint} which corresponds to the given
+     *         layout coordinates.
+     */
+    public static LayoutPoint create(LayoutCanvas canvas, int x, int y) {
+        return new LayoutPoint(canvas, x, y);
+    }
+
+    /**
+     * Constructs a new {@link LayoutPoint} with the given X and Y coordinates.
+     *
+     * @param canvas The canvas which contains this coordinate
+     * @param x The canvas X coordinate
+     * @param y The canvas Y coordinate
+     */
+    private LayoutPoint(LayoutCanvas canvas, int x, int y) {
+        this.mCanvas = canvas;
+        this.x = x;
+        this.y = y;
+    }
+
+    /**
+     * Returns the equivalent {@link ControlPoint} to this
+     * {@link LayoutPoint}.
+     *
+     * @return The equivalent {@link ControlPoint} to this
+     *         {@link LayoutPoint}
+     */
+    public ControlPoint toControl() {
+        int cx = mCanvas.getHorizontalTransform().translate(x);
+        int cy = mCanvas.getVerticalTransform().translate(y);
+
+        return ControlPoint.create(mCanvas, cx, cy);
+    }
+
+    @Override
+    public String toString() {
+        return "LayoutPoint [x=" + x + ", y=" + y + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + x;
+        result = prime * result + y;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        LayoutPoint other = (LayoutPoint) obj;
+        if (x != other.x)
+            return false;
+        if (y != other.y)
+            return false;
+        return true;
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MarqueeGesture.java
new file mode 100644 (file)
index 0000000..f9c91f4
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * 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.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link MarqueeGesture} is a gesture for swiping out a selection rectangle.
+ * With a modifier key, items that intersect the rectangle can be toggled
+ * instead of added to the new selection set.
+ */
+public class MarqueeGesture extends Gesture {
+    /** The {@link Overlay} drawn for the marquee. */
+    private MarqueeOverlay mOverlay;
+
+    /** The canvas associated with this gesture. */
+    private LayoutCanvas mCanvas;
+
+    /** A copy of the initial selection, when we're toggling the marquee. */
+    private Collection<CanvasViewInfo> mInitialSelection;
+
+    /**
+     * Creates a new marquee selection (selection swiping).
+     *
+     * @param canvas The canvas where selection is performed.
+     * @param toggle If true, toggle the membership of contained elements
+     *            instead of adding it.
+     */
+    public MarqueeGesture(LayoutCanvas canvas, boolean toggle) {
+        this.mCanvas = canvas;
+
+        if (toggle) {
+            List<CanvasSelection> selection = canvas.getSelectionManager().getSelections();
+            mInitialSelection = new ArrayList<CanvasViewInfo>(selection.size());
+            for (CanvasSelection item : selection) {
+                mInitialSelection.add(item.getViewInfo());
+            }
+        } else {
+            mInitialSelection = Collections.emptySet();
+        }
+    }
+
+    @Override
+    public void update(ControlPoint pos) {
+        int x = Math.min(pos.x, mStart.x);
+        int y = Math.min(pos.y, mStart.y);
+        int w = Math.abs(pos.x - mStart.x);
+        int h = Math.abs(pos.y - mStart.y);
+
+        mOverlay.updateSize(x, y, w, h);
+
+        // Compute selection overlaps
+        LayoutPoint topLeft = ControlPoint.create(mCanvas, x, y).toLayout();
+        LayoutPoint bottomRight = ControlPoint.create(mCanvas, x + w, y + h).toLayout();
+        mCanvas.getSelectionManager().selectWithin(topLeft, bottomRight, mInitialSelection);
+    }
+
+    @Override
+    public List<Overlay> createOverlays() {
+        mOverlay = new MarqueeOverlay();
+        return Collections.<Overlay> singletonList(mOverlay);
+    }
+
+    /**
+     * An {@link Overlay} for the {@link MarqueeGesture}; paints a selection
+     * overlay rectangle matching the mouse coordinate delta between gesture
+     * start and the current position.
+     */
+    private class MarqueeOverlay extends Overlay {
+        /** Rectangle border color. */
+        private Color mStroke;
+
+        /** Rectangle fill color. */
+        private Color mFill;
+
+        /** Current rectangle coordinates (in terms of control coordinates). */
+        private Rectangle mRectangle = new Rectangle(0, 0, 0, 0);
+
+        /** Alpha value of the fill. */
+        private int mFillAlpha;
+
+        /** Alpha value of the border. */
+        private int mStrokeAlpha;
+
+        /** Constructs a new {@link MarqueeOverlay}. */
+        public MarqueeOverlay() {
+        }
+
+        /**
+         * Updates the size of the marquee rectangle.
+         *
+         * @param x The top left corner of the rectangle, x coordinate.
+         * @param y The top left corner of the rectangle, y coordinate.
+         * @param w Rectangle width.
+         * @param h Rectangle height.
+         */
+        public void updateSize(int x, int y, int w, int h) {
+            mRectangle.x = x;
+            mRectangle.y = y;
+            mRectangle.width = w;
+            mRectangle.height = h;
+        }
+
+        @Override
+        public void create(Device device) {
+            // TODO: Integrate DrawingStyles with this?
+            mStroke = new Color(device, 255, 255, 255);
+            mFill = new Color(device, 128, 128, 128);
+            mFillAlpha = 64;
+            mStrokeAlpha = 255;
+        }
+
+        @Override
+        public void dispose() {
+            mStroke.dispose();
+            mFill.dispose();
+        }
+
+        @Override
+        public void paint(GC gc) {
+            if (mRectangle.width > 0 && mRectangle.height > 0) {
+                gc.setLineStyle(SWT.LINE_SOLID);
+                gc.setLineWidth(1);
+                gc.setForeground(mStroke);
+                gc.setBackground(mFill);
+                gc.setAlpha(mStrokeAlpha);
+                gc.drawRectangle(mRectangle);
+                gc.setAlpha(mFillAlpha);
+                gc.fillRectangle(mRectangle);
+            }
+        }
+    }
+}
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -13,7 +13,6 @@
  * 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.api.DropFeedback;
@@ -22,30 +21,33 @@ import com.android.ide.common.api.Point;
 import com.android.ide.common.api.Rect;
 import com.android.ide.eclipse.adt.AdtPlugin;
 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
 
 import org.eclipse.swt.dnd.DND;
 import org.eclipse.swt.dnd.DropTargetEvent;
-import org.eclipse.swt.dnd.DropTargetListener;
 import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.graphics.GC;
 import org.eclipse.swt.widgets.Display;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 /**
- * Handles drop operations on top of the canvas.
- * <p/>
- * Reference for d'n'd: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
+ * The Move gesture provides the operation for moving widgets around in the canvas.
  */
-/* package */ class CanvasDropListener implements DropTargetListener {
+public class MoveGesture extends DropGesture {
+    /** The associated {@link LayoutCanvas}. */
+    private LayoutCanvas mCanvas;
 
-    private static final boolean DEBUG = false;
+    /** Overlay which paints the drag &amp; drop feedback. */
+    private MoveOverlay mOverlay;
 
-    private final LayoutCanvas mCanvas;
+    private static final boolean DEBUG = false;
 
     /**
      * The top view right under the drag'n'drop cursor.
@@ -88,10 +90,12 @@ import java.util.Set;
      * happens next.
      */
     private NodeProxy mLeaveTargetNode;
+
     /**
      * @see #mLeaveTargetNode
      */
     private DropFeedback mLeaveFeedback;
+
     /**
      * @see #mLeaveTargetNode
      */
@@ -100,15 +104,27 @@ import java.util.Set;
     /** Singleton used to keep track of drag selection in the same Eclipse instance. */
     private final GlobalCanvasDragInfo mGlobalDragInfo;
 
-    public CanvasDropListener(LayoutCanvas canvas) {
-        mCanvas = canvas;
+    /**
+     * Constructs a new {@link MoveGesture}, tied to the given canvas.
+     *
+     * @param canvas The canvas to associate the {@link MoveGesture} with.
+     */
+    public MoveGesture(LayoutCanvas canvas) {
+        this.mCanvas = canvas;
         mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
     }
 
+    @Override
+    public List<Overlay> createOverlays() {
+        mOverlay = new MoveOverlay();
+        return Collections.<Overlay> singletonList(mOverlay);
+    }
+
     /*
      * The cursor has entered the drop target boundaries.
      * {@inheritDoc}
      */
+    @Override
     public void dragEnter(DropTargetEvent event) {
         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);
 
@@ -156,6 +172,7 @@ import java.util.Set;
      * The operation being performed has changed (e.g. modifier key).
      * {@inheritDoc}
      */
+    @Override
     public void dragOperationChanged(DropTargetEvent event) {
         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
 
@@ -187,6 +204,7 @@ import java.util.Set;
      * The cursor has left the drop target boundaries OR data is about to be dropped.
      * {@inheritDoc}
      */
+    @Override
     public void dragLeave(DropTargetEvent event) {
         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");
 
@@ -206,6 +224,7 @@ import java.util.Set;
      * The cursor is moving over the drop target.
      * {@inheritDoc}
      */
+    @Override
     public void dragOver(DropTargetEvent event) {
         processDropEvent(event);
     }
@@ -215,6 +234,7 @@ import java.util.Set;
      * The drop target is given a last chance to change the nature of the drop.
      * {@inheritDoc}
      */
+    @Override
     public void dropAccept(DropTargetEvent event) {
         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");
 
@@ -246,6 +266,7 @@ import java.util.Set;
      * The data is being dropped.
      * {@inheritDoc}
      */
+    @Override
     public void drop(final DropTargetEvent event) {
         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");
 
@@ -269,7 +290,8 @@ import java.util.Set;
         }
 
         if (mTargetNode == null) {
-            if (mCanvas.isResultValid() && mCanvas.isEmptyDocument()) {
+            ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+            if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
                 // There is no target node because the drop happens on an empty document.
                 // Attempt to create a root node accordingly.
                 createDocumentRoot(elements);
@@ -279,7 +301,7 @@ import java.util.Set;
             return;
         }
 
-        final Point where = mCanvas.displayToCanvasPoint(event.x, event.y);
+        final LayoutPoint canvasPoint = ControlPoint.create(mCanvas, event).toLayout();
 
         // Record children of the target right before the drop (such that we can
         // find out after the drop which exact children were inserted)
@@ -297,7 +319,7 @@ import java.util.Set;
                 mCanvas.getRulesEngine().callOnDropped(mTargetNode,
                         elementsFinal,
                         mFeedback,
-                        where);
+                        new Point(canvasPoint.x, canvasPoint.y));
                 // Clean up drag if applicable
                 if (event.detail == DND.DROP_MOVE) {
                     GlobalCanvasDragInfo.getInstance().removeSource();
@@ -345,19 +367,19 @@ import java.util.Set;
     private boolean selectDropped(Collection<INode> nodes) {
         final Collection<CanvasViewInfo> newChildren = new ArrayList<CanvasViewInfo>();
         for (INode node : nodes) {
-            CanvasViewInfo viewInfo = mCanvas.findViewInfoFor(node);
+            CanvasViewInfo viewInfo = mCanvas.getViewHierarchy().findViewInfoFor(node);
             if (viewInfo != null) {
                 newChildren.add(viewInfo);
             }
         }
-        mCanvas.selectMultiple(newChildren);
+        mCanvas.getSelectionManager().selectMultiple(newChildren);
 
         return nodes.size() == newChildren.size();
     }
 
     /**
      * Computes a suitable Undo label to use for a drop operation, such as
-     * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout"
+     * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
      *
      * @param targetNode The target of the drop
      * @param elements The dragged widgets
@@ -390,13 +412,13 @@ import java.util.Set;
      * Returns simple name (basename, following last dot) of a fully qualified
      * class name.
      *
-     * @param fcqn The fqcn to reduce
+     * @param fqcn The fqcn to reduce
      * @return The base name of the fqcn
      */
     private String getSimpleName(String fqcn) {
         // Note that the following works even when there is no dot, since
         // lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
-        // fcqn.substring(0) = fcqn
+        // fcqn.substring(0) = fqcn
         return fqcn.substring(fqcn.lastIndexOf('.') + 1);
     }
 
@@ -417,17 +439,6 @@ import java.util.Set;
     }
 
     /**
-     * Invoked by the canvas to refresh the display.
-     * @param gCWrapper The GC wrapper, never null.
-     */
-    public void paintFeedback(GCWrapper gCWrapper) {
-        if (mTargetNode != null && mFeedback != null && mFeedback.requestPaint) {
-            mCanvas.getRulesEngine().callDropFeedbackPaint(gCWrapper, mTargetNode, mFeedback);
-            mFeedback.requestPaint = false;
-        }
-    }
-
-    /**
      * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
      * If not, try to find a valid data type.
      * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
@@ -466,7 +477,7 @@ import java.util.Set;
      * selected target node.
      */
     private void processDropEvent(DropTargetEvent event) {
-        if (!mCanvas.isResultValid()) {
+        if (!mCanvas.getViewHierarchy().isValid()) {
             // We don't allow drop on an invalid layout, even if we have some obsolete
             // layout info for it.
             event.detail = DND.DROP_NONE;
@@ -474,15 +485,13 @@ import java.util.Set;
             return;
         }
 
-        Point p = mCanvas.displayToCanvasPoint(event.x, event.y);
-        int x = p.x;
-        int y = p.y;
+        LayoutPoint p = ControlPoint.create(mCanvas, event).toLayout();
 
         // Is the mouse currently captured by a DropFeedback.captureArea?
         boolean isCaptured = false;
         if (mFeedback != null) {
             Rect r = mFeedback.captureArea;
-            isCaptured = r != null && r.contains(x, y);
+            isCaptured = r != null && r.contains(p.x, p.y);
         }
 
         // We can't switch views/nodes when the mouse is captured
@@ -490,7 +499,7 @@ import java.util.Set;
         if (isCaptured) {
             vi = mCurrentView;
         } else {
-            vi = mCanvas.findViewInfoAt(x, y);
+            vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
         }
 
         boolean isMove = true;
@@ -532,7 +541,7 @@ import java.util.Set;
                         // the -position- of the mouse), and we want this computation to happen
                         // before we ask the view to draw its feedback.
                         df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
-                                mCurrentDragElements, df, p);
+                                mCurrentDragElements, df, new Point(p.x, p.y));
                     }
 
                     if (df != null &&
@@ -587,7 +596,7 @@ import java.util.Set;
         if (isMove && mTargetNode != null && mFeedback != null) {
             // this is a move inside the same view
             com.android.ide.common.api.Point p2 =
-                new com.android.ide.common.api.Point(x, y);
+                new com.android.ide.common.api.Point(p.x, p.y);
             updateDropFeedback(mFeedback, event);
             DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
                     mTargetNode, mCurrentDragElements, mFeedback, p2);
@@ -655,4 +664,19 @@ import java.util.Set;
 
         mCanvas.createDocumentRoot(rootFqcn);
     }
+
+    /**
+     * An {@link Overlay} to paint the move feedback. This just delegates to the
+     * layout rules.
+     */
+    private class MoveOverlay extends Overlay {
+        @Override
+        public void paint(GC gc) {
+            if (mTargetNode != null && mFeedback != null) {
+                RulesEngine rulesEngine = mCanvas.getRulesEngine();
+                rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
+                mFeedback.requestPaint = false;
+            }
+        }
+    }
 }
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/OutlineOverlay.java
new file mode 100644 (file)
index 0000000..942da2b
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * 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.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Rectangle;
+
+/**
+ * The {@link OutlineOverlay} paints an optional outline on top of the layout,
+ * showing the structure of the individual Android View elements.
+ */
+public class OutlineOverlay extends Overlay {
+    /** The {@link ViewHierarchy} this outline visualizes */
+    private final ViewHierarchy mViewHierarchy;
+
+    /** Outline color. Must be disposed, it's NOT a system color. */
+    private Color mOutlineColor;
+
+    /** Vertical scaling & scrollbar information. */
+    private ScaleInfo mVScale;
+
+    /** Horizontal scaling & scrollbar information. */
+    private ScaleInfo mHScale;
+
+    /**
+     * Constructs a new {@link OutlineOverlay} linked to the given view
+     * hierarchy.
+     *
+     * @param viewHierarchy The {@link ViewHierarchy} to render
+     * @param hScale The {@link ScaleInfo} to use to transfer horizontal layout
+     *            coordinates to screen coordinates
+     * @param vScale The {@link ScaleInfo} to use to transfer vertical layout
+     *            coordinates to screen coordinates
+     */
+    public OutlineOverlay(ViewHierarchy viewHierarchy, ScaleInfo hScale, ScaleInfo vScale) {
+        super();
+        this.mViewHierarchy = viewHierarchy;
+        this.mHScale = hScale;
+        this.mVScale = vScale;
+    }
+
+    @Override
+    public void create(Device device) {
+        mOutlineColor = new Color(device, SwtDrawingStyle.OUTLINE.getStrokeColor());
+    }
+
+    @Override
+    public void dispose() {
+        if (mOutlineColor != null) {
+            mOutlineColor.dispose();
+            mOutlineColor = null;
+        }
+    }
+
+    @Override
+    public void paint(GC gc) {
+        CanvasViewInfo lastRoot = mViewHierarchy.getRoot();
+        if (lastRoot != null) {
+            gc.setForeground(mOutlineColor);
+            gc.setLineStyle(SwtDrawingStyle.OUTLINE.getLineStyle());
+            int oldAlpha = gc.getAlpha();
+            gc.setAlpha(SwtDrawingStyle.OUTLINE.getStrokeAlpha());
+            drawOutline(gc, lastRoot);
+            gc.setAlpha(oldAlpha);
+        }
+    }
+
+    private void drawOutline(GC gc, CanvasViewInfo info) {
+        Rectangle r = info.getAbsRect();
+
+        int x = mHScale.translate(r.x);
+        int y = mVScale.translate(r.y);
+        int w = mHScale.scale(r.width);
+        int h = mVScale.scale(r.height);
+
+        // Add +1 to the width and +1 to the height such that when you have a
+        // series of boxes (in say a LinearLayout), instead of the bottom of one
+        // box and the top of the next box being -adjacent-, they -overlap-.
+        // This makes the outline nicer visually since you don't get
+        // "double thickness" lines for all adjacent boxes.
+        gc.drawRectangle(x, y, w + 1, h + 1);
+
+        for (CanvasViewInfo vi : info.getChildren()) {
+            drawOutline(gc, vi);
+        }
+    }
+
+}
index fdbd8fd..0a267d6 100755 (executable)
@@ -155,8 +155,11 @@ public class OutlinePage2 extends ContentOutlinePage
             }
         });
 
-        mDragSource = LayoutCanvas.createDragSource(getControl(), new DelegateDragListener());
-        mDropTarget = LayoutCanvas.createDropTarget(getControl(), new DelegateDropListener());
+        mDragSource = LayoutCanvas.createDragSource(getControl());
+        mDragSource.addDragListener(new DelegateDragListener());
+
+        mDropTarget = LayoutCanvas.createDropTarget(getControl());
+        mDropTarget.addDropListener(new DelegateDropListener());
 
         setupContextMenu();
 
@@ -716,7 +719,7 @@ public class OutlinePage2 extends ContentOutlinePage
                 LayoutCanvas canvas = mGraphicalEditorPart.getCanvasControl();
                 if (canvas != null) {
                     com.android.ide.common.api.Point p =
-                        canvas.canvasToControlPoint(x, y);
+                        canvas.layoutToControlPoint(x, y);
 
                     inOutXY.x = p.x;
                     inOutXY.y = p.y;
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/Overlay.java
new file mode 100644 (file)
index 0000000..ac96d76
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.Device;
+import org.eclipse.swt.graphics.GC;
+
+/**
+ * An Overlay is a set of graphics which can be painted on top of the visual
+ * editor. Different {@link Gesture}s produce context specific overlays, such as
+ * swiping rectangles from the {@link MarqueeGesture} and guidelines from the
+ * {@link MoveGesture}.
+ */
+public abstract class Overlay {
+    /**
+     * Construct the overlay, using the given graphics context for painting.
+     */
+    public Overlay() {
+        super();
+    }
+
+    /**
+     * Initializes the overlay before the first use, if applicable. This is a
+     * good place to initialize resources like colors.
+     *
+     * @param device The device to allocate resources for; the parameter passed
+     *            to {@link #paint} will correspond to this device.
+     */
+    public void create(Device device) {
+    }
+
+    /**
+     * Releases resources held by the overlay. Called by the editor when an
+     * overlay has been removed.
+     */
+    public void dispose() {
+    }
+
+    /**
+     * Paints the overlay.
+     *
+     * @param gc The SWT {@link GC} object to draw into.
+     */
+    public void paint(GC gc) {
+        throw new IllegalArgumentException("paint() not implemented, probably done "
+                + "with specialized paint signature");
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ScaleInfo.java
new file mode 100644 (file)
index 0000000..0565549
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2009 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.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.ScrollBar;
+
+/**
+ * Helper class to convert between control pixel coordinates and canvas coordinates.
+ * Takes care of the zooming and offset of the canvas.
+ */
+public class ScaleInfo implements ICanvasTransform {
+    /**
+     * The canvas which controls the zooming.
+     */
+    private final LayoutCanvas mCanvas;
+
+    /** Canvas image size (original, before zoom), in pixels. */
+    private int mImgSize;
+
+    /** Client size, in pixels. */
+    private int mClientSize;
+
+    /** Left-top offset in client pixel coordinates. */
+    private int mTranslate;
+
+    /** Scaling factor, > 0. */
+    private double mScale;
+
+    /** Scrollbar widget. */
+    private ScrollBar mScrollbar;
+
+    public ScaleInfo(LayoutCanvas layoutCanvas, ScrollBar scrollbar) {
+        mCanvas = layoutCanvas;
+        mScrollbar = scrollbar;
+        mScale = 1.0;
+        mTranslate = 0;
+
+        mScrollbar.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                // User requested scrolling. Changes translation and redraw canvas.
+                mTranslate = mScrollbar.getSelection();
+                ScaleInfo.this.mCanvas.redraw();
+            }
+        });
+    }
+
+    /**
+     * Sets the new scaling factor. Recomputes scrollbars.
+     * @param scale Scaling factor, > 0.
+     */
+    public void setScale(double scale) {
+        if (mScale != scale) {
+            mScale = scale;
+            resizeScrollbar();
+        }
+    }
+
+    /**
+     * Returns current scaling factor.
+     *
+     * @return The current scaling factor
+     */
+    public double getScale() {
+        return mScale;
+    }
+
+    /**
+     * Returns Canvas image size (original, before zoom), in pixels.
+     *
+     * @return Canvas image size (original, before zoom), in pixels
+     */
+    public int getImgSize() {
+        return mImgSize;
+    }
+
+    /**
+     * Returns the scaled image size in pixels.
+     *
+     * @return The scaled image size in pixels.
+     */
+    public int getScalledImgSize() {
+        return (int) (mImgSize * mScale);
+    }
+
+    /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */
+    public void setSize(int imgSize, int clientSize) {
+        mImgSize = imgSize;
+        setClientSize(clientSize);
+    }
+
+    /** Changes the size of the client size. Recomputes scrollbars. */
+    public void setClientSize(int clientSize) {
+        mClientSize = clientSize;
+        resizeScrollbar();
+    }
+
+    private void resizeScrollbar() {
+        // scaled image size
+        int sx = (int) (mImgSize * mScale);
+
+        // actual client area is always reduced by the margins
+        int cx = mClientSize - 2 * IMAGE_MARGIN;
+
+        if (sx < cx) {
+            mScrollbar.setEnabled(false);
+        } else {
+            mScrollbar.setEnabled(true);
+
+            // max scroll value is the scaled image size
+            // thumb value is the actual viewable area out of the scaled img size
+            mScrollbar.setMaximum(sx);
+            mScrollbar.setThumb(cx);
+        }
+    }
+
+    public int translate(int canvasX) {
+        return IMAGE_MARGIN - mTranslate + (int) (mScale * canvasX);
+    }
+
+    public int scale(int canwasW) {
+        return (int) (mScale * canwasW);
+    }
+
+    public int inverseTranslate(int screenX) {
+        return (int) ((screenX - IMAGE_MARGIN + mTranslate) / mScale);
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java
new file mode 100644 (file)
index 0000000..affd69a
--- /dev/null
@@ -0,0 +1,623 @@
+/*
+ * 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.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.gef.ui.parts.TreeViewer;
+import org.eclipse.jface.util.SafeRunnable;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * The {@link SelectionManager} manages the selection in the canvas editor.
+ * It holds (and can be asked about) the set of selected items, and it also has
+ * operations for manipulating the selection - such as toggling items, copying
+ * the selection to the clipboard, etc.
+ * <p/>
+ * This class implements {@link ISelectionProvider} so that it can delegate
+ * the selection provider from the {@link LayoutCanvasViewer}.
+ * <p/>
+ * Note that {@link LayoutCanvasViewer} sets a selection change listener on this
+ * manager so that it can invoke its own fireSelectionChanged when the canvas'
+ * selection changes.
+ */
+public class SelectionManager implements ISelectionProvider {
+
+    private LayoutCanvas mCanvas;
+
+    /** The current selection list. The list is never null, however it can be empty. */
+    private final LinkedList<CanvasSelection> mSelections = new LinkedList<CanvasSelection>();
+
+    /** An unmodifiable view of {@link #mSelections}. */
+    private final List<CanvasSelection> mUnmodifiableSelection =
+        Collections.unmodifiableList(mSelections);
+
+    /** Barrier set when updating the selection to prevent from recursively
+     * invoking ourselves. */
+    private boolean mInsideUpdateSelection;
+
+    /**
+     * The <em>current</em> alternate selection, if any, which changes when the Alt key is
+     * used during a selection. Can be null.
+     */
+    private CanvasAlternateSelection mAltSelection;
+
+    /** List of clients listening to selection changes. */
+    private final ListenerList mSelectionListeners = new ListenerList();
+
+
+    /**
+     * Constructs a new {@link SelectionManager} associated with the given layout canvas.
+     *
+     * @param layoutCanvas The layout canvas to create a {@link SelectionManager} for.
+     */
+    public SelectionManager(LayoutCanvas layoutCanvas) {
+        this.mCanvas = layoutCanvas;
+    }
+
+    public void addSelectionChangedListener(ISelectionChangedListener listener) {
+         mSelectionListeners.add(listener);
+     }
+
+     public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+         mSelectionListeners.remove(listener);
+     }
+
+    /**
+     * Returns the native {@link CanvasSelection} list.
+     *
+     * @return An immutable list of {@link CanvasSelection}. Can be empty but not null.
+     * @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer}
+     *                      compatible {@link ISelection}.
+     */
+    /* package */ List<CanvasSelection> getSelections() {
+        return mUnmodifiableSelection;
+    }
+
+    /**
+     * Return a snapshot/copy of the selection. Useful for clipboards etc where we
+     * don't want the returned copy to be affected by future edits to the selection.
+     *
+     * @return A copy of the current selection. Never null.
+     */
+    /* package */ List<CanvasSelection> getSnapshot() {
+        return new ArrayList<CanvasSelection>(mSelections);
+    }
+
+    /**
+     * Returns a {@link TreeSelection} compatible with a TreeViewer
+     * where each {@link TreePath} item is actually a {@link CanvasViewInfo}.
+     */
+    public ISelection getSelection() {
+        if (mSelections.isEmpty()) {
+            return TreeSelection.EMPTY;
+        }
+
+        ArrayList<TreePath> paths = new ArrayList<TreePath>();
+
+        for (CanvasSelection cs : mSelections) {
+            CanvasViewInfo vi = cs.getViewInfo();
+            if (vi != null) {
+                ArrayList<Object> segments = new ArrayList<Object>();
+                while (vi != null) {
+                    segments.add(0, vi);
+                    vi = vi.getParent();
+                }
+                paths.add(new TreePath(segments.toArray()));
+            }
+        }
+
+        return new TreeSelection(paths.toArray(new TreePath[paths.size()]));
+    }
+
+    /**
+     * Sets the selection. It must be an {@link ITreeSelection} where each segment
+     * of the tree path is a {@link CanvasViewInfo}. A null selection is considered
+     * as an empty selection.
+     * <p/>
+     * This method is invoked by {@link LayoutCanvasViewer#setSelection(ISelection)}
+     * in response to an <em>outside</em> selection (compatible with ours) that has
+     * changed. Typically it means the outline selection has changed and we're
+     * synchronizing ours to match.
+     */
+    public void setSelection(ISelection selection) {
+        if (mInsideUpdateSelection) {
+            return;
+        }
+
+        try {
+            mInsideUpdateSelection = true;
+
+            if (selection == null) {
+                selection = TreeSelection.EMPTY;
+            }
+
+            if (selection instanceof ITreeSelection) {
+                ITreeSelection treeSel = (ITreeSelection) selection;
+
+                if (treeSel.isEmpty()) {
+                    // Clear existing selection, if any
+                    if (!mSelections.isEmpty()) {
+                        mSelections.clear();
+                        mAltSelection = null;
+                        redraw();
+                    }
+                    return;
+                }
+
+                boolean changed = false;
+
+                // Create a list of all currently selected view infos
+                Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>();
+                for (CanvasSelection cs : mSelections) {
+                    oldSelected.add(cs.getViewInfo());
+                }
+
+                // Go thru new selection and take care of selecting new items
+                // or marking those which are the same as in the current selection
+                for (TreePath path : treeSel.getPaths()) {
+                    Object seg = path.getLastSegment();
+                    if (seg instanceof CanvasViewInfo) {
+                        CanvasViewInfo newVi = (CanvasViewInfo) seg;
+                        if (oldSelected.contains(newVi)) {
+                            // This view info is already selected. Remove it from the
+                            // oldSelected list so that we don't deselect it later.
+                            oldSelected.remove(newVi);
+                        } else {
+                            // This view info is not already selected. Select it now.
+
+                            // reset alternate selection if any
+                            mAltSelection = null;
+                            // otherwise add it.
+                            mSelections.add(createSelection(newVi));
+                            changed = true;
+                        }
+                    }
+                }
+
+                // Deselect old selected items that are not in the new one
+                for (CanvasViewInfo vi : oldSelected) {
+                    deselect(vi);
+                    changed = true;
+                }
+
+                if (changed) {
+                    redraw();
+                    updateMenuActions();
+                }
+
+            }
+        } finally {
+            mInsideUpdateSelection = false;
+        }
+    }
+
+    /**
+     * Performs selection for a mouse event.
+     * <p/>
+     * Shift key (or Command on the Mac) 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).
+     *
+     * @param e The mouse event which triggered the selection. Cannot be null.
+     *            The modifier key mask will be used to determine whether this
+     *            is a plain select or a toggle, etc.
+     */
+    public void select(MouseEvent e) {
+
+        boolean isMultiClick = (e.stateMask & SWT.SHIFT) != 0 ||
+            // On Mac, the Command key is the normal toggle accelerator
+            ((SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) &&
+                    (e.stateMask & SWT.COMMAND) != 0);
+        boolean isCycleClick   = (e.stateMask & SWT.ALT)   != 0;
+
+        LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout();
+
+        if (e.button == 3) {
+            // Right click button is used to display a context menu.
+            // If there's an existing selection and the click is anywhere in this selection
+            // and there are no modifiers being used, we don't want to change the selection.
+            // Otherwise we select the item under the cursor.
+
+            if (!isCycleClick && !isMultiClick) {
+                for (CanvasSelection cs : mSelections) {
+                    if (cs.getRect().contains(p.x, p.y)) {
+                        // The cursor is inside the selection. Don't change anything.
+                        return;
+                    }
+                }
+            }
+
+        } else if (e.button != 1) {
+            // Click was done with something else than the left button for normal selection
+            // or the right button for context menu.
+            // We don't use mouse button 2 yet (middle mouse, or scroll wheel?) for
+            // anything, so let's not change the selection.
+            return;
+        }
+
+        CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
+
+        if (isMultiClick && !isCycleClick) {
+            // 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(createSelection(vi));
+                fireSelectionChanged();
+                redraw();
+            }
+
+        } else if (isCycleClick) {
+            // 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 CanvasAlternateSelection(
+                        vi, mCanvas.getViewHierarchy().findAltViewInfoAt(p));
+
+                // deselect them all, in case they were partially selected
+                deselectAll(mAltSelection.getAltViews());
+
+                // select the current one
+                CanvasViewInfo vi2 = mAltSelection.getCurrent();
+                if (vi2 != null) {
+                    mSelections.addFirst(createSelection(vi2));
+                    fireSelectionChanged();
+                }
+            } else {
+                // We're trying to cycle through the current alternate selection.
+                // First remove the current object.
+                CanvasViewInfo vi2 = mAltSelection.getCurrent();
+                deselect(vi2);
+
+                // Now select the next one.
+                vi2 = mAltSelection.getNext();
+                if (vi2 != null) {
+                    mSelections.addFirst(createSelection(vi2));
+                    fireSelectionChanged();
+                }
+            }
+            redraw();
+
+        } else {
+            // Case where no modifier is pressed: either select or reset the selection.
+
+            selectSingle(vi);
+        }
+    }
+
+    /**
+     * Removes all the currently selected item and only select the given item.
+     * Issues a {@link #redraw()} if the selection changes.
+     *
+     * @param vi The new selected item if non-null. Selection becomes empty if null.
+     */
+    /* package */ void selectSingle(CanvasViewInfo vi) {
+        // reset alternate selection if any
+        mAltSelection = null;
+
+        // reset (multi)selection if any
+        if (!mSelections.isEmpty()) {
+            if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) {
+                // CanvasSelection remains the same, don't touch it.
+                return;
+            }
+            mSelections.clear();
+        }
+
+        if (vi != null) {
+            mSelections.add(createSelection(vi));
+        }
+        fireSelectionChanged();
+        redraw();
+    }
+
+    /**
+     * Selects the given set of {@link CanvasViewInfo}s. This is similar to
+     * {@link #selectSingle} but allows you to make a multi-selection. Issues a
+     * {@link #redraw()}.
+     *
+     * @param viewInfos A collection of {@link CanvasViewInfo} objects to be
+     *            selected, or null or empty to clear the selection.
+     */
+    /* package */ void selectMultiple(Collection<CanvasViewInfo> viewInfos) {
+        // reset alternate selection if any
+        mAltSelection = null;
+
+        mSelections.clear();
+        if (viewInfos != null) {
+            for (CanvasViewInfo viewInfo : viewInfos) {
+                mSelections.add(createSelection(viewInfo));
+            }
+        }
+
+        fireSelectionChanged();
+        redraw();
+    }
+
+    /**
+     * Selects the visual element corresponding to the given XML node
+     * @param xmlNode The Node whose element we want to select.
+     */
+    /* package */ void select(Node xmlNode) {
+        CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoFor(xmlNode);
+        if (vi != null && !vi.isRoot()) {
+            selectSingle(vi);
+        }
+    }
+
+    /**
+     * Selects any views that overlap the given selection rectangle.
+     *
+     * @param topLeft The top left corner defining the selection rectangle.
+     * @param bottomRight The bottom right corner defining the selection
+     *            rectangle.
+     * @param toggled A set of {@link CanvasViewInfo}s that should be toggled
+     *            rather than just added.
+     */
+    public void selectWithin(LayoutPoint topLeft, LayoutPoint bottomRight,
+            Collection<CanvasViewInfo> toggled) {
+        // reset alternate selection if any
+        mAltSelection = null;
+
+        ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+        Collection<CanvasViewInfo> viewInfos = viewHierarchy.findWithin(topLeft, bottomRight);
+
+        if (toggled.size() > 0) {
+            // Copy; we're not allowed to touch the passed in collection
+            Set<CanvasViewInfo> result = new HashSet<CanvasViewInfo>(toggled);
+            for (CanvasViewInfo viewInfo : viewInfos) {
+                if (toggled.contains(viewInfo)) {
+                    result.remove(viewInfo);
+                } else {
+                    result.add(viewInfo);
+                }
+            }
+            viewInfos = result;
+        }
+
+        mSelections.clear();
+        for (CanvasViewInfo viewInfo : viewInfos) {
+            mSelections.add(createSelection(viewInfo));
+        }
+
+        fireSelectionChanged();
+        redraw();
+    }
+
+    /**
+     * Clears the selection and then selects everything (all views and all their
+     * children).
+     */
+    public void selectAll() {
+        // First clear the current selection, if any.
+        mSelections.clear();
+        mAltSelection = null;
+
+        // Now select everything if there's a valid layout
+        for (CanvasViewInfo vi : mCanvas.getViewHierarchy().findAllViewInfos(false)) {
+            mSelections.add(createSelection(vi));
+        }
+
+
+        fireSelectionChanged();
+        redraw();
+    }
+
+    /**
+     * Returns true if and only if there is currently more than one selected
+     * item.
+     *
+     * @return True if more than one item is selected
+     */
+    public boolean hasMultiSelection() {
+        return mSelections.size() > 1;
+    }
+
+    /**
+     * Deselects a view info. Returns true if the object was actually selected.
+     * Callers are responsible for calling redraw() and updateOulineSelection()
+     * after.
+     * @param canvasViewInfo The item to deselect.
+     * @return  True if the object was successfully removed from the selection.
+     */
+    public boolean deselect(CanvasViewInfo canvasViewInfo) {
+        if (canvasViewInfo == null) {
+            return false;
+        }
+
+        for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+            CanvasSelection s = it.next();
+            if (canvasViewInfo == s.getViewInfo()) {
+                it.remove();
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Deselects multiple view infos.
+     * Callers are responsible for calling redraw() and updateOulineSelection() after.
+     */
+    private void deselectAll(List<CanvasViewInfo> canvasViewInfos) {
+        for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+            CanvasSelection s = it.next();
+            if (canvasViewInfos.contains(s.getViewInfo())) {
+                it.remove();
+            }
+        }
+    }
+
+    /** Sync the selection with an updated view info tree */
+    /* package */ void sync(CanvasViewInfo lastValidViewInfoRoot) {
+        // Check if the selection is still the same (based on the object keys)
+        // and eventually recompute their bounds.
+        for (ListIterator<CanvasSelection> it = mSelections.listIterator(); it.hasNext(); ) {
+            CanvasSelection s = it.next();
+
+            // Check if the selected object still exists
+            ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
+            Object key = s.getViewInfo().getUiViewKey();
+            CanvasViewInfo vi = viewHierarchy.findViewInfoKey(key, lastValidViewInfoRoot);
+
+            // 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(createSelection(vi));
+            }
+        }
+        fireSelectionChanged();
+
+        // remove the current alternate selection views
+        mAltSelection = null;
+    }
+
+    /**
+     * Notifies listeners that the selection has changed.
+     */
+    private void fireSelectionChanged() {
+        if (mInsideUpdateSelection) {
+            return;
+        }
+        try {
+            mInsideUpdateSelection = true;
+
+            final SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
+
+            SafeRunnable.run(new SafeRunnable() {
+                public void run() {
+                    for (Object listener : mSelectionListeners.getListeners()) {
+                        ((ISelectionChangedListener) listener).selectionChanged(event);
+                    }
+                }
+            });
+
+            // Update menu actions that depend on the selection
+            updateMenuActions();
+
+        } finally {
+            mInsideUpdateSelection = false;
+        }
+    }
+
+    /**
+     * Sanitizes the selection for a copy/cut or drag operation.
+     * <p/>
+     * Sanitizes the list to make sure all elements have a valid XML attached to it,
+     * that is remove element that have no XML to avoid having to make repeated such
+     * checks in various places after.
+     * <p/>
+     * In case of multiple selection, we also need to remove all children when their
+     * parent is already selected since parents will always be added with all their
+     * children.
+     * <p/>
+     *
+     * @param selection The selection list to be sanitized <b>in-place</b>.
+     *      The <code>selection</code> argument should not be {@link #mSelections} -- the
+     *      given list is going to be altered and we should never alter the user-made selection.
+     *      Instead the caller should provide its own copy.
+     */
+    /* package */ static void sanitize(List<CanvasSelection> selection) {
+        if (selection.isEmpty()) {
+            return;
+        }
+
+        for (Iterator<CanvasSelection> it = selection.iterator(); it.hasNext(); ) {
+            CanvasSelection cs = it.next();
+            CanvasViewInfo vi = cs.getViewInfo();
+            UiViewElementNode key = vi == null ? null : vi.getUiViewKey();
+            Node node = key == null ? null : key.getXmlNode();
+            if (node == null) {
+                // Missing ViewInfo or view key or XML, discard this.
+                it.remove();
+                continue;
+            }
+
+            if (vi != null) {
+                for (Iterator<CanvasSelection> it2 = selection.iterator();
+                     it2.hasNext(); ) {
+                    CanvasSelection cs2 = it2.next();
+                    if (cs != cs2) {
+                        CanvasViewInfo vi2 = cs2.getViewInfo();
+                        if (vi.isParent(vi2)) {
+                            // vi2 is a parent for vi. Remove vi.
+                            it.remove();
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void updateMenuActions() {
+        boolean hasSelection = !mSelections.isEmpty();
+        mCanvas.updateMenuActions(hasSelection);
+    }
+
+    private void redraw() {
+        mCanvas.redraw();
+    }
+
+    private CanvasSelection createSelection(CanvasViewInfo vi) {
+        return new CanvasSelection(vi, mCanvas.getRulesEngine(),
+                mCanvas.getNodeFactory());
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionOverlay.java
new file mode 100644 (file)
index 0000000..05e405a
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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.eclipse.adt.internal.editors.layout.gre.RulesEngine;
+
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.GC;
+
+import java.util.List;
+
+/**
+ * The {@link SelectionOverlay} paints the current selection as an overlay.
+ */
+public class SelectionOverlay extends Overlay {
+    /** CanvasSelection border color. */
+    private Color mSelectionFgColor;
+
+    /**
+     * Constructs a new {@link SelectionOverlay} tied to the given canvas.
+     */
+    public SelectionOverlay() {
+    }
+
+    @Override
+    public void create(Device device) {
+        mSelectionFgColor = new Color(device, SwtDrawingStyle.SELECTION.getStrokeColor());
+    }
+
+    @Override
+    public void dispose() {
+        if (mSelectionFgColor != null) {
+            mSelectionFgColor.dispose();
+            mSelectionFgColor = null;
+        }
+    }
+
+    /**
+     * Paints the selection.
+     *
+     * @param selectionManager The {@link SelectionManager} holding the
+     *            selection.
+     * @param gc The graphics context to draw into.
+     * @param gcWrapper The graphics context wrapper for the layout rules to use.
+     * @param rulesEngine The {@link RulesEngine} holding the rules.
+     */
+    public void paint(SelectionManager selectionManager, GC gc, GCWrapper gcWrapper,
+            RulesEngine rulesEngine) {
+        List<CanvasSelection> selections = selectionManager.getSelections();
+        int n = selections.size();
+        if (n > 0) {
+            boolean isMultipleSelection = n > 1;
+
+            if (n == 1) {
+                gc.setForeground(mSelectionFgColor);
+                selections.get(0).paintParentSelection(rulesEngine, gcWrapper);
+            }
+
+            for (CanvasSelection s : selections) {
+                if (s.isRoot()) {
+                    // The root selection is never painted
+                    continue;
+                }
+                gc.setForeground(mSelectionFgColor);
+                s.paintSelection(rulesEngine, gcWrapper, isMultipleSelection);
+            }
+        }
+    }
+
+}
index 8c298e1..11f3ea2 100755 (executable)
@@ -59,7 +59,7 @@ import java.io.UnsupportedEncodingException;
  * {@link SimpleAttribute}s, all of which have very specific properties that are
  * specifically limited to our needs for drag'n'drop.
  */
-class SimpleXmlTransfer extends ByteArrayTransfer {
+final class SimpleXmlTransfer extends ByteArrayTransfer {
 
     // Reference: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
 
index db8dfcf..36fdad9 100644 (file)
@@ -34,7 +34,7 @@ public enum SwtDrawingStyle {
     /**
      * The style definition corresponding to {@link DrawingStyle#SELECTION}
      */
-    SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 2, SWT.LINE_DASH),
+    SELECTION(new RGB(0x00, 0x99, 0xFF), 255, new RGB(0x00, 0x99, 0xFF), 64, 1, SWT.LINE_DASH),
 
     /**
      * The style definition corresponding to {@link DrawingStyle#GUIDELINE}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ViewHierarchy.java
new file mode 100644 (file)
index 0000000..24d0caa
--- /dev/null
@@ -0,0 +1,423 @@
+/*
+ * 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.api.INode;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
+import com.android.layoutlib.api.ILayoutResult;
+import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
+
+import org.eclipse.swt.graphics.Rectangle;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * The view hierarchy class manages a set of view info objects and performs find
+ * operations on this set.
+ */
+public class ViewHierarchy {
+    private LayoutCanvas mCanvas;
+
+    /**
+     * Constructs a new {@link ViewHierarchy} tied to the given
+     * {@link LayoutCanvas}.
+     *
+     * @param canvas The {@link LayoutCanvas} to create a {@link ViewHierarchy}
+     *            for.
+     */
+    public ViewHierarchy(LayoutCanvas canvas) {
+        this.mCanvas = canvas;
+    }
+
+    /**
+     * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)}
+     * with a valid layout.
+     * <p/>
+     * This <em>can</em> be null to indicate we're dealing with an empty document with
+     * no root node. Null here does not mean the result was invalid, merely that the XML
+     * had no content to display -- we need to treat an empty document as valid so that
+     * we can drop new items in it.
+     */
+    private CanvasViewInfo mLastValidViewInfoRoot;
+
+    /**
+     * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}.
+     * <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.
+     * <p/>
+     * Note that an empty document (with a null {@link #mLastValidViewInfoRoot}) is considered
+     * valid since it is an acceptable drop target.
+     */
+    private boolean mIsResultValid;
+
+    /**
+     * 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.
+     *
+     * Implementation detail: the bridge's computeLayout() method already returns a newly
+     * allocated ILayourResult. That means we can keep this result and hold on to it
+     * when it is valid.
+     *
+     * @param result The new rendering result, either valid or not.
+     */
+    /* package */ void setResult(ILayoutResult result) {
+        mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);
+
+        if (mIsResultValid && result != null) {
+            ILayoutViewInfo root = result.getRootView();
+            if (root == null) {
+                mLastValidViewInfoRoot = null;
+            } else {
+                mLastValidViewInfoRoot = new CanvasViewInfo(result.getRootView());
+            }
+
+            updateNodeProxies(mLastValidViewInfoRoot);
+
+            // Update the selection
+            mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot);
+        }
+    }
+
+    /**
+     * Creates or updates the node proxy for this canvas view info.
+     * <p/>
+     * Since proxies are reused, this will update the bounds of an existing proxy when the
+     * canvas is refreshed and a view changes position or size.
+     * <p/>
+     * This is a recursive call that updates the whole hierarchy starting at the given
+     * view info.
+     */
+    private void updateNodeProxies(CanvasViewInfo vi) {
+        if (vi == null) {
+            return;
+        }
+
+        UiViewElementNode key = vi.getUiViewKey();
+
+        if (key != null) {
+            mCanvas.getNodeFactory().create(vi);
+        }
+
+        for (CanvasViewInfo child : vi.getChildren()) {
+            updateNodeProxies(child);
+        }
+    }
+
+
+
+    /**
+     * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid
+     * {@link ILayoutResult}.
+     * <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.
+     * <p/>
+     * Note that an empty document (with a null {@link #getRoot()}) is considered
+     * valid since it is an acceptable drop target.
+     * @return True when this {@link ViewHierarchy} contains a valid hierarchy of views.
+    */
+    public boolean isValid() {
+        return mIsResultValid;
+    }
+
+    /**
+     * Returns true if the last valid content of the canvas represents an empty document.
+     * @return True if the last valid content of the canvas represents an empty document.
+     */
+    public boolean isEmpty() {
+        return mLastValidViewInfoRoot == null;
+    }
+
+    /** Locates and return any views that overlap the given selection rectangle.
+     * @param topLeft The top left corner of the selection rectangle.
+     * @param bottomRight The bottom right corner of the selection rectangle.
+     * @return A collection of {@link CanvasViewInfo} objects that overlap the
+     *   rectangle.
+     */
+    public Collection<CanvasViewInfo> findWithin(
+            LayoutPoint topLeft,
+            LayoutPoint bottomRight) {
+        Rectangle selectionRectangle = new Rectangle(topLeft.x, topLeft.y, bottomRight.x
+                - topLeft.x, bottomRight.y - topLeft.y);
+        List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+        addWithin(mLastValidViewInfoRoot, selectionRectangle, infos);
+        return infos;
+    }
+
+    /**
+     * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+     * <p/>
+     * Tries to find the inner most child matching the given x,y coordinates in the view
+     * info sub-tree. This uses the potentially-expanded selection bounds.
+     *
+     * Returns null if not found.
+     */
+    private void addWithin(
+            CanvasViewInfo canvasViewInfo,
+            Rectangle canvasRectangle,
+            List<CanvasViewInfo> infos) {
+        if (canvasViewInfo == null) {
+            return;
+        }
+        Rectangle r = canvasViewInfo.getSelectionRect();
+        if (canvasRectangle.intersects(r)) {
+
+            // try to find a matching child first
+            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+                addWithin(child, canvasRectangle, infos);
+            }
+
+            if (canvasViewInfo != mLastValidViewInfoRoot) {
+                infos.add(canvasViewInfo);
+            }
+        }
+    }
+
+    /**
+     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+     * node, or null if it cannot be found.
+     *
+     * @param node The node we want to find a corresponding
+     *            {@link CanvasViewInfo} for.
+     * @return The {@link CanvasViewInfo} corresponding to the given node, or
+     *         null if no match was found.
+     */
+    public CanvasViewInfo findViewInfoFor(Node node) {
+        if (mLastValidViewInfoRoot != null) {
+            return findViewInfoForNode(node, mLastValidViewInfoRoot);
+        }
+        return null;
+    }
+
+    /**
+     * Tries to find a child with the same view XML node in the view info sub-tree.
+     * Returns null if not found.
+     */
+    private CanvasViewInfo findViewInfoForNode(Node xmlNode, CanvasViewInfo canvasViewInfo) {
+        if (canvasViewInfo == null) {
+            return null;
+        }
+        if (canvasViewInfo.getXmlNode() == xmlNode) {
+            return canvasViewInfo;
+        }
+
+        // Try to find a matching child
+        for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+            CanvasViewInfo v = findViewInfoForNode(xmlNode, child);
+            if (v != null) {
+                return v;
+            }
+        }
+
+        return null;
+    }
+
+
+    /**
+     * Tries to find the inner most child matching the given x,y coordinates in
+     * the view info sub-tree, starting at the last know view info root. This
+     * uses the potentially-expanded selection bounds.
+     * <p/>
+     * Returns null if not found or if there's no view info root.
+     *
+     * @param p The point at which to look for the deepest match in the view
+     *            hierarchy
+     * @return A {@link CanvasViewInfo} that intersects the given point, or null
+     *         if nothing was found.
+     */
+    public CanvasViewInfo findViewInfoAt(LayoutPoint p) {
+        if (mLastValidViewInfoRoot == null) {
+            return null;
+        }
+
+        return findViewInfoAt_Recursive(p, mLastValidViewInfoRoot);
+    }
+
+    /**
+     * Recursive internal version of {@link #findViewInfoAt(int, int)}. Please don't use directly.
+     * <p/>
+     * Tries to find the inner most child matching the given x,y coordinates in the view
+     * info sub-tree. This uses the potentially-expanded selection bounds.
+     *
+     * Returns null if not found.
+     */
+    private CanvasViewInfo findViewInfoAt_Recursive(LayoutPoint p, CanvasViewInfo canvasViewInfo) {
+        if (canvasViewInfo == null) {
+            return null;
+        }
+        Rectangle r = canvasViewInfo.getSelectionRect();
+        if (r.contains(p.x, p.y)) {
+
+            // try to find a matching child first
+            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+                CanvasViewInfo v = findViewInfoAt_Recursive(p, child);
+                if (v != null) {
+                    return v;
+                }
+            }
+
+            // if no children matched, this is the view that we're looking for
+            return canvasViewInfo;
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns a list of all the possible alternatives for a given view at the given
+     * position. This is used to build and manage the "alternate" selection that cycles
+     * around the parents or children of the currently selected element.
+     */
+    /* package */ List<CanvasViewInfo> findAltViewInfoAt(LayoutPoint p) {
+        if (mLastValidViewInfoRoot != null) {
+            return findAltViewInfoAt_Recursive(p, mLastValidViewInfoRoot, null);
+        }
+
+        return null;
+    }
+
+    /**
+     * Internal recursive version of {@link #findAltViewInfoAt(int, int, CanvasViewInfo)}.
+     * Please don't use directly.
+     */
+    private List<CanvasViewInfo> findAltViewInfoAt_Recursive(
+            LayoutPoint p, CanvasViewInfo parent, List<CanvasViewInfo> outList) {
+        Rectangle r;
+
+        if (outList == null) {
+            outList = new ArrayList<CanvasViewInfo>();
+
+            if (parent != null) {
+                // add the parent root only once
+                r = parent.getSelectionRect();
+                if (r.contains(p.x, p.y)) {
+                    outList.add(parent);
+                }
+            }
+        }
+
+        if (parent != null && !parent.getChildren().isEmpty()) {
+            // then add all children that match the position
+            for (CanvasViewInfo child : parent.getChildren()) {
+                r = child.getSelectionRect();
+                if (r.contains(p.x, p.y)) {
+                    outList.add(child);
+                }
+            }
+
+            // finally recurse in the children
+            for (CanvasViewInfo child : parent.getChildren()) {
+                r = child.getSelectionRect();
+                if (r.contains(p.x, p.y)) {
+                    findAltViewInfoAt_Recursive(p, child, outList);
+                }
+            }
+        }
+
+        return outList;
+    }
+
+    /**
+     * Locates and returns the {@link CanvasViewInfo} corresponding to the given
+     * node, or null if it cannot be found.
+     *
+     * @param node The node we want to find a corresponding
+     *            {@link CanvasViewInfo} for.
+     * @return The {@link CanvasViewInfo} corresponding to the given node, or
+     *         null if no match was found.
+     */
+    public CanvasViewInfo findViewInfoFor(INode node) {
+        if (mLastValidViewInfoRoot != null && node instanceof NodeProxy) {
+            return findViewInfoKey(((NodeProxy) node).getNode(), mLastValidViewInfoRoot);
+        }
+        return null;
+    }
+
+    /**
+     * Tries to find a child with the same view key in the view info sub-tree.
+     * Returns null if not found.
+     *
+     * @param viewKey The view key that a matching {@link CanvasViewInfo} should
+     *            have as its key.
+     * @param canvasViewInfo A root {@link CanvasViewInfo} to search from.
+     * @return A {@link CanvasViewInfo} matching the given key, or null if not
+     *         found.
+     */
+    public CanvasViewInfo findViewInfoKey(Object viewKey, CanvasViewInfo canvasViewInfo) {
+        if (canvasViewInfo == null) {
+            return null;
+        }
+        if (canvasViewInfo.getUiViewKey() == viewKey) {
+            return canvasViewInfo;
+        }
+
+        // try to find a matching child
+        for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+            CanvasViewInfo v = findViewInfoKey(viewKey, child);
+            if (v != null) {
+                return v;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns a list of ALL ViewInfos (possibly excluding the root, depending
+     * on the parameter for that).
+     *
+     * @param includeRoot If true, include the root in the list, otherwise
+     *            exclude it (but include all its children)
+     * @return A list of canvas view infos.
+     */
+    public List<CanvasViewInfo> findAllViewInfos(boolean includeRoot) {
+        List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+        if (mIsResultValid && mLastValidViewInfoRoot != null) {
+            findAllViewInfos(infos, mLastValidViewInfoRoot, includeRoot);
+        }
+
+        return infos;
+    }
+
+    private void findAllViewInfos(List<CanvasViewInfo> result, CanvasViewInfo canvasViewInfo,
+            boolean includeRoot) {
+        if (canvasViewInfo != null) {
+            if (includeRoot || !canvasViewInfo.isRoot()) {
+                result.add(canvasViewInfo);
+            }
+            for (CanvasViewInfo child : canvasViewInfo.getChildren()) {
+                findAllViewInfos(result, child, true);
+            }
+        }
+    }
+
+    /**
+     * Return the root of the view hierarchy, if any (could be null, for example
+     * on rendering failure).
+     *
+     * @return The current view hierarchy, or null
+     */
+    public CanvasViewInfo getRoot() {
+        return mLastValidViewInfoRoot;
+    }
+
+}
index c6c84a7..530be06 100755 (executable)
@@ -367,7 +367,7 @@ public class NodeProxy implements INode {
     private ViewElementDescriptor getFqcnViewDescritor(String fqcn) {
         AndroidXmlEditor editor = mNode.getEditor();
         if (editor instanceof LayoutEditor) {
-            return ((LayoutEditor) editor).getFqcnViewDescritor(fqcn);
+            return ((LayoutEditor) editor).getFqcnViewDescriptor(fqcn);
         }
 
         return null;
index dc8b79c..1851fa7 100644 (file)
@@ -92,7 +92,7 @@ public class UiViewElementNode extends UiElementNode {
                     }
                 }
             }
-        } else if (ui_parent instanceof UiViewElementNode){
+        } else if (ui_parent instanceof UiViewElementNode) {
             layout_attrs =
                 ((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes();
         }
index 5c88cf2..bb75bcc 100644 (file)
@@ -98,7 +98,7 @@ class InstrumentationRunnerValidator {
     /**
      * Return the set of instrumentation names for the Android project.
      *
-     * @return <code>null</code if error occurred parsing instrumentations, otherwise returns array
+     * @return <code>null</code> if error occurred parsing instrumentations, otherwise returns array
      * of instrumentation class names
      */
     String[] getInstrumentationNames() {
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPointTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ControlPointTest.java
new file mode 100644 (file)
index 0000000..366691e
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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.events.MouseEvent;
+
+public class ControlPointTest extends PointTestCases {
+    public void testCreateFromMouseEvent() throws Exception {
+        MouseEvent mouseEvent = canvasMouseEvent(10, 20, 0);
+
+        ControlPoint point = ControlPoint.create(mCanvas, mouseEvent);
+        assertEquals(10, point.x);
+        assertEquals(20, point.y);
+    }
+
+    public void testCreateFromCoordinates() throws Exception {
+        ControlPoint point = ControlPoint.create(mCanvas, 10, 20);
+        assertEquals(10, point.x);
+        assertEquals(20, point.y);
+    }
+
+    public void testConvertToLayout() throws Exception {
+        ControlPoint point = ControlPoint.create(new TestLayoutCanvas(), 10, 20);
+        assertEquals(10, point.x);
+        assertEquals(20, point.y);
+
+        LayoutPoint layoutPoint = point.toLayout();
+        assertNotNull(layoutPoint);
+        assertEquals(40, layoutPoint.x);
+        assertEquals(60, layoutPoint.y);
+
+        // For sanity let's also convert back and verify
+        ControlPoint controlPoint = layoutPoint.toControl();
+        assertNotNull(controlPoint);
+        assertNotSame(controlPoint, point);
+        assertEquals(point, controlPoint);
+        assertEquals(10, controlPoint.x);
+        assertEquals(20, controlPoint.y);
+    }
+
+    public void testEquals() throws Exception {
+        ControlPoint point1 = ControlPoint.create(mCanvas, 1, 1);
+        ControlPoint point2 = ControlPoint.create(mCanvas, 1, 2);
+        ControlPoint point3 = ControlPoint.create(mCanvas, 2, 1);
+        ControlPoint point2b = ControlPoint.create(mCanvas, 1, 2);
+
+        assertFalse(point2.equals(null));
+
+        assertEquals(point2, point2);
+        assertEquals(point2, point2b);
+        assertEquals(point2.hashCode(), point2b.hashCode());
+        assertNotSame(point2, point2b);
+
+        assertFalse(point1.equals(point2));
+        assertFalse(point1.equals(point3));
+        assertFalse(point2.equals(point3));
+        assertFalse(point1.equals(point2));
+    }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPointTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutPointTest.java
new file mode 100644 (file)
index 0000000..c053de8
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+public class LayoutPointTest extends PointTestCases {
+    public void testCreateFromCoordinates() throws Exception {
+        LayoutPoint point = LayoutPoint.create(mCanvas, 10, 20);
+        assertEquals(10, point.x);
+        assertEquals(20, point.y);
+    }
+
+    public void testEquals() throws Exception {
+        LayoutPoint point1 = LayoutPoint.create(mCanvas, 1, 1);
+        LayoutPoint point2 = LayoutPoint.create(mCanvas, 1, 2);
+        LayoutPoint point3 = LayoutPoint.create(mCanvas, 2, 1);
+        LayoutPoint point2b = LayoutPoint.create(mCanvas, 1, 2);
+
+        assertFalse(point2.equals(null));
+
+        assertEquals(point2, point2);
+        assertEquals(point2, point2b);
+        assertEquals(point2.hashCode(), point2b.hashCode());
+        assertNotSame(point2, point2b);
+
+        assertFalse(point1.equals(point2));
+        assertFalse(point1.equals(point3));
+        assertFalse(point2.equals(point3));
+        assertFalse(point1.equals(point2));
+    }
+
+    public void testConvertToControl() throws Exception {
+        LayoutPoint point = LayoutPoint.create(new TestLayoutCanvas(), 10, 20);
+        assertEquals(10, point.x);
+        assertEquals(20, point.y);
+
+        ControlPoint controlPoint = point.toControl();
+        assertNotNull(controlPoint);
+        assertEquals(-5, controlPoint.x);
+        assertEquals(0, controlPoint.y);
+
+        // For sanity let's also convert back and verify
+        LayoutPoint layoutPoint = controlPoint.toLayout();
+        assertNotNull(layoutPoint);
+        assertNotSame(layoutPoint, point);
+        assertEquals(point, layoutPoint);
+        assertEquals(10, layoutPoint.x);
+        assertEquals(20, layoutPoint.y);
+    }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PointTestCases.java
new file mode 100644 (file)
index 0000000..e68d2a5
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.List;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
+
+import junit.framework.TestCase;
+
+/**
+ * Common utilities for the point tests {@link LayoutPointTest} and
+ * {@link ControlPointTest}
+ */
+public abstract class PointTestCases extends TestCase {
+    LayoutCanvas mCanvas = new TestLayoutCanvas();
+
+    protected MouseEvent canvasMouseEvent(int x, int y, int stateMask) {
+        Event event = new Event();
+        event.x = x;
+        event.y = y;
+        event.stateMask = stateMask;
+        event.widget = mCanvas;
+        MouseEvent mouseEvent = new MouseEvent(event);
+        return mouseEvent;
+    }
+
+    /** Mock implementation of LayoutCanvas */
+    protected static class TestLayoutCanvas extends LayoutCanvas {
+        float mScaleX;
+
+        float mScaleY;
+
+        float mTranslateX;
+
+        float mTranslateY;
+
+        public TestLayoutCanvas(float scaleX, float scaleY, float translateX, float translateY) {
+            super(null, null, new Shell(), 0);
+
+            this.mScaleX = scaleX;
+            this.mScaleY = scaleY;
+            this.mTranslateX = translateX;
+            this.mTranslateY = translateY;
+        }
+
+        public TestLayoutCanvas() {
+            this(2.0f, 2.0f, 20.0f, 20.0f);
+        }
+
+        @Override
+        ScaleInfo getHorizontalTransform() {
+            ScrollBar scrollBar = new List(this, SWT.V_SCROLL|SWT.H_SCROLL).getHorizontalBar();
+            return new TestScaleInfo(scrollBar, mScaleX, mTranslateX);
+        }
+
+        @Override
+        ScaleInfo getVerticalTransform() {
+            ScrollBar scrollBar = new List(this, SWT.V_SCROLL|SWT.H_SCROLL).getVerticalBar();
+            return new TestScaleInfo(scrollBar, mScaleY, mTranslateY);
+        }
+    }
+
+    static class TestScaleInfo extends ScaleInfo {
+        float mScale;
+
+        float mTranslate;
+
+        public TestScaleInfo(ScrollBar scrollBar, float scale, float translate) {
+            super(null, scrollBar);
+            this.mScale = scale;
+            this.mTranslate = translate;
+        }
+
+        @Override
+        public int translate(int value) {
+            return (int) ((value - mTranslate) / mScale);
+        }
+
+        @Override
+        public int inverseTranslate(int value) {
+            return (int) (value * mScale + mTranslate);
+        }
+    }
+}