diff --git a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java index 69846eb68..a0de610a2 100755 --- a/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java +++ b/tools/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutCanvas.java @@ -40,6 +40,10 @@ import org.eclipse.swt.widgets.Display; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.awt.image.Raster; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; /** * Displays the image rendered by the {@link GraphicalEditorPart} and handles @@ -51,46 +55,59 @@ import java.awt.image.Raster; * TODO list: * - gray on error, keep select but disable d'n'd. * - make sure it is scrollable (Canvas derives from Scrollable, so prolly just setting bounds.) - * - handle selection (will need the model, aka the root node). * - handle drop target (from palette). * - handle drag'n'drop (internal, for moving/duplicating). * - handle context menu (depending on selection). * - selection synchronization with the outline (both ways). - * - preserve selection during editor input change if applicable (e.g. when changing configuration.) */ public class LayoutCanvas extends Canvas { + /** + * Margin around the rendered image. Should be enough space to display the layout + * width and height pseudo widgets. + */ private static final int IMAGE_MARGIN = 5; - private static final int SELECTION_MARGIN = 2; + /** + * Minimal size of the selection, in case an empty view or layout is selected. + */ + private static final int SELECTION_MIN_SIZE = 6; private ILayoutResult mLastValidResult; + private ViewInfo mLastValidViewInfoRoot; /** Current background image. Null when there's no image. */ private Image mImage; - /** Current selected view info. Null when none is selected. */ - private ILayoutViewInfo mSelectionViewInfo; - /** Current selection border rectangle. Null when there's no selection. */ - private Rectangle mSelectionRect; - /** The name displayed over the selection, typically the widget class name. */ - private String mSelectionName; + private final LinkedList mSelections = new LinkedList(); + /** Selection border color. Do not dispose, it's a system color. */ private Color mSelectionFgColor; + /** Selection name font. Do not dispose, it's a system font. */ private Font mSelectionFont; + /** Pixel height of the font displaying the selection name. Initially set to 0 and only * initialized in onPaint() when we have a GC. */ private int mSelectionFontHeight; /** Current hover view info. Null when no mouse hover. */ - private ILayoutViewInfo mHoverViewInfo; + private ViewInfo mHoverViewInfo; + /** Current mouse hover border rectangle. Null when there's no mouse hover. */ private Rectangle mHoverRect; - /** Hover border color. Do not dispose, it's a system color. */ + + /** Hover border color. Must be disposed, it's NOT a system color. */ private Color mHoverFgColor; - private boolean mIsResultValid; + private AlternateSelection mAltSelection; + /** + * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult} + * in which case it is also available in {@link #mLastValidResult}. + * When false this means the canvas is displaying an out-dated result image & bounds and some + * features should be disabled accordingly such a drag'n'drop. + */ + private boolean mIsResultValid; public LayoutCanvas(Composite parent, int style) { @@ -98,9 +115,8 @@ public class LayoutCanvas extends Canvas { Display d = getDisplay(); mSelectionFgColor = d.getSystemColor(SWT.COLOR_RED); - mHoverFgColor = mSelectionFgColor; - - mSelectionFont = d.getSystemFont(); + mHoverFgColor = new Color(d, 0xFF, 0x99, 0x00); // orange + mSelectionFont = d.getSystemFont(); addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { @@ -129,6 +145,11 @@ public class LayoutCanvas extends Canvas { }); } + @Override + public void dispose() { + super.dispose(); + } + /** * Sets the result of the layout rendering. The result object indicates if the layout * rendering succeeded. If it did, it contains a bitmap and the objects rectangles. @@ -148,16 +169,29 @@ public class LayoutCanvas extends Canvas { if (mIsResultValid && result != null) { mLastValidResult = result; + mLastValidViewInfoRoot = new ViewInfo(result.getRootView()); setImage(result.getImage()); - // Check if the selection is still the same (based on its key) - // and eventually recompute its bounds. - if (mSelectionViewInfo != null) { - ILayoutViewInfo vi = findViewInfoKey( - mSelectionViewInfo.getViewKey(), - result.getRootView()); - setSelection(vi); + // Check if the selection is still the same (based on the object keys) + // and eventually recompute their bounds. + for (ListIterator it = mSelections.listIterator(); it.hasNext(); ) { + Selection s = it.next(); + + // Check the if the selected object still exists + Object key = s.getViewInfo().getKey(); + ViewInfo vi = findViewInfoKey(key, mLastValidViewInfoRoot); + + // Remove the previous selection -- if the selected object still exists + // we need to recompute its bounds in case it moved so we'll insert a new one + // at the same place. + it.remove(); + if (vi != null) { + it.add(new Selection(vi)); + } } + + // remove the current alternate selection views + mAltSelection = null; } redraw(); @@ -184,6 +218,9 @@ public class LayoutCanvas extends Canvas { mImage = new Image(getDisplay(), imageData); } + /** + * Paints the canvas in response to paint events. + */ private void onPaint(PaintEvent e) { GC gc = e.gc; @@ -212,19 +249,27 @@ public class LayoutCanvas extends Canvas { mSelectionFontHeight = fm.getHeight(); } - if (mSelectionRect != null) { - gc.setForeground(mSelectionFgColor); - gc.setLineStyle(SWT.LINE_SOLID); - gc.drawRectangle(mSelectionRect); + for (Selection s : mSelections) { + drawSelection(gc, s); + } + } - if (mSelectionName != null) { - int x = mSelectionRect.x + 2; - int y = mSelectionRect.y - mSelectionFontHeight; - if (y < 0) { - y = mSelectionRect.y + mSelectionRect.height; - } - gc.drawString(mSelectionName, x, y, true /*transparent*/); + private void drawSelection(GC gc, Selection s) { + Rectangle r = s.getRect(); + + gc.setForeground(mSelectionFgColor); + gc.setLineStyle(SWT.LINE_SOLID); + gc.drawRectangle(s.mRect); + + String name = s.getName(); + + if (name != null) { + int xs = r.x + 2; + int ys = r.y - mSelectionFontHeight; + if (ys < 0) { + ys = r.y + r.height; } + gc.drawString(name, xs, ys, true /*transparent*/); } } @@ -233,8 +278,8 @@ public class LayoutCanvas extends Canvas { */ private void onMouseMove(MouseEvent e) { if (mLastValidResult != null) { - ILayoutViewInfo root = mLastValidResult.getRootView(); - ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root); + ViewInfo root = mLastValidViewInfoRoot; + ViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, root); // We don't hover on the root since it's not a widget per see and it is always there. if (vi == root) { @@ -244,10 +289,12 @@ public class LayoutCanvas extends Canvas { boolean needsUpdate = vi != mHoverViewInfo; mHoverViewInfo = vi; - mHoverRect = vi == null ? null : getViewInfoRect(vi); - if (mHoverRect != null) { - mHoverRect.x += IMAGE_MARGIN; - mHoverRect.y += IMAGE_MARGIN; + if (vi == null) { + mHoverRect = null; + } else { + Rectangle r = vi.getSelectionRect(); + mHoverRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, + r.width, r.height); } if (needsUpdate) { @@ -262,12 +309,125 @@ public class LayoutCanvas extends Canvas { /** * Performs selection on mouse up (not mouse down). + *

+ * Shift key is used to toggle in multi-selection. + * Alt key is used to cycle selection through objects at the same level than the one + * pointed at (i.e. click on an object then alt-click to cycle). */ private void onMouseUp(MouseEvent e) { if (mLastValidResult != null) { - ILayoutViewInfo vi = findViewInfoAt(e.x - IMAGE_MARGIN, e.y - IMAGE_MARGIN, - mLastValidResult.getRootView()); - setSelection(vi); + + boolean isShift = (e.stateMask & SWT.SHIFT) != 0; + boolean isAlt = (e.stateMask & SWT.ALT) != 0; + + int x = e.x - IMAGE_MARGIN; + int y = e.y - IMAGE_MARGIN; + ViewInfo vi = findViewInfoAt(x, y, mLastValidViewInfoRoot); + + if (isShift && !isAlt) { + // Case where shift is pressed: pointed object is toggled. + + // reset alternate selection if any + mAltSelection = null; + + // If nothing has been found at the cursor, assume it might be a user error + // and avoid clearing the existing selection. + + if (vi != null) { + // toggle this selection on-off: remove it if already selected + if (deselect(vi)) { + redraw(); + return; + } + + // otherwise add it. + mSelections.add(new Selection(vi)); + redraw(); + } + + } else if (isAlt) { + // Case where alt is pressed: select or cycle the object pointed at. + + // Note: if shift and alt are pressed, shift is ignored. The alternate selection + // mechanism does not reset the current multiple selection unless they intersect. + + // We need to remember the "origin" of the alternate selection, to be + // able to continue cycling through it later. If there's no alternate selection, + // create one. If there's one but not for the same origin object, create a new + // one too. + if (mAltSelection == null || mAltSelection.getOriginatingView() != vi) { + mAltSelection = new AlternateSelection(vi, findAltViewInfoAt( + x, y, mLastValidViewInfoRoot, null)); + + // deselect them all, in case they were partially selected + deselectAll(mAltSelection.getAltViews()); + + // select the current one + ViewInfo vi2 = mAltSelection.getCurrent(); + if (vi2 != null) { + mSelections.addFirst(new Selection(vi2)); + } + } else { + // We're trying to cycle through the current alternate selection. + // First remove the current object. + ViewInfo vi2 = mAltSelection.getCurrent(); + deselect(vi2); + + // Now select the next one. + vi2 = mAltSelection.getNext(); + if (vi2 != null) { + mSelections.addFirst(new Selection(vi2)); + } + } + redraw(); + + } else { + // Case where no modifier is pressed: either select or reset the selection. + + // reset alternate selection if any + mAltSelection = null; + + // reset (multi)selection if any + if (mSelections.size() > 0) { + if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) { + // Selection remains the same, don't touch it. + return; + } + mSelections.clear(); + } + + if (vi != null) { + mSelections.add(new Selection(vi)); + } + redraw(); + } + } + } + + /** Deselects a view info. Returns true if the object was actually selected. */ + private boolean deselect(ViewInfo viewInfo) { + if (viewInfo == null) { + return false; + } + + for (ListIterator it = mSelections.listIterator(); it.hasNext(); ) { + Selection s = it.next(); + if (viewInfo == s.getViewInfo()) { + it.remove(); + return true; + } + } + + return false; + } + + /** Deselects multiple view infos, */ + private void deselectAll(List viewInfos) { + for (ListIterator it = mSelections.listIterator(); it.hasNext(); ) { + Selection s = it.next(); + if (viewInfos.contains(s.getViewInfo())) { + it.remove(); + } } } @@ -275,61 +435,20 @@ public class LayoutCanvas extends Canvas { // pass, not used yet. } - private void setSelection(ILayoutViewInfo viewInfo) { - boolean needsUpdate = viewInfo != mSelectionViewInfo; - mSelectionViewInfo = viewInfo; - - mSelectionRect = viewInfo == null ? null : getViewInfoRect(viewInfo); - if (mSelectionRect != null) { - mSelectionRect.x += IMAGE_MARGIN; - mSelectionRect.y += IMAGE_MARGIN; - } - - String name = viewInfo == null ? null : viewInfo.getName(); - if (name != null) { - // The name is typically a fully-qualified class name. Let's make it a tad shorter. - - if (name.startsWith("android.")) { // $NON-NLS-1$ - // For android classes, convert android.foo.Name to android...Name - int first = name.indexOf('.'); - int last = name.lastIndexOf('.'); - if (last > first) { - name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ - } - } else { - // For custom non-android classes, it's best to keep the 2 first segments of - // the namespace, e.g. we want to get something like com.example...MyClass - int first = name.indexOf('.'); - first = name.indexOf('.', first + 1); - int last = name.lastIndexOf('.'); - if (last > first) { - name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ - } - } - } - mSelectionName = name; - - if (needsUpdate) { - redraw(); - } - } - /** * Tries to find a child with the same view key in the view info sub-tree. * Returns null if not found. */ - private ILayoutViewInfo findViewInfoKey(Object viewKey, ILayoutViewInfo viewInfo) { - if (viewInfo.getViewKey() == viewKey) { + private ViewInfo findViewInfoKey(Object viewKey, ViewInfo viewInfo) { + if (viewInfo.getKey() == viewKey) { return viewInfo; } // try to find a matching child - if (viewInfo.getChildren() != null) { - for (ILayoutViewInfo child : viewInfo.getChildren()) { - ILayoutViewInfo v = findViewInfoKey(viewKey, child); - if (v != null) { - return v; - } + for (ViewInfo child : viewInfo.getChildren()) { + ViewInfo v = findViewInfoKey(viewKey, child); + if (v != null) { + return v; } } @@ -342,17 +461,15 @@ public class LayoutCanvas extends Canvas { * * Returns null if not found. */ - private ILayoutViewInfo findViewInfoAt(int x, int y, ILayoutViewInfo viewInfo) { - Rectangle r = getViewInfoRect(viewInfo); + private ViewInfo findViewInfoAt(int x, int y, ViewInfo viewInfo) { + Rectangle r = viewInfo.getSelectionRect(); if (r.contains(x, y)) { // try to find a matching child first - if (viewInfo.getChildren() != null) { - for (ILayoutViewInfo child : viewInfo.getChildren()) { - ILayoutViewInfo v = findViewInfoAt(x, y, child); - if (v != null) { - return v; - } + for (ViewInfo child : viewInfo.getChildren()) { + ViewInfo v = findViewInfoAt(x, y, child); + if (v != null) { + return v; } } @@ -363,26 +480,295 @@ public class LayoutCanvas extends Canvas { return null; } - /** - * Returns the bounds of the view info as a rectangle. - * In case the view has a null width or null height, it is expanded using - * {@link #SELECTION_MARGIN}. - */ - private Rectangle getViewInfoRect(ILayoutViewInfo viewInfo) { - int x = viewInfo.getLeft(); - int y = viewInfo.getTop(); - int w = viewInfo.getRight() - x; - int h = viewInfo.getBottom() - y; + private ArrayList findAltViewInfoAt( + int x, int y, ViewInfo parent, ArrayList outList) { + Rectangle r; - if (w == 0) { - x -= SELECTION_MARGIN; - w += 2 * SELECTION_MARGIN; - } - if (h == 0) { - y -= SELECTION_MARGIN; - h += 2* SELECTION_MARGIN; + if (outList == null) { + outList = new ArrayList(); + + // add the parent root only once + r = parent.getSelectionRect(); + if (r.contains(x, y)) { + outList.add(parent); + } } - return new Rectangle(x, y, w, h); + if (parent.getChildren().size() > 0) { + // then add all children that match the position + for (ViewInfo child : parent.getChildren()) { + r = child.getSelectionRect(); + if (r.contains(x, y)) { + outList.add(child); + } + } + + // finally recurse in the children + for (ViewInfo child : parent.getChildren()) { + r = child.getSelectionRect(); + if (r.contains(x, y)) { + findAltViewInfoAt(x, y, child, outList); + } + } + } + + return outList; } + + /** + * Maps a {@link ILayoutViewInfo} in a structure more adapted to our needs. + * The only large difference is that we keep both the original bounds of the view info + * and we pre-compute the selection bounds which are absolute to the rendered image (where + * as the original bounds are relative to the parent view.) + *

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

+ * We can't alter {@link ILayoutViewInfo} as it is part of the LayoutBridge and needs to + * have a fixed API. + */ + private static class ViewInfo { + private final Rectangle mRealRect; + private final Rectangle mSelectionRect; + private final String mName; + private final Object mKey; + private final ViewInfo mParent; + private final ArrayList mChildren = new ArrayList(); + + /** + * Constructs a {@link ViewInfo} hierarchy based on a given {@link ILayoutViewInfo} + * hierarchy. This call is recursives and builds a full tree. + * + * @param viewInfo The root of the {@link ILayoutViewInfo} hierarchy. + */ + public ViewInfo(ILayoutViewInfo viewInfo) { + this(viewInfo, null /*parent*/, 0 /*parentX*/, 0 /*parentY*/); + } + + private ViewInfo(ILayoutViewInfo viewInfo, ViewInfo parent, int parentX, int parentY) { + mParent = parent; + mKey = viewInfo.getViewKey(); + mName = viewInfo.getName(); + + int x = viewInfo.getLeft(); + int y = viewInfo.getTop(); + int w = viewInfo.getRight() - x; + int h = viewInfo.getBottom() - y; + + mRealRect = new Rectangle(x, y, w, h); + + if (parent != null) { + x += parentX; + y += parentY; + } + + if (viewInfo.getChildren() != null) { + for (ILayoutViewInfo child : viewInfo.getChildren()) { + mChildren.add(new ViewInfo(child, this, x, y)); + } + } + + // adjust selection bounds for views which are too small to select + + if (w < SELECTION_MIN_SIZE) { + int d = (SELECTION_MIN_SIZE - w) / 2; + x -= d; + w += SELECTION_MIN_SIZE - w; + } + + if (h < SELECTION_MIN_SIZE) { + int d = (SELECTION_MIN_SIZE - h) / 2; + y -= d; + h += SELECTION_MIN_SIZE - h; + } + + mSelectionRect = new Rectangle(x, y, w - 1, h - 1); + } + + /** Returns the original {@link ILayoutResult} bounds, relative to the parent. */ + public Rectangle getRealRect() { + return mRealRect; + } + + /* + * Returns the absolute selection bounds of the view info as a rectangle. + * The selection bounds will always have a size greater or equal to + * {@link #SELECTION_MIN_SIZE}. + * The width/height is inclusive (i.e. width = right-left-1). + * This is in absolute "screen" coordinates (relative to the rendered bitmap). + */ + public Rectangle getSelectionRect() { + return mSelectionRect; + } + + /** + * Returns the view key. Could be null, although unlikely. + * @see ILayoutViewInfo#getViewKey() + */ + public Object getKey() { + return mKey; + } + + /** + * Returns the parent {@link ViewInfo}. + * It is null for the root and non-null for children. + */ + public ViewInfo getParent() { + return mParent; + } + + /** + * Returns the list of children of this {@link ViewInfo}. + * The list is never null. It can be empty. + * By contract, this.getChildren().get(0..n-1).getParent() == this. + */ + public ArrayList getChildren() { + return mChildren; + } + + /** + * Returns the name of the {@link ViewInfo}. + * Could be null, although unlikely. + * Experience shows this is the full qualified Java name of the View. + * @see ILayoutViewInfo#getName() + */ + public String getName() { + return mName; + } + } + + /** + * Represents one selection. + */ + private static class Selection { + /** Current selected view info. Cannot be null. */ + private final ViewInfo mViewInfo; + + /** Current selection border rectangle. Cannot be null. */ + private final Rectangle mRect; + + /** The name displayed over the selection, typically the widget class name. Can be null. */ + private final String mName; + + /** + * Creates a new {@link Selection} object. + * @param viewInfo The view info being selected. Must not be null. + */ + public Selection(ViewInfo viewInfo) { + + assert viewInfo != null; + + mViewInfo = viewInfo; + + if (viewInfo == null) { + mRect = null; + } else { + Rectangle r = viewInfo.getSelectionRect(); + mRect = new Rectangle(r.x + IMAGE_MARGIN, r.y + IMAGE_MARGIN, r.width, r.height); + } + + String name = viewInfo == null ? null : viewInfo.getName(); + if (name != null) { + // The name is typically a fully-qualified class name. Let's make it a tad shorter. + + if (name.startsWith("android.")) { // $NON-NLS-1$ + // For android classes, convert android.foo.Name to android...Name + int first = name.indexOf('.'); + int last = name.lastIndexOf('.'); + if (last > first) { + name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ + } + } else { + // For custom non-android classes, it's best to keep the 2 first segments of + // the namespace, e.g. we want to get something like com.example...MyClass + int first = name.indexOf('.'); + first = name.indexOf('.', first + 1); + int last = name.lastIndexOf('.'); + if (last > first) { + name = name.substring(0, first) + ".." + name.substring(last); // $NON-NLS-1$ + } + } + } + + mName = name; + } + + /** + * Returns the selected view info. Cannot be null. + */ + public ViewInfo getViewInfo() { + return mViewInfo; + } + + /** + * Returns the selection border rectangle. + * Cannot be null. + */ + public Rectangle getRect() { + return mRect; + } + + /** + * The name displayed over the selection, typically the widget class name. + * Can be null. + */ + public String getName() { + return mName; + } + } + + /** + * Information for the current alternate selection, i.e. the possible selected items + * that are located at the same x/y as the original view, either sibling or parents. + */ + private static class AlternateSelection { + private final ViewInfo mOriginatingView; + private final List mAltViews; + private int mIndex; + + /** + * Creates a new alternate selection based on the given originating view and the + * given list of alternate views. Both cannot be null. + */ + public AlternateSelection(ViewInfo originatingView, List altViews) { + assert originatingView != null; + assert altViews != null; + mOriginatingView = originatingView; + mAltViews = altViews; + mIndex = altViews.size() - 1; + } + + /** Returns the list of alternate views. Cannot be null. */ + public List getAltViews() { + return mAltViews; + } + + /** Returns the originating view. Cannot be null. */ + public ViewInfo getOriginatingView() { + return mOriginatingView; + } + + /** + * Returns the current alternate view to select. + * Initially this is the top-most view. + */ + public ViewInfo getCurrent() { + return mIndex >= 0 ? mAltViews.get(mIndex) : null; + } + + /** + * Changes the current view to be the next one and then returns it. + * This loops through the alternate views. + */ + public ViewInfo getNext() { + if (mIndex == 0) { + mIndex = mAltViews.size() - 1; + } else if (mIndex > 0) { + mIndex--; + } + + return getCurrent(); + } + } + + }