diff options
13 files changed, 513 insertions, 26 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt index 9e0bc97..6da9527 100644 --- a/eclipse/dictionary.txt +++ b/eclipse/dictionary.txt @@ -28,8 +28,10 @@ codenames combo config configurability +congrats coords ddms +deactivated debuggable dedent deprecated @@ -93,6 +95,7 @@ recompilation redo regexp registry +remap reparse reparses residual diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java index 335e565..0938afa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/DrawingStyle.java @@ -91,11 +91,17 @@ public enum DrawingStyle { HELP, /** - * The style used to raw illegal/error/invalid markers + * The style used to draw illegal/error/invalid markers */ INVALID, /** + * The style used to draw empty containers of zero bounds (which are padded + * a bit to make them visible during a drag or selection). + */ + EMPTY, + + /** * A style used for unspecified purposes; can be used by a client to have * yet another color that is domain specific; using this color constant * rather than your own hardcoded value means that you will be guaranteed to diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java index 180c329..ed8a205 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java @@ -278,6 +278,13 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput, } /** + * Tells the graphical editor to recompute its layout. + */ + public void recomputeLayout() { + mGraphicalEditor.recomputeLayout(); + } + + /** * Returns the custom IContentOutlinePage or IPropertySheetPage when asked for it. */ @SuppressWarnings("unchecked") diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java index 5f93387..1e04c6b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParser.java @@ -37,6 +37,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,7 +50,7 @@ import java.util.regex.Pattern; */ public final class UiElementPullParser extends BasePullParser { private final static String ATTR_PADDING = "padding"; //$NON-NLS-1$ - private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$ + private final static Pattern FLOAT_PATTERN = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); //$NON-NLS-1$ private final int[] sIntOut = new int[1]; @@ -61,16 +62,52 @@ public final class UiElementPullParser extends BasePullParser { private List<ElementDescriptor> mLayoutDescriptors; private final int mDensityValue; private final float mXdpi; - private final String mDefaultPaddingValue; - public UiElementPullParser(UiElementNode top, boolean explodeRendering, int densityValue, - float xdpi, IProject project) { + /** + * Number of pixels to pad views with in exploded-rendering mode. + */ + private static final String DEFAULT_PADDING_VALUE = + ExplodedRenderingHelper.PADDING_VALUE + "px"; //$NON-NLS-1$ + + /** + * Number of pixels to pad exploded individual views with. (This is HALF the width of the + * rectangle since padding is repeated on both sides of the empty content.) + */ + private static final String FIXED_PADDING_VALUE = "20px"; //$NON-NLS-1$ + + /** + * Set of nodes that we want to auto-pad using {@link #FIXED_PADDING_VALUE} as the padding + * attribute value. Can be null, which is the case when we don't want to perform any + * <b>individual</b> node exploding. + */ + private final Set<UiElementNode> mExplodeNodes; + + /** + * Constructs a new {@link UiElementPullParser}, a parser dedicated to the special case of + * parsing a layout resource files, and handling "exploded rendering" - adding padding on views + * to make them easier to see and operate on. + * + * @param top The {@link UiElementNode} for the root node. + * @param explodeRendering When true, add padding to <b>all</b> nodes in the hierarchy. This + * will add rather than replace padding of a node. + * @param explodeNodes A set of individual nodes that should be assigned a fixed amount of + * padding ({@link #FIXED_PADDING_VALUE}). This is intended for use with nodes that + * (without padding) would be invisible. This parameter can be null, in which case + * nodes are not individually exploded (but they may all be exploded with the + * explodeRendering parameter. + * @param densityValue the density factor for the screen. + * @param xdpi the screen actual dpi in X + * @param project Project containing this layout. + */ + public UiElementPullParser(UiElementNode top, boolean explodeRendering, + Set<UiElementNode> explodeNodes, + int densityValue, float xdpi, IProject project) { super(); mRoot = top; mExplodedRendering = explodeRendering; + mExplodeNodes = explodeNodes; mDensityValue = densityValue; mXdpi = xdpi; - mDefaultPaddingValue = ExplodedRenderingHelper.PADDING_VALUE + "px"; //$NON-NLS-1$ if (mExplodedRendering) { // get the layout descriptor IAndroidTarget target = Sdk.getCurrent().getTarget(project); @@ -255,7 +292,7 @@ public final class UiElementPullParser extends BasePullParser { public String getAttributeValue(int i) { if (mZeroAttributeIsPadding) { if (i == 0) { - return mDefaultPaddingValue; + return DEFAULT_PADDING_VALUE; } else { i--; } @@ -279,9 +316,17 @@ public final class UiElementPullParser extends BasePullParser { * This is the main method used by the LayoutInflater to query for attributes. */ public String getAttributeValue(String namespace, String localName) { + if (mExplodeNodes != null && ATTR_PADDING.equals(localName) && + SdkConstants.NS_RESOURCES.equals(namespace)) { + UiElementNode node = getCurrentNode(); + if (node != null && mExplodeNodes.contains(node)) { + return FIXED_PADDING_VALUE; + } + } + if (mZeroAttributeIsPadding && ATTR_PADDING.equals(localName) && SdkConstants.NS_RESOURCES.equals(namespace)) { - return mDefaultPaddingValue; + return DEFAULT_PADDING_VALUE; } // get the current uiNode @@ -457,14 +502,14 @@ public final class UiElementPullParser extends BasePullParser { */ private boolean stringToPixel(String s) { // remove the space before and after - s.trim(); + s = s.trim(); int len = s.length(); if (len <= 0) { return false; } - // check that there's no non ascii characters. + // check that there's no non ASCII characters. char[] buf = s.toCharArray(); for (int i = 0 ; i < len ; i++) { if (buf[i] > 255) { @@ -478,7 +523,7 @@ public final class UiElementPullParser extends BasePullParser { } // now look for the string that is after the float... - Matcher m = sFloatPattern.matcher(s); + Matcher m = FLOAT_PATTERN.matcher(s); if (m.matches()) { String f_str = m.group(1); String end = m.group(2); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java index 9714b6c..7485b6d 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasViewInfo.java @@ -18,6 +18,7 @@ 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.UiElementPullParser; 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; @@ -64,6 +65,13 @@ public class CanvasViewInfo implements IPropertySource { private final ArrayList<CanvasViewInfo> mChildren = new ArrayList<CanvasViewInfo>(); /** + * Is this view info an individually exploded view? This is the case for views + * that were specially inflated by the {@link UiElementPullParser} and assigned + * fixed padding because they were invisible and somebody requested visibility. + */ + private boolean mExploded; + + /** * Constructs a {@link CanvasViewInfo} hierarchy based on a given {@link ILayoutViewInfo} * hierarchy. This call is recursive and builds a full tree. * @@ -279,6 +287,45 @@ public class CanvasViewInfo implements IPropertySource { } /** + * Returns true if this {@link CanvasViewInfo} represents an invisible parent - in + * other words, a view that can have children, and that has zero bounds making it + * effectively invisible. (We don't actually look for -0- bounds, but + * bounds smaller than SELECTION_MIN_SIZE.) + * + * @return True if this is an invisible parent. + */ + public boolean isInvisibleParent() { + if (mAbsRect.width < SELECTION_MIN_SIZE || mAbsRect.height < SELECTION_MIN_SIZE) { + return mUiViewKey != null && mUiViewKey.getDescriptor().hasChildren(); + } + + return false; + } + + /** + * Is this {@link CanvasViewInfo} a view that has had its padding inflated in order to + * make it visible during selection or dragging? Note that this is NOT considered to + * be the case in the explode-all-views mode where all nodes have their padding + * increased; it's only used for views that individually exploded because they were + * requested visible and they returned true for {@link #isInvisibleParent()}. + * + * @return True if this is an exploded node. + */ + public boolean isExploded() { + return mExploded; + } + + /** + * Mark this {@link CanvasViewInfo} as having been exploded or not. See the + * {@link #isExploded()} method for details on what this property means. + * + * @param exploded New value of the exploded property to mark this info with. + */ + /* package */ void setExploded(boolean exploded) { + this.mExploded = exploded; + } + + /** * Returns the info represented as a {@link SimpleElement}. * * @return A {@link SimpleElement} wrapping this info. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java new file mode 100644 index 0000000..946c381 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/EmptyViewsOverlay.java @@ -0,0 +1,93 @@ +/* + * 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 EmptyViewsOverlay} paints bounding rectangles for any of the empty and + * invisible container views in the scene. + */ +public class EmptyViewsOverlay extends Overlay { + /** The {@link ViewHierarchy} containing visible view information. */ + private final ViewHierarchy mViewHierarchy; + + /** Border color to paint the bounding boxes with. */ + private Color mBorderColor; + + /** Vertical scaling & scrollbar information. */ + private ScaleInfo mVScale; + + /** Horizontal scaling & scrollbar information. */ + private ScaleInfo mHScale; + + /** + * Constructs a new {@link EmptyViewsOverlay} 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 EmptyViewsOverlay(ViewHierarchy viewHierarchy, ScaleInfo hScale, ScaleInfo vScale) { + super(); + this.mViewHierarchy = viewHierarchy; + this.mHScale = hScale; + this.mVScale = vScale; + } + + @Override + public void create(Device device) { + mBorderColor = new Color(device, SwtDrawingStyle.EMPTY.getStrokeColor()); + } + + @Override + public void dispose() { + if (mBorderColor != null) { + mBorderColor.dispose(); + mBorderColor = null; + } + } + + @Override + public void paint(GC gc) { + gc.setForeground(mBorderColor); + gc.setLineDash(null); + gc.setLineStyle(SwtDrawingStyle.EMPTY.getLineStyle()); + int oldAlpha = gc.getAlpha(); + gc.setAlpha(SwtDrawingStyle.EMPTY.getStrokeAlpha()); + gc.setLineWidth(SwtDrawingStyle.EMPTY.getLineWidth()); + + for (CanvasViewInfo info : mViewHierarchy.getInvisibleViews()) { + 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); + + // +1: See explanation in equivalent code in {@link OutlineOverlay#paint} + gc.drawRectangle(x, y, w + 1, h + 1); + } + + gc.setAlpha(oldAlpha); + } +} 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 index 97a21ee..9da3599 100644 --- 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 @@ -370,6 +370,8 @@ public class GestureManager { * The cursor has entered the drop target boundaries. {@inheritDoc} */ public void dragEnter(DropTargetEvent event) { + mCanvas.showInvisibleViews(true); + if (mCurrentGesture == null) { Gesture newGesture = mZombieGesture; if (newGesture == null) { @@ -406,6 +408,8 @@ public class GestureManager { finishGesture(ControlPoint.create(mCanvas, event), true); mZombieGesture = dropGesture; } + + mCanvas.showInvisibleViews(false); } /** @@ -477,7 +481,6 @@ public class GestureManager { // 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(); @@ -559,6 +562,7 @@ public class GestureManager { e.doit = false; } else { // Otherwise, the drag means you are moving something + mCanvas.showInvisibleViews(true); startGesture(ControlPoint.create(mCanvas, e), new MoveGesture(mCanvas), 0); } @@ -601,6 +605,7 @@ public class GestureManager { GlobalCanvasDragInfo.getInstance().stopDrag(); finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE); + mCanvas.showInvisibleViews(false); mCanvas.redraw(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java index a0fbac6..b0d5ed9 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java @@ -1084,7 +1084,7 @@ public class GraphicalEditorPart extends EditorPart } }; - mCanvasViewer.getCanvas().setResult(result); + mCanvasViewer.getCanvas().setResult(result, null /*explodeNodes*/); return; } @@ -1209,8 +1209,11 @@ public class GraphicalEditorPart extends EditorPart float ydpi = mConfigComposite.getYDpi(); boolean isProjectTheme = mConfigComposite.isProjectTheme(); + LayoutCanvas canvas = getCanvasControl(); + Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); + UiElementPullParser parser = new UiElementPullParser(getModel(), - mUseExplodeMode, density, xdpi, iProject); + mUseExplodeMode, explodeNodes, density, xdpi, iProject); ILayoutResult result = computeLayout(bridge, parser, iProject /* projectKey */, @@ -1223,7 +1226,7 @@ public class GraphicalEditorPart extends EditorPart // post rendering clean up bridge.cleanUp(); - mCanvasViewer.getCanvas().setResult(result); + canvas.setResult(result, explodeNodes); // update the UiElementNode with the layout info. if (result.getSuccess() != ILayoutResult.SUCCESS) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java index c97013b..1c0c798 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java @@ -71,6 +71,10 @@ import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; import org.eclipse.ui.views.contentoutline.IContentOutlinePage; import org.w3c.dom.Node; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * Displays the image rendered by the {@link GraphicalEditorPart} and handles * the interaction with the widgets. @@ -110,6 +114,9 @@ class LayoutCanvas extends Canvas { /** When true, always display the outline of all views. */ private boolean mShowOutline; + /** When true, display the outline of all empty parent views. */ + private boolean mShowInvisible; + /** Drop target associated with this composite. */ private DropTarget mDropTarget; @@ -161,6 +168,9 @@ class LayoutCanvas extends Canvas { /** The overlay which paints the optional outline. */ private OutlineOverlay mOutlineOverlay; + /** The overlay which paints outlines around empty children */ + private EmptyViewsOverlay mEmptyOverlay; + /** The overlay which paints the mouse hover. */ private HoverOverlay mHoverOverlay; @@ -199,7 +209,7 @@ class LayoutCanvas extends Canvas { mFont = display.getSystemFont(); // --- Set up graphic overlays - // mOutlineOverlay is initialized lazily + // mOutlineOverlay and mEmptyOverlay are initialized lazily mHoverOverlay = new HoverOverlay(mHScale, mVScale); mHoverOverlay.create(display); mSelectionOverlay = new SelectionOverlay(); @@ -300,6 +310,11 @@ class LayoutCanvas extends Canvas { mOutlineOverlay = null; } + if (mEmptyOverlay != null) { + mEmptyOverlay.dispose(); + mEmptyOverlay = null; + } + if (mHoverOverlay != null) { mHoverOverlay.dispose(); mHoverOverlay = null; @@ -427,12 +442,17 @@ class LayoutCanvas extends Canvas { * when it is valid. * * @param result The new rendering result, either valid or not. + * @param explodedNodes The set of individual nodes the layout computer was asked to + * explode. Note that these are independent of the explode-all mode where + * all views are exploded; this is used only for the mode ( + * {@link #showInvisibleViews(boolean)}) where individual invisible nodes + * are padded during certain interactions. */ - /* package */ void setResult(ILayoutResult result) { + /* package */ void setResult(ILayoutResult result, Set<UiElementNode> explodedNodes) { // disable any hover clearHover(); - mViewHierarchy.setResult(result); + mViewHierarchy.setResult(result, explodedNodes); if (mViewHierarchy.isValid() && result != null) { Image image = mImageOverlay.setImage(result.getImage()); @@ -532,6 +552,14 @@ class LayoutCanvas extends Canvas { mOutlineOverlay.paint(gc); } + if (mShowInvisible) { + if (mEmptyOverlay == null) { + mEmptyOverlay = new EmptyViewsOverlay(mViewHierarchy, mHScale, mVScale); + mEmptyOverlay.create(getDisplay()); + } + mEmptyOverlay.paint(gc); + } + mHoverOverlay.paint(gc); mSelectionOverlay.paint(mSelectionManager, gc, mGCWrapper, mRulesEngine); mGestureManager.paint(gc); @@ -542,6 +570,72 @@ class LayoutCanvas extends Canvas { } /** + * Shows or hides invisible parent views, which are views which have empty bounds and + * no children. The nodes which will be shown are provided by + * {@link #getNodesToExplode()}. + * + * @param show When true, any invisible parent nodes are padded and highlighted + * ("exploded"), and when false any formerly exploded nodes are hidden. + */ + /* package */ void showInvisibleViews(boolean show) { + if (mShowInvisible == show) { + return; + } + + // Optimization: Avoid doing work when we don't have invisible parents (on show) + // or formerly exploded nodes (on hide). + if (show && !mViewHierarchy.hasInvisibleParents()) { + return; + } else if (!show && !mViewHierarchy.hasExplodedParents()) { + return; + } + + mShowInvisible = show; + mLayoutEditor.recomputeLayout(); + } + + /** + * Returns a set of nodes that should be exploded (forced non-zero padding during render), + * or null if no nodes should be exploded. (Note that this is independent of the + * explode-all mode, where all nodes are padded -- that facility does not use this + * mechanism, which is only intended to be used to expose invisible parent nodes. + * + * @return The set of invisible parents, or null if no views should be expanded. + */ + public Set<UiElementNode> getNodesToExplode() { + if (mShowInvisible) { + return mViewHierarchy.getInvisibleNodes(); + } + + // IF we have selection, and IF we have invisible nodes in the view, + // see if any of the selected items are among the invisible nodes, and if so + // add them to a lazily constructed set which we pass back for rendering. + Set<UiElementNode> result = null; + List<CanvasSelection> selections = mSelectionManager.getSelections(); + if (selections.size() > 0) { + List<CanvasViewInfo> invisibleParents = mViewHierarchy.getInvisibleViews(); + if (invisibleParents.size() > 0) { + for (CanvasSelection item : selections) { + CanvasViewInfo viewInfo = item.getViewInfo(); + // O(n^2) here, but both the selection size and especially the + // invisibleParents size are expected to be small + if (invisibleParents.contains(viewInfo)) { + UiViewElementNode node = viewInfo.getUiViewKey(); + if (node != null) { + if (result == null) { + result = new HashSet<UiElementNode>(); + } + result.add(node); + } + } + } + } + } + + return result; + } + + /** * Clears the hover. */ /* package */ void clearHover() { @@ -889,5 +983,4 @@ class LayoutCanvas extends Canvas { AdtPlugin.printToConsole("Canvas", String.format(message, params)); } } - } 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 index affd69a..3db65f1 100644 --- 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 @@ -180,6 +180,7 @@ public class SelectionManager implements ISelectionProvider { } boolean changed = false; + boolean redoLayout = false; // Create a list of all currently selected view infos Set<CanvasViewInfo> oldSelected = new HashSet<CanvasViewInfo>(); @@ -206,15 +207,24 @@ public class SelectionManager implements ISelectionProvider { mSelections.add(createSelection(newVi)); changed = true; } + if (newVi.isInvisibleParent()) { + redoLayout = true; + } } } // Deselect old selected items that are not in the new one for (CanvasViewInfo vi : oldSelected) { + if (vi.isExploded()) { + redoLayout = true; + } deselect(vi); changed = true; } + if (redoLayout) { + mCanvas.getLayoutEditor().recomputeLayout(); + } if (changed) { redraw(); updateMenuActions(); @@ -238,7 +248,6 @@ public class SelectionManager implements ISelectionProvider { * 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) && @@ -284,6 +293,10 @@ public class SelectionManager implements ISelectionProvider { if (vi != null) { // toggle this selection on-off: remove it if already selected if (deselect(vi)) { + if (vi.isExploded()) { + mCanvas.getLayoutEditor().recomputeLayout(); + } + redraw(); return; } @@ -334,7 +347,6 @@ public class SelectionManager implements ISelectionProvider { } else { // Case where no modifier is pressed: either select or reset the selection. - selectSingle(vi); } } @@ -349,6 +361,8 @@ public class SelectionManager implements ISelectionProvider { // reset alternate selection if any mAltSelection = null; + boolean redoLayout = hasExplodedItems(); + // reset (multi)selection if any if (!mSelections.isEmpty()) { if (mSelections.size() == 1 && mSelections.getFirst().getViewInfo() == vi) { @@ -360,11 +374,30 @@ public class SelectionManager implements ISelectionProvider { if (vi != null) { mSelections.add(createSelection(vi)); + if (vi.isInvisibleParent()) { + redoLayout = true; + } } fireSelectionChanged(); + + if (redoLayout) { + mCanvas.getLayoutEditor().recomputeLayout(); + } + redraw(); } + /** Returns true if the view hierarchy is showing exploded items. */ + private boolean hasExplodedItems() { + for (CanvasSelection item : mSelections) { + if (item.getViewInfo().isExploded()) { + return true; + } + } + + return false; + } + /** * Selects the given set of {@link CanvasViewInfo}s. This is similar to * {@link #selectSingle} but allows you to make a multi-selection. Issues a @@ -377,14 +410,24 @@ public class SelectionManager implements ISelectionProvider { // reset alternate selection if any mAltSelection = null; + boolean redoLayout = hasExplodedItems(); + mSelections.clear(); if (viewInfos != null) { for (CanvasViewInfo viewInfo : viewInfos) { mSelections.add(createSelection(viewInfo)); + if (viewInfo.isInvisibleParent()) { + redoLayout = true; + } } } fireSelectionChanged(); + + if (redoLayout) { + mCanvas.getLayoutEditor().recomputeLayout(); + } + redraw(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java index 36fdad9..2748297 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SwtDrawingStyle.java @@ -90,6 +90,11 @@ public enum SwtDrawingStyle { INVALID(new RGB(0xFF, 0xFF, 0xFF), 255, new RGB(0xFF, 0x00, 0x00), 150, 2, SWT.LINE_SOLID), /** + * The style definition corresponding to {@link DrawingStyle#EMPTY} + */ + EMPTY(new RGB(0xFF, 0xFF, 0x55), 255, new RGB(0xFF, 0xFF, 0x55), 255, 1, SWT.LINE_DASH), + + /** * The style definition corresponding to {@link DrawingStyle#CUSTOM1} */ CUSTOM1(new RGB(0xFF, 0x00, 0xFF), 255, null, 0, 1, SWT.LINE_SOLID), @@ -183,6 +188,8 @@ public enum SwtDrawingStyle { /** * Return the corresponding SwtDrawingStyle for the given * {@link DrawingStyle} + * @param style The style to convert from a {@link DrawingStyle} to a {@link SwtDrawingStyle}. + * @return A corresponding {@link SwtDrawingStyle}. */ public static SwtDrawingStyle of(DrawingStyle style) { switch (style) { @@ -208,6 +215,8 @@ public enum SwtDrawingStyle { return HELP; case INVALID: return INVALID; + case EMPTY: + return EMPTY; case CUSTOM1: return CUSTOM1; case CUSTOM2: 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 index 24d0caa..8c39662 100644 --- 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 @@ -19,6 +19,7 @@ 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.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.layoutlib.api.ILayoutResult; import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; @@ -27,7 +28,10 @@ import org.w3c.dom.Node; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * The view hierarchy class manages a set of view info objects and performs find @@ -48,7 +52,7 @@ public class ViewHierarchy { } /** - * The CanvasViewInfo root created by the last call to {@link #setResult(ILayoutResult)} + * The CanvasViewInfo root created by the last call to {@link #setResult} * with a valid layout. * <p/> * This <em>can</em> be null to indicate we're dealing with an empty document with @@ -59,7 +63,7 @@ public class ViewHierarchy { private CanvasViewInfo mLastValidViewInfoRoot; /** - * True when the last {@link #setResult(ILayoutResult)} provided a valid {@link ILayoutResult}. + * True when the last {@link #setResult} 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. @@ -70,6 +74,27 @@ public class ViewHierarchy { private boolean mIsResultValid; /** + * A list of invisible parents (see {@link CanvasViewInfo#isInvisibleParent()} for + * details) in the current view hierarchy. + */ + private final List<CanvasViewInfo> mInvisibleParents = new ArrayList<CanvasViewInfo>(); + + /** + * A read-only view of {@link #mInvisibleParents}; note that this is NOT a copy so it + * reflects updates to the underlying {@link #mInvisibleParents} list. + */ + private final List<CanvasViewInfo> mInvisibleParentsReadOnly = + Collections.unmodifiableList(mInvisibleParents); + + /** + * Flag which records whether or not we have any exploded parent nodes in this + * view hierarchy. This is used to track whether or not we need to recompute the + * layout when we exit show-all-invisible-parents mode (see + * {@link LayoutCanvas#showInvisibleViews}). + */ + private boolean mExplodedParents; + + /** * 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. * @@ -78,9 +103,15 @@ public class ViewHierarchy { * when it is valid. * * @param result The new rendering result, either valid or not. + * @param explodedNodes The set of individual nodes the layout computer was asked to + * explode. Note that these are independent of the explode-all mode where + * all views are exploded; this is used only for the mode ( + * {@link LayoutCanvas#showInvisibleViews}) where individual invisible + * nodes are padded during certain interactions. */ - /* package */ void setResult(ILayoutResult result) { + /* package */ void setResult(ILayoutResult result, Set<UiElementNode> explodedNodes) { mIsResultValid = (result != null && result.getSuccess() == ILayoutResult.SUCCESS); + mExplodedParents = false; if (mIsResultValid && result != null) { ILayoutViewInfo root = result.getRootView(); @@ -92,8 +123,18 @@ public class ViewHierarchy { updateNodeProxies(mLastValidViewInfoRoot); + // Update the data structures related to tracking invisible and exploded nodes. + // We need to find the {@link CanvasViewInfo} objects that correspond to + // the passed in {@link UiElementNode} keys that were re-rendered, and mark + // them as exploded and store them in a list for rendering. + mExplodedParents = false; + mInvisibleParents.clear(); + addInvisibleParents(mLastValidViewInfoRoot, explodedNodes); + // Update the selection mCanvas.getSelectionManager().sync(mLastValidViewInfoRoot); + } else { + mInvisibleParents.clear(); } } @@ -122,10 +163,45 @@ public class ViewHierarchy { } } + /** + * Make a pass over the view hierarchy and look for two things: + * <ol> + * <li>Invisible parents. These are nodes that can hold children and have empty + * bounds. These are then added to the {@link #mInvisibleParents} list. + * <li>Exploded nodes. These are nodes that were previously marked as invisible, and + * subsequently rendered by a recomputed layout. They now no longer have empty bounds, + * but should be specially marked via {@link CanvasViewInfo#setExploded} such that we + * for example in selection operations can determine if we need to recompute the + * layout. + * </ol> + * + * @param vi + * @param invisibleNodes + */ + private void addInvisibleParents(CanvasViewInfo vi, Set<UiElementNode> invisibleNodes) { + if (vi == null) { + return; + } + + if (vi.isInvisibleParent()) { + mInvisibleParents.add(vi); + } else if (invisibleNodes != null) { + UiViewElementNode key = vi.getUiViewKey(); + if (key != null && invisibleNodes.contains(key)) { + vi.setExploded(true); + mExplodedParents = true; + mInvisibleParents.add(vi); + } + } + + for (CanvasViewInfo child : vi.getChildren()) { + addInvisibleParents(child, invisibleNodes); + } + } /** - * Returns true when the last {@link #setResult(ILayoutResult)} provided a valid + * Returns true when the last {@link #setResult} provided a valid * {@link ILayoutResult}. * <p/> * When false this means the canvas is displaying an out-dated result image & bounds and some @@ -147,6 +223,24 @@ public class ViewHierarchy { return mLastValidViewInfoRoot == null; } + /** + * Returns true if we have parents in this hierarchy that are invisible (e.g. because + * they have no children and zero layout bounds). + * + * @return True if we have invisible parents. + */ + public boolean hasInvisibleParents() { + return mInvisibleParents.size() > 0; + } + + /** + * Returns true if we have views that were exploded during rendering + * @return True if we have exploded parents + */ + public boolean hasExplodedParents() { + return mExplodedParents; + } + /** 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. @@ -411,7 +505,7 @@ public class ViewHierarchy { } /** - * Return the root of the view hierarchy, if any (could be null, for example + * Returns the root of the view hierarchy, if any (could be null, for example * on rendering failure). * * @return The current view hierarchy, or null @@ -420,4 +514,42 @@ public class ViewHierarchy { return mLastValidViewInfoRoot; } + /** + * Returns a collection of views that have zero bounds and that correspond to empty + * parents. Note that the views may not actually have zero bounds; in particular, if + * they are exploded ({@link CanvasViewInfo#isExploded()}, then they will have the + * bounds of a shown invisible node. Therefore, this method returns the views that + * would be invisible in a real rendering of the scene. + * + * @return A collection of empty parent views. + */ + public List<CanvasViewInfo> getInvisibleViews() { + return mInvisibleParentsReadOnly; + } + + /** + * Returns the invisible nodes (the {@link UiElementNode} objects corresponding + * to the {@link CanvasViewInfo} objects returned from {@link #getInvisibleViews()}. + * We are pulling out the nodes since they preserve their identity across layout + * rendering, and in particular we return it as a set such that the layout renderer + * can perform quick identity checks when looking up attribute values during the + * rendering process. + * + * @return A set of the invisible nodes. + */ + public Set<UiElementNode> getInvisibleNodes() { + if (mInvisibleParents.size() == 0) { + return Collections.emptySet(); + } + + Set<UiElementNode> nodes = new HashSet<UiElementNode>(mInvisibleParents.size()); + for (CanvasViewInfo info : mInvisibleParents) { + UiViewElementNode node = info.getUiViewKey(); + if (node != null) { + nodes.add(node); + } + } + + return nodes; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParserTest.java index 68e31d5..f857126 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParserTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/UiElementPullParserTest.java @@ -170,6 +170,7 @@ public class UiElementPullParserTest extends TestCase { UiElementPullParser parser = new UiElementPullParser( ui, // model false, // explodedView + null, // explodeNodes Density.MEDIUM.getDpiValue(), // density (default from ConfigurationComposite) Density.MEDIUM.getDpiValue(), // xdpi (default from ConfigurationComposite) null // iProject |