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
* @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
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