OSDN Git Service

ADT: Display selection and mouse hover in GLE canvas.
authorRaphael <raphael@google.com>
Sat, 5 Sep 2009 00:56:59 +0000 (17:56 -0700)
committerRaphael <raphael@google.com>
Sat, 5 Sep 2009 00:59:56 +0000 (17:59 -0700)
Change-Id: Icc2f8331a099905d6e1aaa52b36cc17a7190cc4b

eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/GraphicalEditorPart.java
eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java

index 3504fe7..cc3d928 100755 (executable)
@@ -85,6 +85,7 @@ import java.util.Map;
  * @since GLE2\r
  *\r
  * TODO List:\r
+ * - display error icon\r
  * - finish palette (see palette's todo list)\r
  * - finish canvas (see canva's todo list)\r
  * - completly rethink the property panel\r
@@ -901,6 +902,9 @@ public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutE
     /**\r
      * Computes a layout by calling the correct computeLayout method of ILayoutBridge based on\r
      * the implementation API level.\r
+     *\r
+     * Implementation detail: the bridge's computeLayout() method already returns a newly\r
+     * allocated ILayourResult.\r
      */\r
     @SuppressWarnings("deprecation")\r
     private static ILayoutResult computeLayout(LayoutBridge bridge,\r
@@ -954,38 +958,6 @@ public class GraphicalEditorPart extends EditorPart implements IGraphicalLayoutE
         return mConfigComposite.getScreenBounds();\r
     }\r
 \r
-    /** @deprecated for GLE2 */\r
-    private void resetNodeBounds(UiElementNode node) {\r
-        node.setEditData(null);\r
-\r
-        List<UiElementNode> children = node.getUiChildren();\r
-        for (UiElementNode child : children) {\r
-            resetNodeBounds(child);\r
-        }\r
-    }\r
-\r
-    /** @deprecated for GLE2 */\r
-    private void updateNodeWithBounds(ILayoutViewInfo r) {\r
-        if (r != null) {\r
-            // update the node itself, as the viewKey is the XML node in this implementation.\r
-            Object viewKey = r.getViewKey();\r
-            if (viewKey instanceof UiElementNode) {\r
-                Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(),\r
-                        r.getRight()-r.getLeft(), r.getBottom() - r.getTop());\r
-\r
-                ((UiElementNode)viewKey).setEditData(bounds);\r
-            }\r
-\r
-            // and then its children.\r
-            ILayoutViewInfo[] children = r.getChildren();\r
-            if (children != null) {\r
-                for (ILayoutViewInfo child : children) {\r
-                    updateNodeWithBounds(child);\r
-                }\r
-            }\r
-        }\r
-    }\r
-\r
     public void reloadPalette() {\r
         if (mPalette != null) {\r
             mPalette.reloadPalette(mLayoutEditor.getTargetData());\r
index 0ce381d..69846eb 100755 (executable)
 package com.android.ide.eclipse.adt.internal.editors.layout;\r
 \r
 import com.android.layoutlib.api.ILayoutResult;\r
+import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;\r
 \r
+import org.eclipse.swt.SWT;\r
+import org.eclipse.swt.events.MouseEvent;\r
+import org.eclipse.swt.events.MouseListener;\r
+import org.eclipse.swt.events.MouseMoveListener;\r
 import org.eclipse.swt.events.PaintEvent;\r
 import org.eclipse.swt.events.PaintListener;\r
+import org.eclipse.swt.graphics.Color;\r
+import org.eclipse.swt.graphics.Font;\r
+import org.eclipse.swt.graphics.FontMetrics;\r
 import org.eclipse.swt.graphics.GC;\r
 import org.eclipse.swt.graphics.Image;\r
 import org.eclipse.swt.graphics.ImageData;\r
 import org.eclipse.swt.graphics.PaletteData;\r
+import org.eclipse.swt.graphics.Rectangle;\r
 import org.eclipse.swt.widgets.Canvas;\r
 import org.eclipse.swt.widgets.Composite;\r
+import org.eclipse.swt.widgets.Display;\r
 \r
 import java.awt.image.BufferedImage;\r
 import java.awt.image.DataBufferInt;\r
@@ -39,36 +49,127 @@ import java.awt.image.Raster;
  * @since GLE2\r
  *\r
  * TODO list:\r
+ * - gray on error, keep select but disable d'n'd.\r
  * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.)\r
- * - handle selection (will need the model, aka the root node)/\r
- * - handle drop target (from palette)/\r
- * - handle drag'n'drop (internal, for moving/duplicating)/\r
- * - handle context menu (depending on selection)/\r
- * - selection synchronization with the outline (both ways)/\r
+ * - handle selection (will need the model, aka the root node).\r
+ * - handle drop target (from palette).\r
+ * - handle drag'n'drop (internal, for moving/duplicating).\r
+ * - handle context menu (depending on selection).\r
+ * - selection synchronization with the outline (both ways).\r
  * - preserve selection during editor input change if applicable (e.g. when changing configuration.)\r
  */\r
 public class LayoutCanvas extends Canvas {\r
 \r
+    private static final int IMAGE_MARGIN = 5;\r
+    private static final int SELECTION_MARGIN = 2;\r
+\r
+    private ILayoutResult mLastValidResult;\r
+\r
+    /** Current background image. Null when there's no image. */\r
     private Image mImage;\r
 \r
+    /** Current selected view info. Null when none is selected. */\r
+    private ILayoutViewInfo mSelectionViewInfo;\r
+    /** Current selection border rectangle. Null when there's no selection. */\r
+    private Rectangle mSelectionRect;\r
+    /** The name displayed over the selection, typically the widget class name. */\r
+    private String mSelectionName;\r
+    /** Selection border color. Do not dispose, it's a system color. */\r
+    private Color mSelectionFgColor;\r
+    /** Selection name font. Do not dispose, it's a system font. */\r
+    private Font mSelectionFont;\r
+    /** Pixel height of the font displaying the selection name. Initially set to 0 and only\r
+     * initialized in onPaint() when we have a GC. */\r
+    private int mSelectionFontHeight;\r
+\r
+    /** Current hover view info. Null when no mouse hover. */\r
+    private ILayoutViewInfo mHoverViewInfo;\r
+    /** Current mouse hover border rectangle. Null when there's no mouse hover. */\r
+    private Rectangle mHoverRect;\r
+    /** Hover border color. Do not dispose, it's a system color. */\r
+    private Color mHoverFgColor;\r
+\r
+    private boolean mIsResultValid;\r
+\r
+\r
+\r
     public LayoutCanvas(Composite parent, int style) {\r
-        super(parent, style);\r
+        super(parent, style | SWT.DOUBLE_BUFFERED);\r
+\r
+        Display d = getDisplay();\r
+        mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED);\r
+        mHoverFgColor = mSelectionFgColor;\r
+\r
+        mSelectionFont = d.getSystemFont();\r
 \r
         addPaintListener(new PaintListener() {\r
             public void paintControl(PaintEvent e) {\r
-                paint(e);\r
+                onPaint(e);\r
+            }\r
+        });\r
+\r
+        addMouseMoveListener(new MouseMoveListener() {\r
+            public void mouseMove(MouseEvent e) {\r
+                onMouseMove(e);\r
+            }\r
+        });\r
+\r
+        addMouseListener(new MouseListener() {\r
+            public void mouseUp(MouseEvent e) {\r
+                onMouseUp(e);\r
+            }\r
+\r
+            public void mouseDown(MouseEvent e) {\r
+                onMouseDown(e);\r
+            }\r
+\r
+            public void mouseDoubleClick(MouseEvent e) {\r
+                onDoubleClick(e);\r
             }\r
         });\r
     }\r
 \r
+    /**\r
+     * Sets the result of the layout rendering. The result object indicates if the layout\r
+     * rendering succeeded. If it did, it contains a bitmap and the objects rectangles.\r
+     *\r
+     * Implementation detail: the bridge's computeLayout() method already returns a newly\r
+     * allocated ILayourResult. That means we can keep this result and hold on to it\r
+     * when it is valid.\r
+     *\r
+     * @param result The new rendering result, either valid or not.\r
+     */\r
     public void setResult(ILayoutResult result) {\r
-        if (result.getSuccess() == ILayoutResult.SUCCESS) {\r
+\r
+        // disable any hover\r
+        mHoverRect = null;\r
+\r
+        mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS);\r
+\r
+        if (mIsResultValid && result != null) {\r
+            mLastValidResult = result;\r
             setImage(result.getImage());\r
+\r
+            // Check if the selection is still the same (based on its key)\r
+            // and eventually recompute its bounds.\r
+            if (mSelectionViewInfo != null) {\r
+                ILayoutViewInfo vi = findViewInfoKey(\r
+                        mSelectionViewInfo.getViewKey(),\r
+                        result.getRootView());\r
+                setSelection(vi);\r
+            }\r
         }\r
+\r
+        redraw();\r
     }\r
 \r
+    //---\r
+\r
+    /**\r
+     * Sets the image of the last *successful* rendering.\r
+     * Converts the AWT image into an SWT image.\r
+     */\r
     private void setImage(BufferedImage awtImage) {\r
-        // Convert the AWT image into an SWT image.\r
         int width = awtImage.getWidth();\r
         int height = awtImage.getHeight();\r
 \r
@@ -81,15 +182,207 @@ public class LayoutCanvas extends Canvas {
         imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);\r
 \r
         mImage = new Image(getDisplay(), imageData);\r
-\r
-        redraw();\r
     }\r
 \r
-    private void paint(PaintEvent e) {\r
+    private void onPaint(PaintEvent e) {\r
+        GC gc = e.gc;\r
+\r
         if (mImage != null) {\r
-            GC gc = e.gc;\r
-            gc.drawImage(mImage, 0, 0);\r
+            if (!mIsResultValid) {\r
+                gc.setAlpha(128);\r
+            }\r
+\r
+            gc.drawImage(mImage, IMAGE_MARGIN, IMAGE_MARGIN);\r
+\r
+            if (!mIsResultValid) {\r
+                gc.setAlpha(255);\r
+            }\r
+        }\r
+\r
+        if (mHoverRect != null) {\r
+            gc.setForeground(mHoverFgColor);\r
+            gc.setLineStyle(SWT.LINE_DOT);\r
+            gc.drawRectangle(mHoverRect);\r
+        }\r
+\r
+        // initialize the selection font height once. We need the GC to do that.\r
+        if (mSelectionFontHeight == 0) {\r
+            gc.setFont(mSelectionFont);\r
+            FontMetrics fm = gc.getFontMetrics();\r
+            mSelectionFontHeight = fm.getHeight();\r
+        }\r
+\r
+        if (mSelectionRect != null) {\r
+            gc.setForeground(mSelectionFgColor);\r
+            gc.setLineStyle(SWT.LINE_SOLID);\r
+            gc.drawRectangle(mSelectionRect);\r
+\r
+            if (mSelectionName != null) {\r
+                int x = mSelectionRect.x + 2;\r
+                int y = mSelectionRect.y - mSelectionFontHeight;\r
+                if (y < 0) {\r
+                    y = mSelectionRect.y + mSelectionRect.height;\r
+                }\r
+                gc.drawString(mSelectionName, x, y, true /*transparent*/);\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Hover on top of a known child.\r
+     */\r
+    private void onMouseMove(MouseEvent e) {\r
+        if (mLastValidResult != null) {\r
+            ILayoutViewInfo root = mLastValidResult.getRootView();\r
+            ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root);\r
+\r
+            // We don't hover on the root since it's not a widget per see and it is always there.\r
+            if (vi == root) {\r
+                vi = null;\r
+            }\r
+\r
+            boolean needsUpdate = vi != mHoverViewInfo;\r
+            mHoverViewInfo = vi;\r
+\r
+            mHoverRect = vi == null ? null : getViewInfoRect(vi);\r
+            if (mHoverRect != null) {\r
+                mHoverRect.x += IMAGE_MARGIN;\r
+                mHoverRect.y += IMAGE_MARGIN;\r
+            }\r
+\r
+            if (needsUpdate) {\r
+                redraw();\r
+            }\r
+        }\r
+    }\r
+\r
+    private void onMouseDown(MouseEvent e) {\r
+        // pass, not used yet.\r
+    }\r
+\r
+    /**\r
+     * Performs selection on mouse up (not mouse down).\r
+     */\r
+    private void onMouseUp(MouseEvent e) {\r
+        if (mLastValidResult != null) {\r
+            ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN,\r
+                    mLastValidResult.getRootView());\r
+            setSelection(vi);\r
+        }\r
+    }\r
+\r
+    private void onDoubleClick(MouseEvent e) {\r
+        // pass, not used yet.\r
+    }\r
+\r
+    private void setSelection(ILayoutViewInfo viewInfo) {\r
+        boolean needsUpdate = viewInfo != mSelectionViewInfo;\r
+        mSelectionViewInfo = viewInfo;\r
+\r
+        mSelectionRect = viewInfo == null ? null : getViewInfoRect(viewInfo);\r
+        if (mSelectionRect != null) {\r
+            mSelectionRect.x += IMAGE_MARGIN;\r
+            mSelectionRect.y += IMAGE_MARGIN;\r
+        }\r
+\r
+        String name = viewInfo == null ? null : viewInfo.getName();\r
+        if (name != null) {\r
+            // The name is typically a fully-qualified class name. Let's make it a tad shorter.\r
+\r
+            if (name.startsWith("android.")) {                                      // $NON-NLS-1$\r
+                // For android classes, convert android.foo.Name to android...Name\r
+                int first = name.indexOf('.');\r
+                int last = name.lastIndexOf('.');\r
+                if (last > first) {\r
+                    name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$\r
+                }\r
+            } else {\r
+                // For custom non-android classes, it's best to keep the 2 first segments of\r
+                // the namespace, e.g. we want to get something like com.example...MyClass\r
+                int first = name.indexOf('.');\r
+                first = name.indexOf('.', first + 1);\r
+                int last = name.lastIndexOf('.');\r
+                if (last > first) {\r
+                    name = name.substring(0, first) + ".." + name.substring(last);   // $NON-NLS-1$\r
+                }\r
+            }\r
+        }\r
+        mSelectionName = name;\r
+\r
+        if (needsUpdate) {\r
+            redraw();\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Tries to find a child with the same view key in the view info sub-tree.\r
+     * Returns null if not found.\r
+     */\r
+    private ILayoutViewInfo findViewInfoKey(Object viewKey, ILayoutViewInfo viewInfo) {\r
+        if (viewInfo.getViewKey() == viewKey) {\r
+            return viewInfo;\r
+        }\r
+\r
+        // try to find a matching child\r
+        if (viewInfo.getChildren() != null) {\r
+            for (ILayoutViewInfo child : viewInfo.getChildren()) {\r
+                ILayoutViewInfo v = findViewInfoKey(viewKey, child);\r
+                if (v != null) {\r
+                    return v;\r
+                }\r
+            }\r
+        }\r
+\r
+        return null;\r
+    }\r
+\r
+    /**\r
+     * Tries to find the inner most child matching the given x,y coordinates in the view\r
+     * info sub-tree. This uses the potentially-expanded selection bounds.\r
+     *\r
+     * Returns null if not found.\r
+     */\r
+    private ILayoutViewInfo findViewInfoAt(int x, int y, ILayoutViewInfo viewInfo) {\r
+        Rectangle r = getViewInfoRect(viewInfo);\r
+        if (r.contains(x, y)) {\r
+\r
+            // try to find a matching child first\r
+            if (viewInfo.getChildren() != null) {\r
+                for (ILayoutViewInfo child : viewInfo.getChildren()) {\r
+                    ILayoutViewInfo v = findViewInfoAt(x, y, child);\r
+                    if (v != null) {\r
+                        return v;\r
+                    }\r
+                }\r
+            }\r
+\r
+            // if no children matched, this is the view that we're looking for\r
+            return viewInfo;\r
         }\r
+\r
+        return null;\r
     }\r
 \r
+    /**\r
+     * Returns the bounds of the view info as a rectangle.\r
+     * In case the view has a null width or null height, it is expanded using\r
+     * {@link #SELECTION_MARGIN}.\r
+     */\r
+    private Rectangle getViewInfoRect(ILayoutViewInfo viewInfo) {\r
+        int x = viewInfo.getLeft();\r
+        int y = viewInfo.getTop();\r
+        int w = viewInfo.getRight() - x;\r
+        int h = viewInfo.getBottom() - y;\r
+\r
+        if (w == 0) {\r
+            x -= SELECTION_MARGIN;\r
+            w += 2 * SELECTION_MARGIN;\r
+        }\r
+        if (h == 0) {\r
+            y -= SELECTION_MARGIN;\r
+            h += 2* SELECTION_MARGIN;\r
+        }\r
+\r
+        return new Rectangle(x, y, w, h);\r
+    }\r
 }\r