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