diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src')
215 files changed, 15433 insertions, 6226 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index a555ef4..83ce9ef 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -44,7 +44,6 @@ import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; -import com.android.ide.common.api.IValidator; import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.RuleAction; @@ -191,27 +190,11 @@ public class BaseViewRule extends AbstractViewRule { // Ids must be set individually so open the id dialog for each // selected node (though allow cancel to break the loop) for (INode node : selectedNodes) { - // Strip off the @id prefix stuff - String oldId = node.getStringAttr(ANDROID_URI, ATTR_ID); - oldId = stripIdPrefix(ensureValidString(oldId)); - IValidator validator = mRulesEngine.getResourceValidator("id",//$NON-NLS-1$ - false /*uniqueInProject*/, - true /*uniqueInLayout*/, - false /*exists*/, - oldId); - String newId = mRulesEngine.displayInput("New Id:", oldId, validator); - if (newId != null && newId.trim().length() > 0) { - if (!newId.startsWith(NEW_ID_PREFIX)) { - newId = NEW_ID_PREFIX + stripIdPrefix(newId); - } - node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, - ATTR_ID, newId)); - editedProperty(ATTR_ID); - } else if (newId == null) { - // Cancelled + if (!mRulesEngine.rename(node)) { break; } } + editedProperty(ATTR_ID); return; } else if (isProp) { INode firstNode = selectedNodes.get(0); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java index e809d00..f99cf0c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FragmentRule.java @@ -30,7 +30,7 @@ public class FragmentRule extends BaseViewRule { @Override public void onCreate(@NonNull INode node, @NonNull INode parent, @NonNull InsertType insertType) { - // When dropping a fragment tag, ask the user which layout to include. + // When dropping a fragment tag, ask the user which class to use. if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW String fqcn = mRulesEngine.displayFragmentSourceInput(); if (fqcn != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java index a197e23..80a23c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java @@ -28,6 +28,7 @@ import static com.android.SdkConstants.GRAVITY_VALUE_FILL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL; import static com.android.SdkConstants.GRAVITY_VALUE_LEFT; +import static com.android.SdkConstants.GRID_LAYOUT; import static com.android.SdkConstants.VALUE_HORIZONTAL; import static com.android.SdkConstants.VALUE_TRUE; @@ -53,6 +54,7 @@ import com.android.ide.common.api.SegmentType; import com.android.ide.common.layout.grid.GridDropHandler; import com.android.ide.common.layout.grid.GridLayoutPainter; import com.android.ide.common.layout.grid.GridModel; +import com.android.ide.common.layout.grid.GridModel.ViewData; import com.android.utils.Pair; import java.net.URL; @@ -143,7 +145,7 @@ public class GridLayoutRule extends BaseLayoutRule { * Whether the grid is edited in "grid mode" where the operations are row/column based * rather than free-form */ - public static boolean sGridMode = false; + public static boolean sGridMode = true; /** Constructs a new {@link GridLayoutRule} */ public GridLayoutRule() { @@ -228,6 +230,9 @@ public class GridLayoutRule extends BaseLayoutRule { // Add and Remove Column actions only apply in Grid Mode if (sGridMode) { + actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show Structure", + sShowStructure, actionCallback, ICON_SHOW_STRUCT, 147, false)); + // Add Row and Add Column actions.add(RuleAction.createSeparator(150)); actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback, @@ -366,7 +371,8 @@ public class GridLayoutRule extends BaseLayoutRule { public String getNamespace(INode layout) { String namespace = ANDROID_URI; - if (!layout.getFqcn().equals(FQCN_GRID_LAYOUT)) { + String fqcn = layout.getFqcn(); + if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) { namespace = mRulesEngine.getAppNameSpace(); } @@ -407,10 +413,12 @@ public class GridLayoutRule extends BaseLayoutRule { boolean moved) { super.onRemovingChildren(deleted, parent, moved); - // Attempt to clean up spacer objects for any newly-empty rows or columns - // as the result of this deletion - GridModel grid = GridModel.get(mRulesEngine, parent, null); - grid.onDeleted(deleted); + if (!sGridMode) { + // Attempt to clean up spacer objects for any newly-empty rows or columns + // as the result of this deletion + GridModel grid = GridModel.get(mRulesEngine, parent, null); + grid.onDeleted(deleted); + } } @Override @@ -454,6 +462,35 @@ public class GridLayoutRule extends BaseLayoutRule { Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { if (resizingWidget(state)) { + if (state.fillWidth || state.fillHeight || state.wrapWidth || state.wrapHeight) { + GridModel grid = getGrid(state); + ViewData view = grid.getView(node); + if (view != null) { + String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); + int gravity = GravityHelper.getGravity(gravityString, 0); + if (view.column > 0 && verticalEdge != null && state.fillWidth) { + state.fillWidth = false; + state.wrapWidth = true; + gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; + gravity |= GravityHelper.GRAVITY_FILL_HORIZ; + } else if (verticalEdge != null && state.wrapWidth) { + gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; + gravity |= GravityHelper.GRAVITY_LEFT; + } + if (view.row > 0 && horizontalEdge != null && state.fillHeight) { + state.fillHeight = false; + state.wrapHeight = true; + gravity &= ~GravityHelper.GRAVITY_VERT_MASK; + gravity |= GravityHelper.GRAVITY_FILL_VERT; + } else if (horizontalEdge != null && state.wrapHeight) { + gravity &= ~GravityHelper.GRAVITY_VERT_MASK; + gravity |= GravityHelper.GRAVITY_TOP; + } + gravityString = GravityHelper.getGravity(gravity); + grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString); + // Fall through and set layout_width and/or layout_height to wrap_content + } + } super.setNewSizeBounds(state, node, layout, oldBounds, newBounds, horizontalEdge, verticalEdge); } else { @@ -463,6 +500,22 @@ public class GridLayoutRule extends BaseLayoutRule { GridModel grid = getGrid(state); grid.setColumnSpanAttribute(node, columnSpan); grid.setRowSpanAttribute(node, rowSpan); + + ViewData view = grid.getView(node); + if (view != null) { + String gravityString = grid.getGridAttribute(view.node, ATTR_LAYOUT_GRAVITY); + int gravity = GravityHelper.getGravity(gravityString, 0); + if (verticalEdge != null && columnSpan > 1) { + gravity &= ~GravityHelper.GRAVITY_HORIZ_MASK; + gravity |= GravityHelper.GRAVITY_FILL_HORIZ; + } + if (horizontalEdge != null && rowSpan > 1) { + gravity &= ~GravityHelper.GRAVITY_VERT_MASK; + gravity |= GravityHelper.GRAVITY_FILL_VERT; + } + gravityString = GravityHelper.getGravity(gravity); + grid.setGridAttribute(view.node, ATTR_LAYOUT_GRAVITY, gravityString); + } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java index d7a3026..610fe5d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -188,6 +188,9 @@ public class LinearLayoutRule extends BaseLayoutRule { weight = mRulesEngine.displayInput("Enter Weight Value:", weight, null); if (weight != null) { + if (weight.isEmpty()) { + weight = null; // remove attribute + } for (INode child : children) { child.setAttribute(ANDROID_URI, ATTR_LAYOUT_WEIGHT, weight); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java index 1dafe53..9f2b4ae 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ScrollViewRule.java @@ -19,7 +19,9 @@ package com.android.ide.common.layout; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.ATTR_ORIENTATION; import static com.android.SdkConstants.FQCN_LINEAR_LAYOUT; +import static com.android.SdkConstants.VALUE_VERTICAL; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -58,7 +60,8 @@ public class ScrollViewRule extends FrameLayoutRule { // Insert a default linear layout (which will in turn be registered as // a child of this node and the create child method above will set its // fill parent attributes, its id, etc. - node.appendChild(FQCN_LINEAR_LAYOUT); + INode linear = node.appendChild(FQCN_LINEAR_LAYOUT); + linear.setAttribute(ANDROID_URI, ATTR_ORIENTATION, VALUE_VERTICAL); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java new file mode 100644 index 0000000..a89a3d8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/ViewTagRule.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 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.common.layout; + +import static com.android.SdkConstants.ATTR_CLASS; + +import com.android.annotations.NonNull; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.InsertType; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; + +/** + * An {@link IViewRule} for the special XML {@code <view>} tag. + */ +public class ViewTagRule extends BaseViewRule { + @Override + public void onCreate(@NonNull INode node, @NonNull INode parent, + @NonNull InsertType insertType) { + // When dropping a view tag, ask the user which custom view class to use + if (insertType == InsertType.CREATE) { // NOT InsertType.CREATE_PREVIEW + String fqcn = mRulesEngine.displayCustomViewClassInput(); + if (fqcn != null) { + if (!ViewElementDescriptor.viewNeedsPackage(fqcn)) { + fqcn = fqcn.substring(fqcn.lastIndexOf('.') + 1); + } + node.editXml("Set Custom View Class", + new PropertySettingNodeHandler(null, ATTR_CLASS, + fqcn.length() > 0 ? fqcn : null)); + } else { + // Remove the view; the insertion was canceled + parent.removeChild(node); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java index 8a6fdef..8bdb56b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridDropHandler.java @@ -15,17 +15,17 @@ */ package com.android.ide.common.layout.grid; -import static com.android.ide.common.layout.GravityHelper.getGravity; -import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; -import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; -import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; -import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; import static com.android.SdkConstants.ATTR_COLUMN_COUNT; import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; import static com.android.SdkConstants.ATTR_LAYOUT_ROW; import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; +import static com.android.ide.common.layout.GravityHelper.getGravity; +import static com.android.ide.common.layout.GridLayoutRule.GRID_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MARGIN_SIZE; +import static com.android.ide.common.layout.GridLayoutRule.MAX_CELL_DIFFERENCE; +import static com.android.ide.common.layout.GridLayoutRule.SHORT_GAP_DP; import static com.android.ide.common.layout.grid.GridModel.UNDEFINED; import static java.lang.Math.abs; @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; /** * The {@link GridDropHandler} handles drag and drop operations into and within a @@ -83,8 +84,10 @@ public class GridDropHandler { int x1 = p.x; int y1 = p.y; + Rect dragBounds = feedback.dragBounds; + int w = dragBounds != null ? dragBounds.w : 0; + int h = dragBounds != null ? dragBounds.h : 0; if (!GridLayoutRule.sGridMode) { - Rect dragBounds = feedback.dragBounds; if (dragBounds != null) { // Sometimes the items are centered under the mouse so // offset by the top left corner distance @@ -92,8 +95,6 @@ public class GridDropHandler { y1 += dragBounds.y; } - int w = dragBounds != null ? dragBounds.w : 0; - int h = dragBounds != null ? dragBounds.h : 0; int x2 = x1 + w; int y2 = y1 + h; @@ -185,19 +186,72 @@ public class GridDropHandler { int SLOP = 2; int radius = mRule.getNewCellSize(); if (rightDistance < radius + SLOP) { - column++; + column = Math.min(column + 1, mGrid.actualColumnCount); leftDistance = rightDistance; } if (bottomDistance < radius + SLOP) { - row++; + row = Math.min(row + 1, mGrid.actualRowCount); topDistance = bottomDistance; } - boolean matchLeft = leftDistance < radius + SLOP; - boolean matchTop = topDistance < radius + SLOP; + boolean createColumn = leftDistance < radius + SLOP; + boolean createRow = topDistance < radius + SLOP; + if (x1 >= bounds.x2()) { + createColumn = true; + } + if (y1 >= bounds.y2()) { + createRow = true; + } - mColumnMatch = new GridMatch(SegmentType.LEFT, 0, x1, column, matchLeft, 0); - mRowMatch = new GridMatch(SegmentType.TOP, 0, y1, row, matchTop, 0); + int cellWidth = leftDistance + rightDistance; + int cellHeight = topDistance + bottomDistance; + SegmentType horizontalType = SegmentType.LEFT; + SegmentType verticalType = SegmentType.TOP; + int minDistance = 10; // Don't center or right/bottom align in tiny cells + if (!createColumn && leftDistance > minDistance + && dragBounds != null && dragBounds.w < cellWidth - 10) { + if (rightDistance < leftDistance) { + horizontalType = SegmentType.RIGHT; + } + + int centerDistance = Math.abs(cellWidth / 2 - leftDistance); + if (centerDistance < leftDistance / 2 && centerDistance < rightDistance / 2) { + horizontalType = SegmentType.CENTER_HORIZONTAL; + } + } + if (!createRow && topDistance > minDistance + && dragBounds != null && dragBounds.h < cellHeight - 10) { + if (bottomDistance < topDistance) { + verticalType = SegmentType.BOTTOM; + } + int centerDistance = Math.abs(cellHeight / 2 - topDistance); + if (centerDistance < topDistance / 2 && centerDistance < bottomDistance / 2) { + verticalType = SegmentType.CENTER_VERTICAL; + } + } + + mColumnMatch = new GridMatch(horizontalType, 0, x1, column, createColumn, 0); + mRowMatch = new GridMatch(verticalType, 0, y1, row, createRow, 0); + + StringBuilder description = new StringBuilder(50); + String rowString = Integer.toString(mColumnMatch.cellIndex + 1); + String columnString = Integer.toString(mRowMatch.cellIndex + 1); + if (mRowMatch.createCell && mRowMatch.cellIndex < mGrid.actualRowCount) { + description.append(String.format("Shift row %1$d down", mRowMatch.cellIndex + 1)); + description.append('\n'); + } + if (mColumnMatch.createCell && mColumnMatch.cellIndex < mGrid.actualColumnCount) { + description.append(String.format("Shift column %1$d right", + mColumnMatch.cellIndex + 1)); + description.append('\n'); + } + description.append(String.format("Insert into cell (%1$s,%2$s)", + rowString, columnString)); + description.append('\n'); + description.append(String.format("Align %1$s, %2$s", + horizontalType.name().toLowerCase(Locale.US), + verticalType.name().toLowerCase(Locale.US))); + feedback.tooltip = description.toString(); } } @@ -713,16 +767,46 @@ public class GridDropHandler { String fqcn = element.getFqcn(); INode newChild = targetNode.appendChild(fqcn); + int column = mColumnMatch.cellIndex; if (mColumnMatch.createCell) { - mGrid.addColumn(mColumnMatch.cellIndex, + mGrid.addColumn(column, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); } + int row = mRowMatch.cellIndex; if (mRowMatch.createCell) { - mGrid.addRow(mRowMatch.cellIndex, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); + mGrid.addRow(row, newChild, UNDEFINED, false, UNDEFINED, UNDEFINED); } - mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, mColumnMatch.cellIndex); - mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, mRowMatch.cellIndex); + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_COLUMN, column); + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_ROW, row); + + int gravity = 0; + if (mColumnMatch.type == SegmentType.RIGHT) { + gravity |= GravityHelper.GRAVITY_RIGHT; + } else if (mColumnMatch.type == SegmentType.CENTER_HORIZONTAL) { + gravity |= GravityHelper.GRAVITY_CENTER_HORIZ; + } + if (mRowMatch.type == SegmentType.BASELINE) { + // There *is* no baseline gravity constant, instead, leave the + // vertical gravity unspecified and GridLayout will treat it as + // baseline alignment + //gravity |= GravityHelper.GRAVITY_BASELINE; + } else if (mRowMatch.type == SegmentType.BOTTOM) { + gravity |= GravityHelper.GRAVITY_BOTTOM; + } else if (mRowMatch.type == SegmentType.CENTER_VERTICAL) { + gravity |= GravityHelper.GRAVITY_CENTER_VERT; + } + if (!GravityHelper.isConstrainedHorizontally(gravity)) { + gravity |= GravityHelper.GRAVITY_LEFT; + } + if (!GravityHelper.isConstrainedVertically(gravity)) { + gravity |= GravityHelper.GRAVITY_TOP; + } + mGrid.setGridAttribute(newChild, ATTR_LAYOUT_GRAVITY, getGravity(gravity)); + + if (mGrid.declaredColumnCount == UNDEFINED || mGrid.declaredColumnCount < column + 1) { + mGrid.setGridAttribute(mGrid.layout, ATTR_COLUMN_COUNT, column + 1); + } return newChild; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java index 9e7cfae..7e2d3a7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridLayoutPainter.java @@ -282,10 +282,12 @@ public class GridLayoutPainter { gc.drawRect(b.x + 2 * radius, b.y + 2 * radius, b.x2() - 2 * radius, b.y2() - 2 * radius); - int column = data.getColumnMatch().cellIndex; - int row = data.getRowMatch().cellIndex; - boolean createColumn = data.getColumnMatch().createCell; - boolean createRow = data.getRowMatch().createCell; + GridMatch columnMatch = data.getColumnMatch(); + GridMatch rowMatch = data.getRowMatch(); + int column = columnMatch.cellIndex; + int row = rowMatch.cellIndex; + boolean createColumn = columnMatch.createCell; + boolean createRow = rowMatch.createCell; Rect cellBounds = grid.getCellBounds(row, column, 1, 1); @@ -312,7 +314,22 @@ public class GridLayoutPainter { } gc.useStyle(DrawingStyle.DROP_PREVIEW); - mRule.drawElement(gc, first, offsetX, offsetY); + + Rect bounds = first.getBounds(); + int x = offsetX; + int y = offsetY; + if (columnMatch.type == SegmentType.RIGHT) { + x += cellBounds.w - bounds.w; + } else if (columnMatch.type == SegmentType.CENTER_HORIZONTAL) { + x += cellBounds.w / 2 - bounds.w / 2; + } + if (rowMatch.type == SegmentType.BOTTOM) { + y += cellBounds.h - bounds.h; + } else if (rowMatch.type == SegmentType.CENTER_VERTICAL) { + y += cellBounds.h / 2 - bounds.h / 2; + } + + mRule.drawElement(gc, first, x, y); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java index 186e7d0..9bee343 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridMatch.java @@ -124,6 +124,7 @@ class GridMatch implements Comparable<GridMatch> { } return String.format("Align bottom at y=%1d", matchedLine - layout.getBounds().y); case CENTER_VERTICAL: + return "Center vertically"; case UNKNOWN: default: return null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java index fa9a11f..a453147 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/grid/GridModel.java @@ -80,7 +80,7 @@ public class GridModel { static final int UNDEFINED = Integer.MIN_VALUE; /** The size of spacers in the dimension that they are not defining */ - private static final int SPACER_SIZE_DP = 1; + static final int SPACER_SIZE_DP = 1; /** Attribute value used for {@link #SPACER_SIZE_DP} */ private static final String SPACER_SIZE = String.format(VALUE_N_DP, SPACER_SIZE_DP); @@ -361,7 +361,8 @@ public class GridModel { if (mNamespace == null) { mNamespace = ANDROID_URI; - if (!layout.getFqcn().equals(FQCN_GRID_LAYOUT)) { + String fqcn = layout.getFqcn(); + if (!fqcn.equals(GRID_LAYOUT) && !fqcn.equals(FQCN_GRID_LAYOUT)) { mNamespace = mRulesEngine.getAppNameSpace(); } } @@ -681,11 +682,26 @@ public class GridModel { if (cellBounds != null) { int[] xs = cellBounds.getFirst(); int[] ys = cellBounds.getSecond(); + Rect layoutBounds = layout.getBounds(); + + // Handle "blank" grid layouts: insert a fake grid of CELL_COUNT^2 cells + // where the user can do initial placement + if (actualColumnCount <= 1 && actualRowCount <= 1 && mChildViews.isEmpty()) { + final int CELL_COUNT = 1; + xs = new int[CELL_COUNT + 1]; + ys = new int[CELL_COUNT + 1]; + int cellWidth = layoutBounds.w / CELL_COUNT; + int cellHeight = layoutBounds.h / CELL_COUNT; + + for (int i = 0; i <= CELL_COUNT; i++) { + xs[i] = i * cellWidth; + ys[i] = i * cellHeight; + } + } actualColumnCount = xs.length - 1; actualRowCount = ys.length - 1; - Rect layoutBounds = layout.getBounds(); int layoutBoundsX = layoutBounds.x; int layoutBoundsY = layoutBounds.y; mLeft = new int[xs.length]; @@ -1810,7 +1826,7 @@ public class GridModel { * Data about a view in a table; this is not the same as a cell because multiple views * can share a single cell, and a view can span many cells. */ - class ViewData { + public class ViewData { public final INode node; public final int index; public int row; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java index be928b0..e246975 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/resources/platform/AttributeInfo.java @@ -20,6 +20,7 @@ import static com.android.SdkConstants.ANDROID_PREFIX; import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_THEME_REF; import static com.android.SdkConstants.VALUE_FALSE; import static com.android.SdkConstants.VALUE_TRUE; import static com.android.ide.common.api.IAttributeInfo.Format.BOOLEAN; @@ -211,6 +212,11 @@ public class AttributeInfo implements IAttributeInfo { // All other formats require a nonempty string if (value.isEmpty()) { + // Except for flags + if (mFormats.contains(FLAG)) { + return true; + } + return false; } char first = value.charAt(0); @@ -262,6 +268,14 @@ public class AttributeInfo implements IAttributeInfo { //String name = url.substring(nameBegin); return true; } + } else if (value.startsWith(PREFIX_THEME_REF)) { + if (projectResources != null) { + return projectResources.hasResourceItem(ResourceType.ATTR, + value.substring(PREFIX_THEME_REF.length())); + } else { + // Until proven otherwise + return true; + } } } @@ -290,7 +304,7 @@ public class AttributeInfo implements IAttributeInfo { } if (mFormats.contains(BOOLEAN)) { - if (value.equals(VALUE_TRUE) || value.equals(VALUE_FALSE)) { + if (value.equalsIgnoreCase(VALUE_TRUE) || value.equalsIgnoreCase(VALUE_FALSE)) { return true; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java index e9cee47..76808e4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt; import static com.android.SdkConstants.DOT_AIDL; import static com.android.SdkConstants.DOT_DEP; +import static com.android.SdkConstants.DOT_FS; import static com.android.SdkConstants.DOT_JAVA; import static com.android.SdkConstants.DOT_RS; @@ -118,6 +119,8 @@ public class AdtConstants { public final static String RE_AIDL_EXT = "\\" + DOT_AIDL + "$"; //$NON-NLS-1$ //$NON-NLS-2$ /** Regexp for rs extension, i.e. "\.rs$" */ public final static String RE_RS_EXT = "\\" + DOT_RS + "$"; //$NON-NLS-1$ //$NON-NLS-2$ + /** Regexp for rs extension, i.e. "\.fs$" */ + public final static String RE_FS_EXT = "\\" + DOT_FS + "$"; //$NON-NLS-1$ //$NON-NLS-2$ /** Regexp for .d extension, i.e. "\.d$" */ public final static String RE_DEP_EXT = "\\" + DOT_DEP + "$"; //$NON-NLS-1$ //$NON-NLS-2$ @@ -148,6 +151,10 @@ public class AdtConstants { * when an AndroidClasspathContainerInitializer has succeeded in creating an * AndroidClasspathContainer */ public final static String MARKER_TARGET = AdtPlugin.PLUGIN_ID + ".targetProblem"; //$NON-NLS-1$ + /** Marker for Android Build Tools errors. + * This is not cleared on each build like other markers. Instead, it's cleared + * when the build tools are setup in the projectState. */ + public final static String MARKER_BUILD_TOOLS = AdtPlugin.PLUGIN_ID + ".buildToolsProblem"; //$NON-NLS-1$ /** Marker for Android Dependency errors. * This is not cleared on each build like other markers. Instead, it's cleared * when a LibraryClasspathContainerInitializer has succeeded in creating a @@ -218,4 +225,7 @@ public class AdtConstants { /** Documentation marker for elements, attributes etc that should be hidden */ public static final String DOC_HIDE = "@hide"; //$NON-NLS-1$ + + public static final String DEX_OPTIONS_FORCEJUMBO = "dex.force.jumbo"; //$NON-NLS-1$ + public static final String DEX_OPTIONS_DISABLE_MERGER = "dex.disable.merger"; //$NON-NLS-1$ } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java index a7ef6c6..7aec8f5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java @@ -265,16 +265,6 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { // Listen on resource file edits for updates to file inclusion IncludeFinder.start(); - - // Parse the SDK content. - // This is deferred in separate jobs to avoid blocking the bundle start. - final boolean isSdkLocationValid = checkSdkLocationAndId(); - if (isSdkLocationValid) { - // parse the SDK resources. - // Wait 2 seconds before starting the job. This leaves some time to the - // other bundles to initialize. - parseSdkContent(2000 /*milliseconds*/); - } } /* @@ -303,6 +293,16 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { /** Called when the workbench has been started */ public void workbenchStarted() { + // Parse the SDK content. + // This is deferred in separate jobs to avoid blocking the bundle start. + final boolean isSdkLocationValid = checkSdkLocationAndId(); + if (isSdkLocationValid) { + // parse the SDK resources. + // Wait 2 seconds before starting the job. This leaves some time to the + // other bundles to initialize. + parseSdkContent(2000 /*milliseconds*/); + } + Display display = getDisplay(); mRed = new Color(display, 0xFF, 0x00, 0x00); @@ -326,19 +326,50 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { return sPlugin; } + /** + * Returns the current display, if any + * + * @return the display + */ + @NonNull public static Display getDisplay() { - IWorkbench bench = null; synchronized (AdtPlugin.class) { - if (sPlugin == null) { - return null; + if (sPlugin != null) { + IWorkbench bench = sPlugin.getWorkbench(); + if (bench != null) { + Display display = bench.getDisplay(); + if (display != null) { + return display; + } + } } - bench = sPlugin.getWorkbench(); } - if (bench != null) { - return bench.getDisplay(); + Display display = Display.getCurrent(); + if (display != null) { + return display; } - return null; + + return Display.getDefault(); + } + + /** + * Returns the shell, if any + * + * @return the shell, if any + */ + @Nullable + public static Shell getShell() { + Display display = AdtPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + if (shell == null) { + Shell[] shells = display.getShells(); + if (shells.length > 0) { + shell = shells[0]; + } + } + + return shell; } /** Returns the adb path relative to the sdk folder */ @@ -513,6 +544,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { * @param string the string to be searched for * @return true if the file is found and contains the given string anywhere within it */ + @SuppressWarnings("resource") // Closed by streamContains public static boolean fileContains(IFile file, String string) { InputStream contents = null; try { @@ -1197,14 +1229,13 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { } private void openSdkManager() { - // Windows only: open the standalone external SDK Manager since we know + // Open the standalone external SDK Manager since we know // that ADT on Windows is bound to be locking some SDK folders. - // Also when this is invoked becasue SdkManagerAction.run() fails, this + // + // Also when this is invoked because SdkManagerAction.run() fails, this // test will fail and we'll fallback on using the internal one. - if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { - if (SdkManagerAction.openExternalSdkManager()) { - return; - } + if (SdkManagerAction.openExternalSdkManager()) { + return; } // Otherwise open the regular SDK Manager bundled within ADT @@ -1402,7 +1433,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { // project that have been resolved before the sdk was loaded // will have a ProjectState where the IAndroidTarget is null // so we load the target now that the SDK is loaded. - sdk.loadTarget(Sdk.getProjectState(iProject)); + sdk.loadTargetAndBuildTools(Sdk.getProjectState(iProject)); list.add(javaProject); } } @@ -1580,7 +1611,10 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { monitor.addFileListener(new IFileListener() { @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject) { + return; + } if (flags == IResourceDelta.MARKERS || !SdkConstants.EXT_XML.equals(extension)) { // ONLY the markers changed, or not XML file: not relevant to this listener return; @@ -1709,7 +1743,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { (List<ITargetChangeListener>)mTargetChangeListeners.clone(); Display display = AdtPlugin.getDisplay(); - if (display == null) { + if (display == null || display.isDisposed()) { return; } display.asyncExec(new Runnable() { @@ -1850,7 +1884,7 @@ public class AdtPlugin extends AbstractUIPlugin implements ILogger { // --------- ILogger methods ----------- @Override - public void error(Throwable t, String format, Object... args) { + public void error(@Nullable Throwable t, @Nullable String format, Object... args) { if (t != null) { log(t, format, args); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java index d5fa567..697a0bc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtUtils.java @@ -18,15 +18,20 @@ package com.android.ide.eclipse.adt; import static com.android.SdkConstants.TOOLS_PREFIX; import static com.android.SdkConstants.TOOLS_URI; +import static org.eclipse.ui.IWorkbenchPage.MATCH_INPUT; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.sdk.SdkVersionInfo; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper.IProjectFilter; import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.repository.PkgProps; @@ -34,6 +39,10 @@ import com.android.utils.XmlUtils; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; +import org.eclipse.core.filebuffers.FileBuffers; +import org.eclipse.core.filebuffers.ITextFileBuffer; +import org.eclipse.core.filebuffers.ITextFileBufferManager; +import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; @@ -47,6 +56,7 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.core.IJavaProject; @@ -68,10 +78,16 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.editors.text.TextFileDocumentProvider; +import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Attr; +import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -80,6 +96,8 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -348,6 +366,47 @@ public class AdtUtils { } /** + * Looks through the open editors and returns the editors that have the + * given file as input. + * + * @param file the file to search for + * @param restore whether editors should be restored (if they have an open + * tab, but the editor hasn't been restored since the most recent + * IDE start yet + * @return a collection of editors + */ + @NonNull + public static Collection<IEditorPart> findEditorsFor(@NonNull IFile file, boolean restore) { + FileEditorInput input = new FileEditorInput(file); + List<IEditorPart> result = null; + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow[] windows = workbench.getWorkbenchWindows(); + for (IWorkbenchWindow window : windows) { + IWorkbenchPage[] pages = window.getPages(); + for (IWorkbenchPage page : pages) { + IEditorReference[] editors = page.findEditors(input, null, MATCH_INPUT); + if (editors != null) { + for (IEditorReference reference : editors) { + IEditorPart editor = reference.getEditor(restore); + if (editor != null) { + if (result == null) { + result = new ArrayList<IEditorPart>(); + } + result.add(editor); + } + } + } + } + } + + if (result == null) { + return Collections.emptyList(); + } + + return result; + } + + /** * Attempts to convert the given {@link URL} into a {@link File}. * * @param url the {@link URL} to be converted @@ -651,6 +710,23 @@ public class AdtUtils { } /** + * Returns the XML editor for the given editor part + * + * @param part the editor part to look up the editor for + * @return the editor or null if this part is not an XML editor + */ + @Nullable + public static AndroidXmlEditor getXmlEditor(@NonNull IEditorPart part) { + if (part instanceof AndroidXmlEditor) { + return (AndroidXmlEditor) part; + } else if (part instanceof GraphicalEditorPart) { + ((GraphicalEditorPart) part).getEditorDelegate().getEditor(); + } + + return null; + } + + /** * Sets the given tools: attribute in the given XML editor document, adding * the tools name space declaration if necessary, formatting the affected * document region, and optionally comma-appending to an existing value and @@ -678,11 +754,11 @@ public class AdtUtils { editor.wrapUndoEditXmlModel(description, new Runnable() { @Override public void run() { - String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null); + String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, null, true); if (prefix == null) { // Add in new prefix... prefix = XmlUtils.lookupNamespacePrefix(element, - TOOLS_URI, TOOLS_PREFIX); + TOOLS_URI, TOOLS_PREFIX, true /*create*/); if (value != null) { // ...and ensure that the header is formatted such that // the XML namespace declaration is placed in the right @@ -740,41 +816,6 @@ public class AdtUtils { } /** - * Returns the applicable build code (for - * {@code android.os.Build.VERSION_CODES}) for the corresponding API level, - * or null if it's unknown. - * - * @param api the API level to look up a version code for - * @return the corresponding build code field name, or null - */ - @Nullable - public static String getBuildCodes(int api) { - // See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html - switch (api) { - case 1: return "BASE"; //$NON-NLS-1$ - case 2: return "BASE_1_1"; //$NON-NLS-1$ - case 3: return "CUPCAKE"; //$NON-NLS-1$ - case 4: return "DONUT"; //$NON-NLS-1$ - case 5: return "ECLAIR"; //$NON-NLS-1$ - case 6: return "ECLAIR_0_1"; //$NON-NLS-1$ - case 7: return "ECLAIR_MR1"; //$NON-NLS-1$ - case 8: return "FROYO"; //$NON-NLS-1$ - case 9: return "GINGERBREAD"; //$NON-NLS-1$ - case 10: return "GINGERBREAD_MR1"; //$NON-NLS-1$ - case 11: return "HONEYCOMB"; //$NON-NLS-1$ - case 12: return "HONEYCOMB_MR1"; //$NON-NLS-1$ - case 13: return "HONEYCOMB_MR2"; //$NON-NLS-1$ - case 14: return "ICE_CREAM_SANDWICH"; //$NON-NLS-1$ - case 15: return "ICE_CREAM_SANDWICH_MR1"; //$NON-NLS-1$ - case 16: return "JELLY_BEAN"; //$NON-NLS-1$ - // If you add more versions here, also update #getAndroidName and - // LintConstants#HIGHEST_KNOWN_API - } - - return null; - } - - /** * Returns a string label for the given target, of the form * "API 16: Android 4.1 (Jelly Bean)". * @@ -802,6 +843,97 @@ public class AdtUtils { } /** + * Sets the given tools: attribute in the given XML editor document, adding + * the tools name space declaration if necessary, and optionally + * comma-appending to an existing value. + * + * @param file the file associated with the element + * @param element the associated element + * @param description the description of the attribute (shown in the undo + * event) + * @param name the name of the attribute + * @param value the attribute value + * @param appendValue if true, add this value as a comma separated value to + * the existing attribute value, if any + */ + public static void setToolsAttribute( + @NonNull final IFile file, + @NonNull final Element element, + @NonNull final String description, + @NonNull final String name, + @Nullable final String value, + final boolean appendValue) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + if (modelManager == null) { + return; + } + + try { + IStructuredModel model = null; + if (model == null) { + model = modelManager.getModelForEdit(file); + } + if (model != null) { + try { + model.aboutToChangeModel(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Document doc = domModel.getDocument(); + if (doc != null && element.getOwnerDocument() == doc) { + String prefix = XmlUtils.lookupNamespacePrefix(element, TOOLS_URI, + null, true); + if (prefix == null) { + // Add in new prefix... + prefix = XmlUtils.lookupNamespacePrefix(element, + TOOLS_URI, TOOLS_PREFIX, true); + } + + String v = value; + if (appendValue && v != null) { + String prev = element.getAttributeNS(TOOLS_URI, name); + if (prev.length() > 0) { + v = prev + ',' + value; + } + } + + // Use the non-namespace form of set attribute since we can't + // reference the namespace until the model has been reloaded + if (v != null) { + element.setAttribute(prefix + ':' + name, v); + } else { + element.removeAttribute(prefix + ':' + name); + } + } + } + } finally { + model.changedModel(); + String updated = model.getStructuredDocument().get(); + model.releaseFromEdit(); + model.save(file); + + // Must also force a save on disk since the above model.save(file) often + // (always?) has no effect. + ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager(); + NullProgressMonitor monitor = new NullProgressMonitor(); + IPath path = file.getFullPath(); + manager.connect(path, LocationKind.IFILE, monitor); + try { + ITextFileBuffer buffer = manager.getTextFileBuffer(path, + LocationKind.IFILE); + IDocument currentDocument = buffer.getDocument(); + currentDocument.set(updated); + buffer.commit(monitor, true); + } finally { + manager.disconnect(path, LocationKind.IFILE, monitor); + } + } + } + } catch (Exception e) { + AdtPlugin.log(e, null); + } + } + + /** * Returns the Android version and code name of the given API level * * @param api the api level @@ -826,8 +958,9 @@ public class AdtUtils { case 14: return "API 14: Android 4.0 (IceCreamSandwich)"; case 15: return "API 15: Android 4.0.3 (IceCreamSandwich)"; case 16: return "API 16: Android 4.1 (Jelly Bean)"; - // If you add more versions here, also update #getBuildCodes and - // LintConstants#HIGHEST_KNOWN_API + case 17: return "API 17: Android 4.2 (Jelly Bean)"; + // If you add more versions here, also update LintUtils#getBuildCodes and + // SdkConstants#HIGHEST_KNOWN_API default: { // Consult SDK manager to see if we know any more (later) names, @@ -857,7 +990,7 @@ public class AdtUtils { * @return the highest known API number */ public static int getHighestKnownApiLevel() { - return SdkConstants.HIGHEST_KNOWN_API; + return SdkVersionInfo.HIGHEST_KNOWN_API; } /** @@ -1236,6 +1369,62 @@ public class AdtUtils { } /** + * Returns all resource variations for the given file + * + * @param file resource file, which should be an XML file in one of the + * various resource folders, e.g. res/layout, res/values-xlarge, etc. + * @param includeSelf if true, include the file itself in the list, + * otherwise exclude it + * @return a list of all the resource variations + */ + public static List<IFile> getResourceVariations(@Nullable IFile file, boolean includeSelf) { + if (file == null) { + return Collections.emptyList(); + } + + // Compute the set of layout files defining this layout resource + List<IFile> variations = new ArrayList<IFile>(); + String name = file.getName(); + IContainer parent = file.getParent(); + if (parent != null) { + IContainer resFolder = parent.getParent(); + if (resFolder != null) { + String parentName = parent.getName(); + String prefix = parentName; + int qualifiers = prefix.indexOf('-'); + + if (qualifiers != -1) { + parentName = prefix.substring(0, qualifiers); + prefix = prefix.substring(0, qualifiers + 1); + } else { + prefix = prefix + '-'; + } + try { + for (IResource resource : resFolder.members()) { + String n = resource.getName(); + if ((n.startsWith(prefix) || n.equals(parentName)) + && resource instanceof IContainer) { + IContainer layoutFolder = (IContainer) resource; + IResource r = layoutFolder.findMember(name); + if (r instanceof IFile) { + IFile variation = (IFile) r; + if (!includeSelf && file.equals(variation)) { + continue; + } + variations.add(variation); + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + } + + return variations; + } + + /** * Returns whether the current thread is the UI thread * * @return true if the current thread is the UI thread @@ -1287,4 +1476,108 @@ public class AdtUtils { return s; } + + /** + * Looks up the {@link ResourceFolderType} corresponding to a given + * {@link ResourceType}: the folder where those resources can be found. + * <p> + * Note that {@link ResourceType#ID} is a special case: it can not just + * be defined in {@link ResourceFolderType#VALUES}, but it can also be + * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and + * {@link ResourceFolderType#MENU} folders. + * + * @param type the resource type + * @return the corresponding resource folder type + */ + @NonNull + public static ResourceFolderType getFolderTypeFor(@NonNull ResourceType type) { + switch (type) { + case ANIM: + return ResourceFolderType.ANIM; + case ANIMATOR: + return ResourceFolderType.ANIMATOR; + case ARRAY: + return ResourceFolderType.VALUES; + case COLOR: + return ResourceFolderType.COLOR; + case DRAWABLE: + return ResourceFolderType.DRAWABLE; + case INTERPOLATOR: + return ResourceFolderType.INTERPOLATOR; + case LAYOUT: + return ResourceFolderType.LAYOUT; + case MENU: + return ResourceFolderType.MENU; + case MIPMAP: + return ResourceFolderType.MIPMAP; + case RAW: + return ResourceFolderType.RAW; + case XML: + return ResourceFolderType.XML; + case ATTR: + case BOOL: + case DECLARE_STYLEABLE: + case DIMEN: + case FRACTION: + case ID: + case INTEGER: + case PLURALS: + case PUBLIC: + case STRING: + case STYLE: + case STYLEABLE: + return ResourceFolderType.VALUES; + default: + assert false : type; + return ResourceFolderType.VALUES; + + } + } + + /** + * Looks up the {@link ResourceType} defined in a given {@link ResourceFolderType}. + * <p> + * Note that for {@link ResourceFolderType#VALUES} there are many, many + * different types of resources that can be defined, so this method returns + * {@code null} for that scenario. + * <p> + * Note also that {@link ResourceType#ID} is a special case: it can not just + * be defined in {@link ResourceFolderType#VALUES}, but it can also be + * defined inline via {@code @+id} in {@link ResourceFolderType#LAYOUT} and + * {@link ResourceFolderType#MENU} folders. + * + * @param folderType the resource folder type + * @return the corresponding resource type, or null if {@code folderType} is + * {@link ResourceFolderType#VALUES} + */ + @Nullable + public static ResourceType getResourceTypeFor(@NonNull ResourceFolderType folderType) { + switch (folderType) { + case ANIM: + return ResourceType.ANIM; + case ANIMATOR: + return ResourceType.ANIMATOR; + case COLOR: + return ResourceType.COLOR; + case DRAWABLE: + return ResourceType.DRAWABLE; + case INTERPOLATOR: + return ResourceType.INTERPOLATOR; + case LAYOUT: + return ResourceType.LAYOUT; + case MENU: + return ResourceType.MENU; + case MIPMAP: + return ResourceType.MIPMAP; + case RAW: + return ResourceType.RAW; + case XML: + return ResourceType.XML; + case VALUES: + return null; + default: + assert false : folderType; + return null; + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java index b9505da..dfc2e33 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/VersionCheck.java @@ -21,7 +21,7 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler; import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler.Solution; import com.android.ide.eclipse.adt.Messages; -import com.android.sdklib.internal.repository.packages.FullRevision; +import com.android.sdklib.repository.FullRevision; import com.android.sdklib.repository.PkgProps; import org.osgi.framework.Constants; @@ -49,7 +49,7 @@ public final class VersionCheck { /** * The minimum version of the SDK Tools that this version of ADT requires. */ - private final static FullRevision MIN_TOOLS_REV = new FullRevision(20); + private final static FullRevision MIN_TOOLS_REV = new FullRevision(22, 0, 0, 0); /** * Pattern to get the minimum plugin version supported by the SDK. This is read from diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddSupportJarAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddSupportJarAction.java index 44321aa..c21c8a4 100755..100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddSupportJarAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AddSupportJarAction.java @@ -184,7 +184,7 @@ public class AddSupportJarAction implements IObjectActionDelegate { // and get the installation path of the library. AdtUpdateDialog window = new AdtUpdateDialog( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), new AdtConsoleSdkLog(), sdkLocation); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AvdManagerAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AvdManagerAction.java index 46177b0..2597090 100755..100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AvdManagerAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/AvdManagerAction.java @@ -55,7 +55,7 @@ public class AvdManagerAction implements IWorkbenchWindowActionDelegate, IObject // Runs the updater window, directing all logs to the ADT console. AvdManagerWindow window = new AvdManagerWindow( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), new AdtConsoleSdkLog(), sdk.getSdkLocation(), AvdInvocationContext.IDE); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/SdkManagerAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/SdkManagerAction.java index 9cae8a4..9d33230 100755..100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/SdkManagerAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/SdkManagerAction.java @@ -20,15 +20,14 @@ import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.internal.build.DexWrapper; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.io.FileOp; +import com.android.sdklib.repository.ISdkChangeListener; import com.android.sdklib.util.GrabProcessOutput; import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; import com.android.sdklib.util.GrabProcessOutput.Wait; -import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.repository.SdkUpdaterWindow; import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; @@ -137,7 +136,7 @@ public class SdkManagerAction implements IWorkbenchWindowActionDelegate, IObject final AtomicBoolean returnValue = new AtomicBoolean(false); final CloseableProgressMonitorDialog p = - new CloseableProgressMonitorDialog(AdtPlugin.getDisplay().getActiveShell()); + new CloseableProgressMonitorDialog(AdtPlugin.getShell()); p.setOpenOnRun(true); try { p.run(true /*fork*/, true /*cancelable*/, new IRunnableWithProgress() { @@ -262,7 +261,7 @@ public class SdkManagerAction implements IWorkbenchWindowActionDelegate, IObject // log window now.) SdkUpdaterWindow window = new SdkUpdaterWindow( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), new AdtConsoleSdkLog() { @Override public void info(@NonNull String msgFormat, Object... args) { @@ -313,9 +312,7 @@ public class SdkManagerAction implements IWorkbenchWindowActionDelegate, IObject if (sdk != null) { sdk.unloadTargetData(true /*preventReload*/); - - DexWrapper dx = sdk.getDexWrapper(); - dx.unload(); + sdk.unloadDexWrappers(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/AssetType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/AssetType.java index 3e2bd67..5cfeebb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/AssetType.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/AssetType.java @@ -68,7 +68,7 @@ public enum AssetType { /** Whether this asset type needs a shape parameter */ boolean needsShape() { - return this == LAUNCHER || this == NOTIFICATION; + return this == LAUNCHER; } /** Whether this asset type needs foreground and background color parameters */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java index 426dbae..17336ad 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/assetstudio/ConfigureAssetSetPage.java @@ -17,7 +17,6 @@ package com.android.ide.eclipse.adt.internal.assetstudio; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.DEFAULT_LAUNCHER_ICON; - import static java.awt.image.BufferedImage.TYPE_INT_ARGB; import com.android.annotations.NonNull; @@ -106,7 +105,7 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen ModifyListener { private final CreateAssetSetWizardState mValues; - private static final int PREVIEW_AREA_WIDTH = 120; + private static final int PREVIEW_AREA_WIDTH = 144; private boolean mShown; @@ -481,7 +480,7 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen // Initial image - use the most recently used image, or the default launcher // icon created in our default projects, if there if (mValues.imagePath != null) { - sImagePath = mValues.imagePath.getPath();; + sImagePath = mValues.imagePath.getPath(); } if (sImagePath == null) { IProject project = mValues.project; @@ -1149,10 +1148,7 @@ public class ConfigureAssetSetPage extends WizardPage implements SelectionListen } case NOTIFICATION: { generator = new NotificationIconGenerator(); - NotificationIconGenerator.NotificationOptions notificationOptions = - new NotificationIconGenerator.NotificationOptions(); - notificationOptions.shape = mValues.shape; - options = notificationOptions; + options = new NotificationIconGenerator.NotificationOptions(); break; } case TAB: diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java index defaca6..98a1fab 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java @@ -20,6 +20,7 @@ import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.XMLNS_ANDROID; import static com.android.SdkConstants.XMLNS_URI; +import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; @@ -343,7 +344,7 @@ public class AaptQuickFix implements IMarkerResolutionGenerator2, IQuickAssistPr } private void perform() { - Pair<ResourceType,String> resource = ResourceHelper.parseResource(mResource); + Pair<ResourceType,String> resource = ResourceRepository.parseResource(mResource); ResourceType type = resource.getFirst(); String name = resource.getSecond(); String value = ""; //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java index 52e887a..806fa9c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java @@ -17,12 +17,14 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.SdkConstants; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.io.FileOp; @@ -45,7 +47,9 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -65,6 +69,7 @@ public class AidlProcessor extends SourceProcessor { */ private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):?\\s(.+)$"); //$NON-NLS-1$ + private final static Set<String> EXTENSIONS = Collections.singleton(SdkConstants.EXT_AIDL); private enum AidlType { UNKNOWN, INTERFACE, PARCELABLE; @@ -78,13 +83,14 @@ public class AidlProcessor extends SourceProcessor { // "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$"); - public AidlProcessor(IJavaProject javaProject, IFolder genFolder) { - super(javaProject, genFolder); + public AidlProcessor(@NonNull IJavaProject javaProject, @NonNull BuildToolInfo buildToolInfo, + @NonNull IFolder genFolder) { + super(javaProject, buildToolInfo, genFolder); } @Override - protected String getExtension() { - return SdkConstants.EXT_AIDL; + protected Set<String> getExtensions() { + return EXTENSIONS; } @Override @@ -92,16 +98,15 @@ public class AidlProcessor extends SourceProcessor { return PROPERTY_COMPILE_AIDL; } - @SuppressWarnings("deprecation") @Override protected void doCompileFiles(List<IFile> sources, BaseBuilder builder, - IProject project, IAndroidTarget projectTarget, int targetApi, + IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException { // create the command line List<String> commandList = new ArrayList<String>( 4 + sourceFolders.size() + libraryProjectsOut.size()); - commandList.add(projectTarget.getPath(IAndroidTarget.AIDL)); + commandList.add(getBuildToolInfo().getPath(BuildToolInfo.PathId.AIDL)); commandList.add(quote("-p" + projectTarget.getPath(IAndroidTarget.ANDROID_AIDL))); //$NON-NLS-1$ // since the path are relative to the workspace and not the project itself, we need diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java index 5fb6660..da8c2ea 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.SdkConstants; +import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; @@ -26,6 +27,7 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.IAndroidTarget.IOptionalLibrary; import com.android.sdklib.build.ApkBuilder; @@ -39,6 +41,9 @@ import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; import com.android.sdklib.util.GrabProcessOutput; import com.android.sdklib.util.GrabProcessOutput.IProcessOutput; import com.android.sdklib.util.GrabProcessOutput.Wait; +import com.google.common.hash.HashCode; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; @@ -101,9 +106,16 @@ public class BuildHelper { private static final String COMMAND_CRUNCH = "crunch"; //$NON-NLS-1$ private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$ + @NonNull private final IProject mProject; + @NonNull + private final BuildToolInfo mBuildToolInfo; + @NonNull private final AndroidPrintStream mOutStream; + @NonNull private final AndroidPrintStream mErrStream; + private final boolean mForceJumbo; + private final boolean mDisableDexMerger; private final boolean mVerbose; private final boolean mDebugMode; @@ -132,14 +144,20 @@ public class BuildHelper { * @param verbose * @throws CoreException */ - public BuildHelper(IProject project, AndroidPrintStream outStream, - AndroidPrintStream errStream, boolean debugMode, boolean verbose, - ResourceMarker resMarker) throws CoreException { + public BuildHelper(@NonNull IProject project, + @NonNull BuildToolInfo buildToolInfo, + @NonNull AndroidPrintStream outStream, + @NonNull AndroidPrintStream errStream, + boolean forceJumbo, boolean disableDexMerger, boolean debugMode, + boolean verbose, ResourceMarker resMarker) throws CoreException { mProject = project; + mBuildToolInfo = buildToolInfo; mOutStream = outStream; mErrStream = errStream; mDebugMode = debugMode; mVerbose = verbose; + mForceJumbo = forceJumbo; + mDisableDexMerger = disableDexMerger; gatherPaths(resMarker); } @@ -683,7 +701,7 @@ public class BuildHelper { // get the dex wrapper Sdk sdk = Sdk.getCurrent(); - DexWrapper wrapper = sdk.getDexWrapper(); + DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo); if (wrapper == null) { throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, @@ -704,24 +722,28 @@ public class BuildHelper { // replace the libs by their dexed versions (dexing them if needed.) List<String> finalInputPaths = new ArrayList<String>(inputPaths.size()); - if (inputPaths.size() == 1) { + if (mDisableDexMerger || inputPaths.size() == 1) { // only one input, no need to put a pre-dexed version, even if this path is // just a jar file (case for proguard'ed builds) finalInputPaths.addAll(inputPaths); } else { + for (String input : inputPaths) { File inputFile = new File(input); if (inputFile.isDirectory()) { finalInputPaths.add(input); } else if (inputFile.isFile()) { - File dexedLib = new File(dexedLibs, inputFile.getName()); + String fileName = getDexFileName(inputFile); + + File dexedLib = new File(dexedLibs, fileName); String dexedLibPath = dexedLib.getAbsolutePath(); if (dexedLib.isFile() == false || dexedLib.lastModified() < inputFile.lastModified()) { if (mVerbose) { - mOutStream.println("Pre-Dexing " + input); + mOutStream.println( + String.format("Pre-Dexing %1$s -> %2$s", input, fileName)); } if (dexedLib.isFile()) { @@ -729,13 +751,19 @@ public class BuildHelper { } int res = wrapper.run(dexedLibPath, Collections.singleton(input), - mVerbose, mOutStream, mErrStream); + mForceJumbo, mVerbose, mOutStream, mErrStream); if (res != 0) { // output error message and mark the project. String message = String.format(Messages.Dalvik_Error_d, res); throw new DexException(message); } + } else { + if (mVerbose) { + mOutStream.println( + String.format("Using Pre-Dexed %1$s <- %2$s", + fileName, input)); + } } finalInputPaths.add(dexedLibPath); @@ -751,6 +779,7 @@ public class BuildHelper { int res = wrapper.run(osOutFilePath, finalInputPaths, + mForceJumbo, mVerbose, mOutStream, mErrStream); @@ -775,6 +804,22 @@ public class BuildHelper { } } + private String getDexFileName(File inputFile) { + // get the filename + String name = inputFile.getName(); + // remove the extension + int pos = name.lastIndexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + // add a hash of the original file path + HashFunction hashFunction = Hashing.md5(); + HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath()); + + return name + "-" + hashCode.toString() + ".jar"; + } + /** * Executes aapt. If any error happen, files or the project will be marked. * @param command The command for aapt to execute. Currently supported: package and crunch @@ -796,7 +841,7 @@ public class BuildHelper { String configFilter, int versionCode) throws AaptExecException, AaptResultException { IAndroidTarget target = Sdk.getCurrent().getTarget(mProject); - @SuppressWarnings("deprecation") String aapt = target.getPath(IAndroidTarget.AAPT); + String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT); // Create the command line. ArrayList<String> commandArray = new ArrayList<String>(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java index 5a71edc..a99dc76 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java @@ -185,7 +185,7 @@ public class ConvertSwitchQuickFixProcessor implements IQuickFixProcessor { @Override public void apply(IDocument document) { - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); ConvertSwitchDialog dialog = new ConvertSwitchDialog(shell, mExpression); dialog.open(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java index 710d257..1c7c2e3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java @@ -53,6 +53,7 @@ public final class DexWrapper { private Field mArgVerbose; private Field mArgJarOutput; private Field mArgFileNames; + private Field mArgForceJumbo; private Field mConsoleOut; private Field mConsoleErr; @@ -92,6 +93,7 @@ public final class DexWrapper { mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$ mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$ mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$ + mArgForceJumbo = argClass.getField("forceJumbo"); //$NON-NLS-1$ mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$ mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$ @@ -140,6 +142,7 @@ public final class DexWrapper { * * @param osOutFilePath the OS path to the outputfile (classes.dex * @param osFilenames list of input source files (.class and .jar files) + * @param forceJumbo force jumbo mode. * @param verbose verbose mode. * @param outStream the stdout console * @param errStream the stderr console @@ -147,13 +150,15 @@ public final class DexWrapper { * @throws CoreException */ public synchronized int run(String osOutFilePath, Collection<String> osFilenames, - boolean verbose, PrintStream outStream, PrintStream errStream) throws CoreException { + boolean forceJumbo, boolean verbose, + PrintStream outStream, PrintStream errStream) throws CoreException { assert mRunMethod != null; assert mArgConstructor != null; assert mArgOutName != null; assert mArgJarOutput != null; assert mArgFileNames != null; + assert mArgForceJumbo != null; assert mArgVerbose != null; assert mConsoleOut != null; assert mConsoleErr != null; @@ -175,6 +180,7 @@ public final class DexWrapper { mArgOutName.set(args, osOutFilePath); mArgFileNames.set(args, osFilenames.toArray(new String[osFilenames.size()])); mArgJarOutput.set(args, osOutFilePath.endsWith(SdkConstants.DOT_JAR)); + mArgForceJumbo.set(args, forceJumbo); mArgVerbose.set(args, verbose); // call the run method diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java index 7a169f3..9ceba20 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java @@ -12,6 +12,8 @@ public class Messages extends NLS { public static String AAPT_Exec_Error_d; + public static String AAPT_Exec_Error_Other_s; + public static String Added_s_s_Needs_Updating; public static String AIDL_Exec_Error_s; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptProcessor.java index 5b58c4f..af58e41 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptProcessor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptProcessor.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.SdkConstants; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder; @@ -25,7 +26,9 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceFolderType; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; +import com.google.common.collect.Sets; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; @@ -47,6 +50,7 @@ import java.io.IOException; 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; @@ -63,6 +67,12 @@ public class RenderScriptProcessor extends SourceProcessor { */ private static Pattern sLlvmPattern1 = Pattern.compile("^(.+?):(\\d+):(\\d+):\\s(.+)$"); //$NON-NLS-1$ + private final static Set<String> EXTENSIONS = Sets.newHashSetWithExpectedSize(2); + static { + EXTENSIONS.add(SdkConstants.EXT_RS); + EXTENSIONS.add(SdkConstants.EXT_FS); + } + private static class RsChangeHandler extends SourceChangeHandler { @Override @@ -85,44 +95,71 @@ public class RenderScriptProcessor extends SourceProcessor { // remove the file name segment relative = relative.removeLastSegments(1); // add the file name of a Renderscript file. - relative = relative.append(file.getName().replaceAll(AdtConstants.RE_DEP_EXT, - SdkConstants.DOT_RS)); - - // now look for a match in the source folders. - List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths( - processor.getJavaProject()); - IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); - - for (IPath sourceFolderPath : sourceFolders) { - IFolder sourceFolder = root.getFolder(sourceFolderPath); - // we don't look in the 'gen' source folder as there will be no source in there. - if (sourceFolder.exists() && sourceFolder.equals(genFolder) == false) { - IFile sourceFile = sourceFolder.getFile(relative); - SourceFileData data = processor.getFileData(sourceFile); - if (data != null) { - addFileToCompile(sourceFile); - return true; - } - } + relative = relative.append(file.getName().replaceAll( + AdtConstants.RE_DEP_EXT, SdkConstants.DOT_RS)); + + if (!findInSourceFolders(processor, genFolder, relative)) { + // could be a FilterScript file? + relative = file.getFullPath().makeRelativeTo(genFolder.getFullPath()); + // remove the file name segment + relative = relative.removeLastSegments(1); + // add the file name of a FilterScript file. + relative = relative.append(file.getName().replaceAll( + AdtConstants.RE_DEP_EXT, SdkConstants.DOT_FS)); + + return findInSourceFolders(processor, genFolder, relative); } + + return true; } return r; } + private boolean findInSourceFolders(SourceProcessor processor, IFolder genFolder, + IPath relative) { + // now look for a match in the source folders. + List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths( + processor.getJavaProject()); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + + for (IPath sourceFolderPath : sourceFolders) { + IFolder sourceFolder = root.getFolder(sourceFolderPath); + // we don't look in the 'gen' source folder as there will be no source in there. + if (sourceFolder.exists() && sourceFolder.equals(genFolder) == false) { + IFile sourceFile = sourceFolder.getFile(relative); + SourceFileData data = processor.getFileData(sourceFile); + if (data != null) { + addFileToCompile(sourceFile); + return true; + } + } + } + + return false; + } + @Override protected boolean filterResourceFolder(IContainer folder) { return ResourceFolderType.RAW.getName().equals(folder.getName()); } } - public RenderScriptProcessor(IJavaProject javaProject, IFolder genFolder) { - super(javaProject, genFolder, new RsChangeHandler()); + private int mTargetApi = 11; + + public RenderScriptProcessor(@NonNull IJavaProject javaProject, + @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder) { + super(javaProject, buildToolInfo, genFolder, new RsChangeHandler()); + } + + public void setTargetApi(int targetApi) { + // make sure the target api value is good. Must be 11+ or llvm-rs-cc complains. + mTargetApi = targetApi < 11 ? 11 : targetApi; } @Override - protected String getExtension() { - return SdkConstants.EXT_RS; + protected Set<String> getExtensions() { + return EXTENSIONS; } @Override @@ -130,10 +167,9 @@ public class RenderScriptProcessor extends SourceProcessor { return PROPERTY_COMPILE_RS; } - @SuppressWarnings("deprecation") @Override protected void doCompileFiles(List<IFile> sources, BaseBuilder builder, - IProject project, IAndroidTarget projectTarget, int targetApi, + IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException { @@ -146,27 +182,22 @@ public class RenderScriptProcessor extends SourceProcessor { int depIndex; - // make sure the target api value is good. Must be 11+ or llvm-rs-cc complains. - if (targetApi < 11) { - targetApi = 11; - } - // create the command line String[] command = new String[15]; int index = 0; command[index++] = quote(sdkOsPath + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + SdkConstants.FN_RENDERSCRIPT); command[index++] = "-I"; //$NON-NLS-1$ - command[index++] = quote(projectTarget.getPath(IAndroidTarget.ANDROID_RS_CLANG)); + command[index++] = quote(getBuildToolInfo().getPath(BuildToolInfo.PathId.ANDROID_RS_CLANG)); command[index++] = "-I"; //$NON-NLS-1$ - command[index++] = quote(projectTarget.getPath(IAndroidTarget.ANDROID_RS)); + command[index++] = quote(getBuildToolInfo().getPath(BuildToolInfo.PathId.ANDROID_RS)); command[index++] = "-p"; //$NON-NLS-1$ command[index++] = quote(genFolder.getLocation().toOSString()); command[index++] = "-o"; //$NON-NLS-1$ command[index++] = quote(rawFolder.getLocation().toOSString()); command[index++] = "-target-api"; //$NON-NLS-1$ - command[index++] = Integer.toString(targetApi); + command[index++] = Integer.toString(mTargetApi); command[index++] = "-d"; //$NON-NLS-1$ command[depIndex = index++] = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java index 537dd61..5436798 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java @@ -59,7 +59,7 @@ public class SourceChangeHandler { public void handleSourceFile(IFile file, int kind) { // first the file itself if this is a match for the processor's extension - if (mProcessor.getExtension().equals(file.getFileExtension())) { + if (mProcessor.getExtensions().contains(file.getFileExtension())) { if (kind == IResourceDelta.REMOVED) { mRemoved.add(file); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java index 8e50ec6..a82d54d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java @@ -17,9 +17,11 @@ package com.android.ide.eclipse.adt.internal.build; import com.android.SdkConstants; +import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import org.eclipse.core.resources.IFile; @@ -38,6 +40,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -58,6 +61,7 @@ public abstract class SourceProcessor { private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>(); private final IJavaProject mJavaProject; + private BuildToolInfo mBuildToolInfo; private final IFolder mGenFolder; private final SourceChangeHandler mDeltaVisitor; @@ -82,9 +86,11 @@ public abstract class SourceProcessor { return path; } - protected SourceProcessor(IJavaProject javaProject, IFolder genFolder, - SourceChangeHandler deltaVisitor) { + protected SourceProcessor(@NonNull IJavaProject javaProject, + @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder, + @NonNull SourceChangeHandler deltaVisitor) { mJavaProject = javaProject; + mBuildToolInfo = buildToolInfo; mGenFolder = genFolder; mDeltaVisitor = deltaVisitor; @@ -108,8 +114,13 @@ public abstract class SourceProcessor { } } - protected SourceProcessor(IJavaProject javaProject, IFolder genFolder) { - this(javaProject, genFolder, new SourceChangeHandler()); + protected SourceProcessor(@NonNull IJavaProject javaProject, + @NonNull BuildToolInfo buildToolInfo, @NonNull IFolder genFolder) { + this(javaProject, buildToolInfo, genFolder, new SourceChangeHandler()); + } + + public void setBuildToolInfo(BuildToolInfo buildToolInfo) { + mBuildToolInfo = buildToolInfo; } @@ -167,6 +178,10 @@ public abstract class SourceProcessor { return mJavaProject; } + final BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } + final IFolder getGenFolder() { return mGenFolder; } @@ -210,7 +225,7 @@ public abstract class SourceProcessor { * Returns the extension of the source files handled by this processor. * @return */ - protected abstract String getExtension(); + protected abstract Set<String> getExtensions(); protected abstract String getSavePropertyName(); @@ -219,7 +234,7 @@ public abstract class SourceProcessor { * */ public final int compileFiles(BaseBuilder builder, - IProject project, IAndroidTarget projectTarget, int minSdkVersion, + IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException { @@ -241,7 +256,7 @@ public abstract class SourceProcessor { // list of files that have failed compilation. List<IFile> stillNeedCompilation = new ArrayList<IFile>(); - doCompileFiles(mToCompile, builder, project, projectTarget, minSdkVersion, sourceFolders, + doCompileFiles(mToCompile, builder, project, projectTarget, sourceFolders, stillNeedCompilation, libraryProjectsOut, monitor); mToCompile.clear(); @@ -273,7 +288,7 @@ public abstract class SourceProcessor { protected abstract void doCompileFiles( List<IFile> filesToCompile, BaseBuilder builder, - IProject project, IAndroidTarget projectTarget, int targetApi, + IProject project, IAndroidTarget projectTarget, List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException; @@ -365,14 +380,16 @@ public abstract class SourceProcessor { for (IResource r : members) { // get the type of the resource switch (r.getType()) { - case IResource.FILE: + case IResource.FILE: { // if this a file, check that the file actually exist // and that it's the type of of file that's used in this processor - if (r.exists() && - getExtension().equalsIgnoreCase(r.getFileExtension())) { + String extension = r.exists() ? r.getFileExtension() : null; + if (extension != null && + getExtensions().contains(extension.toLowerCase(Locale.US))) { mFiles.put((IFile) r, new SourceFileData((IFile) r)); } break; + } case IResource.FOLDER: // recursively go through children scanFolderForSourceFiles(sourceFolder, (IFolder)r); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties index 70a3ab2..f387ab5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties @@ -14,6 +14,7 @@ Unparsed_AAPT_Errors=Unparsed aapt error(s)\! Check the console for output. Unparsed_AIDL_Errors=Unparsed aidl error\! Check the console for output. AAPT_Exec_Error_s=Error executing aapt. Please check aapt is present at %1$s AAPT_Exec_Error_d=Error executing aapt: Return code %1$d +AAPT_Exec_Error_Other_s=Error executing aapt: %1$s Dalvik_Error_d=Conversion to Dalvik format failed with error %1$d DX_Jar_Error=Dx.jar is not found inside the plugin. Reinstall ADT\! Dalvik_Error_s=Conversion to Dalvik format failed: %1$s diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java index f58af56..1cbf7f2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java @@ -16,19 +16,24 @@ package com.android.ide.eclipse.adt.internal.build.builders; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.sdk.LoadStatus; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.build.BuildHelper; import com.android.ide.eclipse.adt.internal.build.Messages; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler; import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.io.IAbstractFile; import com.android.io.StreamException; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import org.eclipse.core.resources.IContainer; @@ -64,6 +69,13 @@ public abstract class BaseBuilder extends IncrementalProjectBuilder { private SAXParserFactory mParserFactory; /** + * The build tool to use to build. This is guaranteed to be non null after a call to + * {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be + * queried. + */ + protected BuildToolInfo mBuildToolInfo; + + /** * Base Resource Delta Visitor to handle XML error */ protected static class BaseDeltaVisitor implements XmlErrorListener { @@ -293,9 +305,11 @@ public abstract class BaseBuilder extends IncrementalProjectBuilder { * display any errors. * * @param javaProject The {@link IJavaProject} being compiled. + * @param projectState the project state, optional. will be queried if null. * @throws CoreException */ - protected void abortOnBadSetup(IJavaProject javaProject) throws AbortBuildException { + protected void abortOnBadSetup(@NonNull IJavaProject javaProject, + @Nullable ProjectState projectState) throws AbortBuildException, CoreException { IProject iProject = javaProject.getProject(); // check if we have finished loading the project target. Sdk sdk = Sdk.getCurrent(); @@ -303,8 +317,12 @@ public abstract class BaseBuilder extends IncrementalProjectBuilder { throw new AbortBuildException(); } + if (projectState == null) { + projectState = Sdk.getProjectState(javaProject.getProject()); + } + // get the target for the project - IAndroidTarget target = sdk.getTarget(javaProject.getProject()); + IAndroidTarget target = projectState.getTarget(); if (target == null) { throw new AbortBuildException(); @@ -315,6 +333,20 @@ public abstract class BaseBuilder extends IncrementalProjectBuilder { throw new AbortBuildException(); } + mBuildToolInfo = projectState.getBuildToolInfo(); + if (mBuildToolInfo == null) { + mBuildToolInfo = sdk.getLatestBuildTool(); + + if (mBuildToolInfo == null) { + throw new AbortBuildException(); + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject, + String.format("Using default Build Tools revision %s", + mBuildToolInfo.getRevision()) + ); + } + } + // abort if there are TARGET or ADT type markers stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO, false /*checkSeverity*/); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java index 67c7e8a..529dad2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java @@ -111,6 +111,22 @@ class ChangedFileSetHelper { } /** + * Returns a {@link ChangedFileSet} for the generated R.txt file + * @param project the project + * @return a ChangeFileSet + */ + static ChangedFileSet getTextSymbols(@NonNull IProject project) { + // input path is inside the project's android output folder + String path = getRelativeAndroidOut(project); + + ChangedFileSet set = new ChangedFileSet( + "textSymbols", //$NON-NLS-1$ + path + '/' + SdkConstants.FN_RESOURCE_TEXT); + + return set; + } + + /** * Returns a {@link ChangedFileSet} for a project's javac output. * @param project the project * @return a ChangedFileSet diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java index e5b5a43..093072b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java @@ -17,6 +17,8 @@ package com.android.ide.eclipse.adt.internal.build.builders; import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AndroidPrintStream; @@ -314,14 +316,15 @@ public class PostCompilerBuilder extends BaseBuilder { IJavaProject referencedJavaProject = referencedJavaProjects.get(i); delta = getDelta(referencedJavaProject.getProject()); if (delta != null) { + IProject referencedProject = referencedJavaProject.getProject(); PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor( - project, referencedJavaProject.getProject(), + project, referencedProject, "POST:RefedProject"); - ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(project); + ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(referencedProject); visitor.addSet(javaResCfs); - ChangedFileSet bytecodeCfs = ChangedFileSetHelper.getByteCodeCfs(project); + ChangedFileSet bytecodeCfs = ChangedFileSetHelper.getByteCodeCfs(referencedProject); visitor.addSet(bytecodeCfs); delta.accept(visitor); @@ -340,18 +343,20 @@ public class PostCompilerBuilder extends BaseBuilder { // Top level check to make sure the build can move forward. Only do this after recording // delta changes. - abortOnBadSetup(javaProject); + abortOnBadSetup(javaProject, projectState); + + // Get the output stream. Since the builder is created for the life of the + // project, they can be kept around. + if (mOutStream == null) { + mOutStream = new AndroidPrintStream(project, null /*prefix*/, + AdtPlugin.getOutStream()); + mErrStream = new AndroidPrintStream(project, null /*prefix*/, + AdtPlugin.getOutStream()); + } // remove older packaging markers. removeMarkersFromContainer(javaProject.getProject(), AdtConstants.MARKER_PACKAGING); - if (androidOutputFolder == null) { - // mark project and exit - markProject(AdtConstants.MARKER_PACKAGING, Messages.Failed_To_Get_Output, - IMarker.SEVERITY_ERROR); - return allRefProjects; - } - // finished with the common init and tests. Special case of the library. if (isLibrary) { // check the jar output file is present, if not create it. @@ -366,8 +371,10 @@ public class PostCompilerBuilder extends BaseBuilder { if (DEBUG_LOG) { AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName()); } - BuildHelper helper = new BuildHelper(project, + BuildHelper helper = new BuildHelper(project, mBuildToolInfo, mOutStream, mErrStream, + false /*jumbo mode doesn't matter here*/, + false /*dex merger doesn't matter here*/, true /*debugMode*/, AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE, mResourceMarker); @@ -468,20 +475,21 @@ public class PostCompilerBuilder extends BaseBuilder { ic.refreshLocal(IResource.DEPTH_ONE, monitor); } - // Get the DX output stream. Since the builder is created for the life of the - // project, they can be kept around. - if (mOutStream == null) { - mOutStream = new AndroidPrintStream(project, null /*prefix*/, - AdtPlugin.getOutStream()); - mErrStream = new AndroidPrintStream(project, null /*prefix*/, - AdtPlugin.getOutStream()); - } - // we need to test all three, as we may need to make the final package // but not the intermediary ones. if (mPackageResources || mConvertToDex || mBuildFinalPackage) { - BuildHelper helper = new BuildHelper(project, + String forceJumboStr = projectState.getProperty( + AdtConstants.DEX_OPTIONS_FORCEJUMBO); + Boolean jumbo = Boolean.valueOf(forceJumboStr); + + String dexMergerStr = projectState.getProperty( + AdtConstants.DEX_OPTIONS_DISABLE_MERGER); + Boolean dexMerger = Boolean.valueOf(dexMergerStr); + + BuildHelper helper = new BuildHelper(project, mBuildToolInfo, mOutStream, mErrStream, + jumbo.booleanValue(), + dexMerger.booleanValue(), true /*debugMode*/, AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE, mResourceMarker); @@ -635,6 +643,8 @@ public class PostCompilerBuilder extends BaseBuilder { Messages.ApkBuilder_Update_or_Execute_manually_s, e.getCommandLine()); + AdtPlugin.log(e, msg); + return allRefProjects; } catch (ApkCreationException e) { String eMessage = e.getMessage(); @@ -643,6 +653,8 @@ public class PostCompilerBuilder extends BaseBuilder { String msg = String.format(Messages.Final_Archive_Error_s, eMessage); BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + + AdtPlugin.log(e, msg); } catch (AndroidLocationException e) { String eMessage = e.getMessage(); @@ -650,6 +662,7 @@ public class PostCompilerBuilder extends BaseBuilder { String msg = String.format(Messages.Final_Archive_Error_s, eMessage); BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + AdtPlugin.log(e, msg); } catch (NativeLibInJarException e) { String msg = e.getMessage(); @@ -663,6 +676,7 @@ public class PostCompilerBuilder extends BaseBuilder { AdtPlugin.printErrorToConsole(project, msg); BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); + AdtPlugin.log(e, msg); } catch (DuplicateFileException e) { String msg1 = String.format( "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", @@ -901,8 +915,10 @@ public class PostCompilerBuilder extends BaseBuilder { } @Override - protected void abortOnBadSetup(IJavaProject javaProject) throws AbortBuildException { - super.abortOnBadSetup(javaProject); + protected void abortOnBadSetup( + @NonNull IJavaProject javaProject, + @Nullable ProjectState projectState) throws AbortBuildException, CoreException { + super.abortOnBadSetup(javaProject, projectState); IProject iProject = getProject(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java index 1507a8d..ae6b3f5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.build.builders; import com.android.SdkConstants; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.common.xml.ManifestData; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; @@ -46,6 +47,7 @@ import com.android.io.StreamException; import com.android.manifmerger.ManifestMerger; import com.android.manifmerger.MergerLog; import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.internal.build.BuildConfigGenerator; import com.android.sdklib.internal.build.SymbolLoader; @@ -55,7 +57,9 @@ import com.android.sdklib.io.FileOp; import com.android.utils.ILogger; import com.android.utils.Pair; import com.android.xml.AndroidManifest; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; @@ -76,6 +80,7 @@ import org.xml.sax.SAXException; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -139,6 +144,9 @@ public class PreCompilerBuilder extends BaseBuilder { */ private DerivedProgressMonitor mDerivedProgressMonitor; + private AidlProcessor mAidlProcessor; + private RenderScriptProcessor mRenderScriptProcessor; + /** * Progress monitor waiting the end of the process to set a persistent value * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>, @@ -258,6 +266,8 @@ public class PreCompilerBuilder extends BaseBuilder { return null; } + boolean isLibrary = projectState.isLibrary(); + IAndroidTarget projectTarget = projectState.getTarget(); // get the libraries @@ -267,7 +277,9 @@ public class PreCompilerBuilder extends BaseBuilder { IJavaProject javaProject = JavaCore.create(project); // Top level check to make sure the build can move forward. - abortOnBadSetup(javaProject); + abortOnBadSetup(javaProject, projectState); + + setupSourceProcessors(javaProject, projectState); // now we need to get the classpath list List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(javaProject); @@ -321,17 +333,22 @@ public class PreCompilerBuilder extends BaseBuilder { // Notify the ResourceManager: ResourceManager resManager = ResourceManager.getInstance(); - ProjectResources projectResources = resManager.getProjectResources(project); if (ResourceManager.isAutoBuilding()) { + ProjectResources projectResources = resManager.getProjectResources(project); + IdeScanningContext context = new IdeScanningContext(projectResources, project, true); - resManager.processDelta(delta, context); + boolean wasCleared = projectResources.ensureInitialized(); + + if (!wasCleared) { + resManager.processDelta(delta, context); + } // Check whether this project or its dependencies (libraries) have // resources that need compilation - if (context.needsFullAapt()) { + if (wasCleared || context.needsFullAapt()) { mMustCompileResources = true; // Must also call markAaptRequested on the project to not just @@ -362,7 +379,7 @@ public class PreCompilerBuilder extends BaseBuilder { // if the main manifest didn't change, then we check for the library // ones (will trigger manifest merging too) - if (mMustMergeManifest == false && libProjects.size() > 0) { + if (libProjects.size() > 0) { for (IProject libProject : libProjects) { IResourceDelta delta = getDelta(libProject); if (delta != null) { @@ -371,12 +388,24 @@ public class PreCompilerBuilder extends BaseBuilder { "PRE:LibManifest"); //$NON-NLS-1$ visitor.addSet(ChangedFileSetHelper.MANIFEST); + ChangedFileSet textSymbolCFS = null; + if (isLibrary == false) { + textSymbolCFS = ChangedFileSetHelper.getTextSymbols( + libProject); + visitor.addSet(textSymbolCFS); + } + delta.accept(visitor); mMustMergeManifest |= visitor.checkSet(ChangedFileSetHelper.MANIFEST); - // no need to test others. - if (mMustMergeManifest) { + if (textSymbolCFS != null) { + mMustCompileResources |= visitor.checkSet(textSymbolCFS); + } + + // no need to test others if we have all flags at true. + if (mMustMergeManifest && + (mMustCompileResources || textSymbolCFS == null)) { break; } } @@ -631,10 +660,27 @@ public class PreCompilerBuilder extends BaseBuilder { // run the source processors int processorStatus = SourceProcessor.COMPILE_STATUS_NONE; + + // get the renderscript target + int rsTarget = minSdkValue == -1 ? 11 : minSdkValue; + String rsTargetStr = projectState.getProperty(ProjectProperties.PROPERTY_RS_TARGET); + if (rsTargetStr != null) { + try { + rsTarget = Integer.parseInt(rsTargetStr); + } catch (NumberFormatException e) { + handleException(e, String.format( + "Property %s is not an integer.", + ProjectProperties.PROPERTY_RS_TARGET)); + return result; + } + } + + mRenderScriptProcessor.setTargetApi(rsTarget); + for (SourceProcessor processor : mProcessors) { try { processorStatus |= processor.compileFiles(this, - project, projectTarget, minSdkValue, sourceFolderPathList, + project, projectTarget, sourceFolderPathList, libProjectsOut, monitor); } catch (Throwable t) { handleException(t, String.format( @@ -664,8 +710,8 @@ public class PreCompilerBuilder extends BaseBuilder { proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD); } - handleResources(project, javaPackage, projectTarget, manifestFile, libProjects, - projectState.isLibrary(), proguardFile); + handleResources(project, javaPackage, projectTarget, manifestFile, + libProjects, isLibrary, proguardFile); } if (processorStatus == SourceProcessor.COMPILE_STATUS_NONE && @@ -721,6 +767,10 @@ public class PreCompilerBuilder extends BaseBuilder { // Also clean up lint EclipseLintClient.clearMarkers(project); + + // clean the project repo + ProjectResources res = ResourceManager.getInstance().getProjectResources(project); + res.clear(); } @Override @@ -749,19 +799,26 @@ public class PreCompilerBuilder extends BaseBuilder { mLastBuildConfigMode = v; } - IJavaProject javaProject = JavaCore.create(project); - - // load the source processors - SourceProcessor aidlProcessor = new AidlProcessor(javaProject, mGenFolder); - SourceProcessor renderScriptProcessor = new RenderScriptProcessor(javaProject, - mGenFolder); - mProcessors.add(aidlProcessor); - mProcessors.add(renderScriptProcessor); } catch (Throwable throwable) { AdtPlugin.log(throwable, "Failed to finish PrecompilerBuilder#startupOnInitialize()"); } } + private void setupSourceProcessors(@NonNull IJavaProject javaProject, + @NonNull ProjectState projectState) { + if (mAidlProcessor == null) { + mAidlProcessor = new AidlProcessor(javaProject, mBuildToolInfo, mGenFolder); + mRenderScriptProcessor = new RenderScriptProcessor(javaProject, mBuildToolInfo, + mGenFolder); + mProcessors.add(mAidlProcessor); + mProcessors.add(mRenderScriptProcessor); + } else { + mAidlProcessor.setBuildToolInfo(mBuildToolInfo); + mRenderScriptProcessor.setBuildToolInfo(mBuildToolInfo); + } + } + + @SuppressWarnings("deprecation") private void handleBuildConfig(@SuppressWarnings("rawtypes") Map args) throws IOException, CoreException { boolean debugMode = !args.containsKey(RELEASE_REQUESTED); @@ -851,7 +908,8 @@ public class PreCompilerBuilder extends BaseBuilder { } @Override - public void error(Throwable t, String errorFormat, Object... args) { + public void error(@Nullable Throwable t, @Nullable String errorFormat, + Object... args) { errors.add(String.format(errorFormat, args)); } }), @@ -867,7 +925,8 @@ public class PreCompilerBuilder extends BaseBuilder { if (merger.process( outFile.getLocation().toFile(), manifest.getLocation().toFile(), - libManifests) == false) { + libManifests, + null /*injectAttributes*/, null /*packageOverride*/) == false) { if (errors.size() > 1) { StringBuilder sb = new StringBuilder(); for (String s : errors) { @@ -989,6 +1048,7 @@ public class PreCompilerBuilder extends BaseBuilder { * @param proguardFile an optional path to store proguard information * @throws AbortBuildException */ + @SuppressWarnings("deprecation") private void execAapt(IProject project, IAndroidTarget projectTarget, String osOutputPath, String osResPath, String osManifestPath, IFolder packageFolder, ArrayList<IFolder> libResFolders, List<Pair<File, String>> libRFiles, @@ -1004,8 +1064,7 @@ public class PreCompilerBuilder extends BaseBuilder { // launch aapt: create the command line ArrayList<String> array = new ArrayList<String>(); - @SuppressWarnings("deprecation") - String aaptPath = projectTarget.getPath(IAndroidTarget.AAPT); + String aaptPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT); array.add(aaptPath); array.add("package"); //$NON-NLS-1$ @@ -1109,24 +1168,65 @@ public class PreCompilerBuilder extends BaseBuilder { } // now if the project has libraries, R needs to be created for each libraries - if (!libRFiles.isEmpty()) { - SymbolLoader symbolValues = new SymbolLoader(new File(outputFolder, "R.txt")); - symbolValues.load(); + // unless this is a library. + if (isLibrary == false && !libRFiles.isEmpty()) { + File rFile = new File(outputFolder, SdkConstants.FN_RESOURCE_TEXT); + // if the project has no resources, the file could not exist. + if (rFile.isFile()) { + // Load the full symbols from the full R.txt file. + SymbolLoader fullSymbolValues = new SymbolLoader(rFile); + fullSymbolValues.load(); + + Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create(); + + // First pass processing the libraries, collecting them by packageName, + // and ignoring the ones that have the same package name as the application + // (since that R class was already created). + + for (Pair<File, String> lib : libRFiles) { + String libPackage = lib.getSecond(); + File rText = lib.getFirst(); + + if (rText.isFile()) { + // load the lib symbols + SymbolLoader libSymbols = new SymbolLoader(rText); + libSymbols.load(); + + // store these symbols by associating them with the package name. + libMap.put(libPackage, libSymbols); + } + } - for (Pair<File, String> libData : libRFiles) { - SymbolLoader symbols = new SymbolLoader(libData.getFirst()); - symbols.load(); + // now loop on all the package names, merge all the symbols to write, + // and write them + for (String packageName : libMap.keySet()) { + Collection<SymbolLoader> symbols = libMap.get(packageName); - SymbolWriter writer = new SymbolWriter(osOutputPath, libData.getSecond(), - symbols, symbolValues); - writer.write(); + SymbolWriter writer = new SymbolWriter(osOutputPath, packageName, + fullSymbolValues); + for (SymbolLoader symbolLoader : symbols) { + writer.addSymbolsToWrite(symbolLoader); + } + writer.write(); + } } } } catch (IOException e1) { // something happen while executing the process, // mark the project and exit - String msg = String.format(Messages.AAPT_Exec_Error_s, array.get(0)); + String msg; + String path = array.get(0); + if (!new File(path).exists()) { + msg = String.format(Messages.AAPT_Exec_Error_s, path); + } else { + String description = e1.getLocalizedMessage(); + if (e1.getCause() != null && e1.getCause() != e1) { + description = description + ": " + e1.getCause().getLocalizedMessage(); + } + msg = String.format(Messages.AAPT_Exec_Error_Other_s, description); + } + markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR); // Add workaround for the Linux problem described here: diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java index 39f7d1f..770710d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java @@ -24,8 +24,6 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; -import com.android.ide.eclipse.adt.internal.sdk.Sdk; -import com.android.sdklib.IAndroidTarget; import com.android.utils.Pair; import org.eclipse.core.resources.IFolder; @@ -87,7 +85,7 @@ public class ResourceManagerBuilder extends BaseBuilder { // check for existing target marker, in which case we abort. // (this means: no SDK, no target, or unresolvable target.) try { - abortOnBadSetup(javaProject); + abortOnBadSetup(javaProject, null); } catch (AbortBuildException e) { return null; } @@ -129,13 +127,6 @@ public class ResourceManagerBuilder extends BaseBuilder { return null; } - // check the project has a target - IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); - if (projectTarget == null) { - // no target. marker has been set by the container initializer: exit silently. - return null; - } - // check the 'gen' source folder is present boolean hasGenSrcFolder = false; // whether the project has a 'gen' source folder setup diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java index b39c4cb..5aac51f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java @@ -16,8 +16,11 @@ package com.android.ide.eclipse.adt.internal.editors; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.PREFIX_ANDROID; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; import static com.android.SdkConstants.UNIT_DP; import static com.android.SdkConstants.UNIT_IN; import static com.android.SdkConstants.UNIT_MM; @@ -44,6 +47,9 @@ import com.android.utils.Pair; import com.android.utils.XmlUtils; import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.ui.ISharedImages; +import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; @@ -224,8 +230,10 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { // or their values). if (info.isInValue) { - computeAttributeValues(proposals, offset, parent, info.name, currentNode, - wordPrefix, info.skipEndTag, info.replaceLength); + if (computeAttributeValues(proposals, offset, parent, info.name, currentNode, + wordPrefix, info.skipEndTag, info.replaceLength)) { + return; + } } // Look up attribute proposals based on descriptors @@ -461,14 +469,24 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { choices = UiResourceAttributeNode.computeResourceStringMatches( mEditor, attributeDescriptor, value); attrInfo.skipEndTag = false; + } else if (value.startsWith(PREFIX_THEME_REF) + && !attributeInfo.getFormats().contains(Format.REFERENCE)) { + choices = UiResourceAttributeNode.computeResourceStringMatches( + mEditor, attributeDescriptor, value); + attrInfo.skipEndTag = false; } return choices; } - protected void computeAttributeValues(List<ICompletionProposal> proposals, int offset, + /** + * Compute attribute values. Return true if the complete set of values was + * added, so addition descriptor information should not be added. + */ + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, String parentTagName, String attributeName, Node node, String wordPrefix, boolean skipEndTag, int replaceLength) { + return false; } protected void computeTextValues(List<ICompletionProposal> proposals, int offset, @@ -502,8 +520,8 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { * * @return An ElementDescriptor[] or null. */ - private Object[] getElementChoicesForTextNode(Node parentNode) { - Object[] choices = null; + protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) { + ElementDescriptor[] choices = null; String parent; if (parentNode.getNodeType() == Node.ELEMENT_NODE) { // We're editing a text node which parent is an element node. Limit @@ -549,10 +567,12 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { } Map<String, String> nsUriMap = new HashMap<String, String>(); + boolean haveLayoutParams = false; for (Object choice : choices) { String keyword = null; String nsPrefix = null; + String nsUri = null; Image icon = null; String tooltip = null; if (choice instanceof ElementDescriptor) { @@ -570,11 +590,11 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { // Get the namespace URI for the attribute. Note that some attributes // do not have a namespace and thus return null here. - String nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); + nsUri = ((AttributeDescriptor)choice).getNamespaceUri(); if (nsUri != null) { nsPrefix = nsUriMap.get(nsUri); if (nsPrefix == null) { - nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri); + nsPrefix = XmlUtils.lookupNamespacePrefix(currentNode, nsUri, false); nsUriMap.put(nsUri, nsPrefix); } } @@ -595,6 +615,10 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { if (isAttribute) { icon = IconFactory.getInstance().getIcon(ATTRIBUTE_ICON_FILENAME); } + } else if (choice instanceof IType) { + IType type = (IType) choice; + keyword = type.getFullyQualifiedName(); + icon = JavaUI.getSharedImages().getImage(ISharedImages.IMG_OBJS_CUNIT); } else { continue; // discard unknown choice } @@ -646,6 +670,11 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { cursorPosition++; } + if (nsPrefix != null && + keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length())) { + haveLayoutParams = true; + } + // For attributes, automatically insert ns:attribute="" and place the cursor // inside the quotes. // Special case for attributes: insert ="" stuff and locate caret inside "" @@ -659,10 +688,42 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor { icon, // Image image displayString, // displayString null, // IContextInformation contextInformation - tooltip // String additionalProposalInfo + tooltip, // String additionalProposalInfo + nsPrefix, + nsUri )); } } + + if (wordPrefix.length() > 0 && haveLayoutParams + && !wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { + // Sort layout parameters to the front if we automatically inserted some + // that you didn't request. For example, you typed "width" and we match both + // "width" and "layout_width" - should match layout_width. + String nsPrefix = nsUriMap.get(ANDROID_URI); + if (nsPrefix == null) { + nsPrefix = PREFIX_ANDROID; + } else { + nsPrefix += ':'; + } + if (!(wordPrefix.startsWith(nsPrefix) + && wordPrefix.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()))) { + int nextLayoutIndex = 0; + for (int i = 0, n = proposals.size(); i < n; i++) { + ICompletionProposal proposal = proposals.get(i); + String keyword = proposal.getDisplayString(); + if (keyword.startsWith(nsPrefix) && + keyword.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX, nsPrefix.length()) + && i != nextLayoutIndex) { + // Swap to front + ICompletionProposal temp = proposals.get(nextLayoutIndex); + proposals.set(nextLayoutIndex, proposal); + proposals.set(i, temp); + nextLayoutIndex++; + } + } + } + } } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java new file mode 100644 index 0000000..8716487 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidDoubleClickStrategy.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 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; + +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; +import org.eclipse.wst.xml.ui.internal.doubleclick.XMLDoubleClickStrategy; + +/** + * Custom version of {@link XMLDoubleClickStrategy} which is smarter about + * selecting portions of resource references, etc. + */ +@SuppressWarnings("restriction") // XML API +public class AndroidDoubleClickStrategy extends XMLDoubleClickStrategy { + /** + * Creates a new {@linkplain AndroidDoubleClickStrategy} + */ + public AndroidDoubleClickStrategy() { + } + + @Override + protected void processElementDoubleClicked() { + // Special case: if you click on the local name portion of an attribute pair, + // select only the local name. For example, if you click anywhere in the "text" region + // of "android:text", select just the "text" portion. + if (fTextRegion.getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME) { + String regionText = fStructuredDocumentRegion.getText(fTextRegion); + int cursor = fCaretPosition - fStructuredDocumentRegion.getStartOffset(fTextRegion); + int ns = regionText.indexOf(':'); + if (cursor > ns) { + int start = ns + 1; + fTextViewer.setSelectedRange(fStructuredDocumentRegion.getStartOffset(fTextRegion) + + start, fTextRegion.getTextLength() - start); + return; + } + } + + super.processElementDoubleClicked(); + } + + @Override + protected Point getWord(String string, int cursor) { + if (string == null) { + return null; + } + + // Default implementation will strip off the surrounding quotes etc: + Point position = super.getWord(string, cursor); + + assert cursor >= position.x && cursor <= position.y; + + // Special case: when you click on a resource identifier name, only select the + // name portion + if (string.startsWith(PREFIX_RESOURCE_REF, position.x) || + string.startsWith(PREFIX_THEME_REF, position.x)) { + int nameStart = string.indexOf('/', position.x + 1); + if (nameStart != -1 && nameStart < cursor) { + position.x = nameStart + 1; + return position; + } + } + + // Special case: when you have a dotted name, such as com.android.tools.MyClass, + // and you click on the last part, select only that part + int lastDot = string.lastIndexOf('.', cursor); + if (lastDot >= position.x && lastDot < position.y - 1) { + int next = string.indexOf('.', cursor); + if (next == -1 || next > position.y) { + position.x = lastDot + 1; + } + } + + return position; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java index c5aa8cd..8a078ef 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlAutoEditStrategy.java @@ -24,7 +24,7 @@ import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; import com.android.utils.Pair; import org.eclipse.jface.text.BadLocationException; @@ -148,7 +148,7 @@ public class AndroidXmlAutoEditStrategy implements IAutoEditStrategy { StringBuilder sb = new StringBuilder(c.text); sb.append(lineIndent); - String oneIndentUnit = XmlFormatPreferences.create().getOneIndentUnit(); + String oneIndentUnit = EclipseXmlFormatPreferences.create().getOneIndentUnit(); if (addIndent) { sb.append(oneIndentUnit); } @@ -276,7 +276,7 @@ public class AndroidXmlAutoEditStrategy implements IAutoEditStrategy { } if (addIndent) { - sb.append(XmlFormatPreferences.create() + sb.append(EclipseXmlFormatPreferences.create() .getOneIndentUnit()); } c.text = sb.toString(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java index fcfdfd1..1d4e133 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java @@ -25,6 +25,7 @@ import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; @@ -41,6 +42,8 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.ui.actions.IJavaEditorActionDefinitionIds; +import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.text.BadLocationException; @@ -802,7 +805,16 @@ public abstract class AndroidXmlEditor extends FormEditor { */ private void createTextEditor() { try { - mTextEditor = new StructuredTextEditor(); + mTextEditor = new StructuredTextEditor() { + @Override + protected void createActions() { + super.createActions(); + + Action action = new RenameResourceXmlTextAction(mTextEditor); + action.setActionDefinitionId(IJavaEditorActionDefinitionIds.RENAME_ELEMENT); + setAction(IJavaEditorActionDefinitionIds.RENAME_ELEMENT, action); + } + }; int index = addPage(mTextEditor, getEditorInput()); mTextPageIndex = index; setPageText(index, mTextEditor.getTitle()); @@ -1058,7 +1070,12 @@ public abstract class AndroidXmlEditor extends FormEditor { end = begin + 1; } - reformatRegion(begin, end); + if (mFormatChildren + && node == node.getOwnerDocument().getDocumentElement()) { + reformatDocument(); + } else { + reformatRegion(begin, end); + } } } mFormatNode = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java index 74b7dd8..2d44677 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/CompletionProposal.java @@ -15,20 +15,34 @@ */ package com.android.ide.eclipse.adt.internal.editors; +import static com.android.SdkConstants.XMLNS; + import com.android.ide.common.api.IAttributeInfo; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.IDescriptorProvider; import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.utils.XmlUtils; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ISourceRange; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,18 +68,21 @@ class CompletionProposal implements ICompletionProposal { private final AndroidContentAssist mAssist; private final Object mChoice; private final int mCursorPosition; - private final int mReplacementOffset; + private int mReplacementOffset; private final int mReplacementLength; private final String mReplacementString; private final Image mImage; private final String mDisplayString; private final IContextInformation mContextInformation; + private final String mNsPrefix; + private final String mNsUri; private String mAdditionalProposalInfo; CompletionProposal(AndroidContentAssist assist, Object choice, String replacementString, int replacementOffset, int replacementLength, int cursorPosition, Image image, String displayString, - IContextInformation contextInformation, String additionalProposalInfo) { + IContextInformation contextInformation, String additionalProposalInfo, + String nsPrefix, String nsUri) { assert replacementString != null; assert replacementOffset >= 0; assert replacementLength >= 0; @@ -81,6 +98,8 @@ class CompletionProposal implements ICompletionProposal { mDisplayString = displayString; mContextInformation = contextInformation; mAdditionalProposalInfo = additionalProposalInfo; + mNsPrefix = nsPrefix; + mNsUri = nsUri; } @Override @@ -142,6 +161,24 @@ class CompletionProposal implements ICompletionProposal { } } + } else if (mChoice instanceof IType) { + IType type = (IType) mChoice; + try { + ISourceRange javadocRange = type.getJavadocRange(); + if (javadocRange != null && javadocRange.getLength() > 0) { + ISourceRange sourceRange = type.getSourceRange(); + if (sourceRange != null) { + String source = type.getSource(); + int start = javadocRange.getOffset() - sourceRange.getOffset(); + int length = javadocRange.getLength(); + String doc = source.substring(start, start + length); + return doc; + } + } + return type.getAttachedJavadoc(new NullProgressMonitor()); + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + } } } @@ -151,6 +188,39 @@ class CompletionProposal implements ICompletionProposal { @Override public void apply(IDocument document) { try { + Position position = new Position(mReplacementOffset); + document.addPosition(position); + + // Ensure that the namespace is defined in the document + String prefix = mNsPrefix; + if (mNsUri != null && prefix != null) { + Document dom = DomUtilities.getDocument(mAssist.getEditor()); + if (dom != null) { + Element root = dom.getDocumentElement(); + if (root != null) { + // Is the namespace already defined? + boolean found = false; + NamedNodeMap attributes = root.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + String name = attribute.getName(); + if (name.startsWith(XMLNS) && mNsUri.equals(attribute.getValue())) { + found = true; + break; + } + } + if (!found) { + if (prefix.endsWith(":")) { //$NON-NLS-1$ + prefix = prefix.substring(0, prefix.length() - 1); + } + XmlUtils.lookupNamespacePrefix(root, mNsUri, prefix, true); + } + } + } + } + + mReplacementOffset = position.getOffset(); + document.removePosition(position); document.replace(mReplacementOffset, mReplacementLength, mReplacementString); } catch (BadLocationException x) { // ignore diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java index 18135aa..0e46255 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/Hyperlinks.java @@ -22,6 +22,8 @@ import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_ON_CLICK; import static com.android.SdkConstants.CLASS_ACTIVITY; @@ -36,8 +38,10 @@ import static com.android.SdkConstants.PREFIX_THEME_REF; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.TAG_RESOURCES; import static com.android.SdkConstants.TAG_STYLE; +import static com.android.SdkConstants.TOOLS_URI; import static com.android.SdkConstants.VIEW; import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.ide.common.resources.ResourceRepository.parseResource; import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME; import static com.android.xml.AndroidManifest.ATTRIBUTE_PACKAGE; import static com.android.xml.AndroidManifest.NODE_ACTIVITY; @@ -56,11 +60,13 @@ import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestEditor; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.ide.eclipse.adt.io.IFolderWrapper; @@ -217,7 +223,7 @@ public class Hyperlinks { // It's a value -declaration-, nowhere else to jump // (though we could consider jumping to the R-file; would that // be helpful?) - return false; + return !ATTR_ID.equals(attribute.getLocalName()); } Pair<ResourceType,String> resource = parseResource(value); @@ -342,7 +348,9 @@ public class Hyperlinks { String tag = context.getElement().getTagName(); String attributeName = attribute.getLocalName(); return ATTR_CLASS.equals(attributeName) && (VIEW.equals(tag) || VIEW_FRAGMENT.equals(tag)) - || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag); + || ATTR_NAME.equals(attributeName) && VIEW_FRAGMENT.equals(tag) + || (ATTR_CONTEXT.equals(attributeName) + && TOOLS_URI.equals(attribute.getNamespaceURI())); } /** Returns true if this represents an onClick attribute specifying a method handler */ @@ -369,7 +377,18 @@ public class Hyperlinks { /** Returns the FQCN for a class declaration at the given context */ private static String getClassFqcn(XmlContext context) { if (isClassAttribute(context)) { - return context.getAttribute().getValue(); + String value = context.getAttribute().getValue(); + if (!value.isEmpty() && value.charAt(0) == '.') { + IProject project = getProject(); + if (project != null) { + ManifestInfo info = ManifestInfo.get(project); + String pkg = info.getPackage(); + if (pkg != null) { + value = pkg + value; + } + } + } + return value; } else if (isClassElement(context)) { return context.getElement().getTagName(); } @@ -896,24 +915,50 @@ public class Hyperlinks { String targetTag = getTagName(type); Element root = document.getDocumentElement(); if (root.getTagName().equals(TAG_RESOURCES)) { - NodeList children = root.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element)child; - if (element.getTagName().equals(targetTag)) { - String elementName = element.getAttribute(ATTR_NAME); - if (elementName.equals(name)) { - IRegion region = null; - if (element instanceof IndexedRegion) { - IndexedRegion r = (IndexedRegion) element; - // IndexedRegion.getLength() returns bogus values - int length = r.getEndOffset() - r.getStartOffset(); - region = new Region(r.getStartOffset(), length); + NodeList topLevel = root.getChildNodes(); + Pair<IFile, IRegion> value = findValueInChildren(name, file, targetTag, topLevel); + if (value == null && type == ResourceType.ATTR) { + for (int i = 0, n = topLevel.getLength(); i < n; i++) { + Node child = topLevel.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + String tagName = element.getTagName(); + if (tagName.equals("declare-styleable")) { + NodeList children = element.getChildNodes(); + value = findValueInChildren(name, file, targetTag, children); + if (value != null) { + return value; } + } + } + } + } - return Pair.of(file, region); + return value; + } + + return null; + } + + private static Pair<IFile, IRegion> findValueInChildren(String name, IFile file, + String targetTag, NodeList children) { + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element)child; + String tagName = element.getTagName(); + if (tagName.equals(targetTag)) { + String elementName = element.getAttribute(ATTR_NAME); + if (elementName.equals(name)) { + IRegion region = null; + if (element instanceof IndexedRegion) { + IndexedRegion r = (IndexedRegion) element; + // IndexedRegion.getLength() returns bogus values + int length = r.getEndOffset() - r.getStartOffset(); + region = new Region(r.getStartOffset(), length); } + + return Pair.of(file, region); } } } @@ -957,16 +1002,25 @@ public class Hyperlinks { private static Pair<IFile, IRegion> findIdInDocument(String id, IFile file, Document document) { String targetAttribute = NEW_ID_PREFIX + id; - return findIdInElement(document.getDocumentElement(), file, targetAttribute); + Element root = document.getDocumentElement(); + Pair<IFile, IRegion> result = findIdInElement(root, file, targetAttribute, + true /*requireId*/); + if (result == null) { + result = findIdInElement(root, file, targetAttribute, false /*requireId*/); + } + return result; } private static Pair<IFile, IRegion> findIdInElement( - Element root, IFile file, String targetAttribute) { + Element root, IFile file, String targetAttribute, boolean requireIdAttribute) { NamedNodeMap attributes = root.getAttributes(); for (int i = 0, n = attributes.getLength(); i < n; i++) { Node item = attributes.item(i); if (item instanceof Attr) { - Attr attribute = (Attr)item; + Attr attribute = (Attr) item; + if (requireIdAttribute && !ATTR_ID.equals(attribute.getLocalName())) { + continue; + } String value = attribute.getValue(); if (value.equals(targetAttribute)) { // Select the element -containing- the id rather than the attribute itself @@ -989,7 +1043,8 @@ public class Hyperlinks { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element)child; - Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute); + Pair<IFile, IRegion> result = findIdInElement(element, file, targetAttribute, + requireIdAttribute); if (result != null) { return result; } @@ -1097,16 +1152,6 @@ public class Hyperlinks { return getResourceLinks(range, url, project, configuration); } - /** Parse a resource reference or a theme reference and return the individual parts */ - private static Pair<ResourceType,String> parseResource(String url) { - if (url.startsWith(PREFIX_THEME_REF)) { - url = PREFIX_RESOURCE_REF + url.substring(PREFIX_THEME_REF.length()); - return ResourceHelper.parseResource(url); - } - - return ResourceHelper.parseResource(url); - } - /** * Computes hyperlinks to resource definitions for resource urls (e.g. * {@code @android:string/ok} or {@code @layout/foo}. May create multiple links. @@ -1141,6 +1186,22 @@ public class Hyperlinks { } List<ResourceFile> sourceFiles = resources.getSourceFiles(type, name, null /*configuration*/); + if (sourceFiles == null) { + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null) { + List<IProject> libraries = projectState.getFullLibraryProjects(); + if (libraries != null && !libraries.isEmpty()) { + for (IProject library : libraries) { + resources = ResourceManager.getInstance().getProjectResources(library); + sourceFiles = resources.getSourceFiles(type, name, null /*configuration*/); + if (sourceFiles != null && !sourceFiles.isEmpty()) { + break; + } + } + } + } + } + ResourceFile best = null; if (configuration != null && sourceFiles != null && sourceFiles.size() > 0) { List<ResourceFile> bestFiles = resources.getSourceFiles(type, name, configuration); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java index 1d27e33..bb5d1ba 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/OutlineLabelProvider.java @@ -24,6 +24,8 @@ import static com.android.SdkConstants.ATTR_SRC; import static com.android.SdkConstants.ATTR_TEXT; import static com.android.SdkConstants.DRAWABLE_PREFIX; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.VIEW; +import static com.android.SdkConstants.VIEW_TAG; import org.eclipse.swt.graphics.Image; import org.eclipse.wst.xml.ui.internal.contentoutline.JFaceNodeLabelProvider; @@ -43,6 +45,11 @@ class OutlineLabelProvider extends JFaceNodeLabelProvider { if (element instanceof Element) { Element e = (Element) element; String tagName = e.getTagName(); + if (VIEW_TAG.equals(tagName)) { + // Can't have both view.png and View.png; issues on case sensitive vs + // case insensitive file systems + tagName = VIEW; + } IconFactory factory = IconFactory.getInstance(); Image img = factory.getIcon(tagName, null); if (img != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java index 777cf1d..8a4cf23 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/animator/AnimationContentAssist.java @@ -70,7 +70,7 @@ public final class AnimationContentAssist extends AndroidContentAssist { } @Override - protected void computeAttributeValues(List<ICompletionProposal> proposals, int offset, + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, String parentTagName, String attributeName, Node node, String wordPrefix, boolean skipEndTag, int replaceLength) { @@ -95,8 +95,8 @@ public final class AnimationContentAssist extends AndroidContentAssist { } - super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node, - wordPrefix, skipEndTag, replaceLength); + return super.computeAttributeValues(proposals, offset, parentTagName, attributeName, + node, wordPrefix, skipEndTag, replaceLength); } else if (parentTagName.equals(OBJECT_ANIMATOR) && attributeName.endsWith(PROPERTY_NAME)) { @@ -156,12 +156,13 @@ public final class AnimationContentAssist extends AndroidContentAssist { addMatchingProposals(proposals, pairs.toArray(), offset, node, wordPrefix, (char) 0 /* needTag */, true /* isAttribute */, false /* isNew */, skipEndTag /* skipEndTag */, replaceLength); - return; } } + + return false; } else { - super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node, - wordPrefix, skipEndTag, replaceLength); + return super.computeAttributeValues(proposals, offset, parentTagName, attributeName, + node, wordPrefix, skipEndTag, replaceLength); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java index 55d463b..224c28f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/common/CommonMatchingStrategy.java @@ -47,23 +47,35 @@ public class CommonMatchingStrategy implements IEditorMatchingStrategy { IFile file = fileInput.getFile(); if (file.getParent().getName().startsWith(FD_RES_LAYOUT)) { ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); - if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT - && AdtPrefs.getPrefs().isSharedLayoutEditor()) { - LayoutEditorMatchingStrategy m = new LayoutEditorMatchingStrategy(); - return m.matches(editorRef, fileInput); + if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) { + if (AdtPrefs.getPrefs().isSharedLayoutEditor()) { + LayoutEditorMatchingStrategy m = new LayoutEditorMatchingStrategy(); + return m.matches(editorRef, fileInput); + } else { + // Skip files that don't match by name (see below). However, for + // layout files we can't just use editorRef.getName(), since + // the name sometimes includes the parent folder name (when the + // files are in layout- folders. + if (!(editorRef.getName().endsWith(file.getName()) && + editorRef.getId().equals(CommonXmlEditor.ID))) { + return false; + } + } + } + } else { + // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput() + // is expensive so try exclude files that definitely don't match, such + // as those with the wrong extension or wrong file name + if (!(file.getName().equals(editorRef.getName()) && + editorRef.getId().equals(CommonXmlEditor.ID))) { + return false; } } - // Per the IEditorMatchingStrategy documentation, editorRef.getEditorInput() - // is expensive so try exclude files that definitely don't match, such - // as those with the wrong extension or wrong file name - if (file.getName().equals(editorRef.getName()) && - editorRef.getId().equals(CommonXmlEditor.ID)) { - try { - return input.equals(editorRef.getEditorInput()); - } catch (PartInitException e) { - AdtPlugin.log(e, null); - } + try { + return input.equals(editorRef.getEditorInput()); + } catch (PartInitException e) { + AdtPlugin.log(e, null); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java index 9c29077..9f69e41 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/AndroidXmlFormattingStrategy.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.editors.formatting; +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findLineStart; import static com.android.ide.eclipse.adt.internal.editors.AndroidXmlAutoEditStrategy.findTextStart; import static com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors.SELECTOR_TAG; @@ -27,12 +28,22 @@ import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG import static org.eclipse.wst.xml.core.internal.regions.DOMRegionContext.XML_TAG_OPEN; import com.android.SdkConstants; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatPreferences; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.common.xml.XmlPrettyPrinter; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.resources.ResourceType; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; @@ -70,7 +81,7 @@ import java.util.Queue; * Formatter which formats XML content according to the established Android coding * conventions. It performs the format by computing the smallest set of DOM nodes * overlapping the formatted region, then it pretty-prints that XML region - * using the {@link XmlPrettyPrinter}, and then it replaces the affected region + * using the {@link EclipseXmlPrettyPrinter}, and then it replaces the affected region * by the pretty-printed region. * <p> * This strategy is also used for delegation. If the user has chosen to use the @@ -83,6 +94,8 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy private final Queue<IDocument> mDocuments = new LinkedList<IDocument>(); private final LinkedList<TypedPosition> mPartitions = new LinkedList<TypedPosition>(); private ContextBasedFormattingStrategy mDelegate = null; + /** False if document is known not to be in an Android project, null until initialized */ + private Boolean mIsAndroid; /** * Creates a new {@link AndroidXmlFormattingStrategy} @@ -91,7 +104,8 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy } private ContextBasedFormattingStrategy getDelegate() { - if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { + if (!AdtPrefs.getPrefs().getUseCustomXmlFormatter() + || mIsAndroid != null && !mIsAndroid.booleanValue()) { if (mDelegate == null) { mDelegate = new XMLFormattingStrategy(); } @@ -158,7 +172,7 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy * @param length the length of the text range to be formatted * @return a {@link TextEdit} which edits the model into a formatted document */ - public static TextEdit format(IStructuredModel model, int start, int length) { + private static TextEdit format(IStructuredModel model, int start, int length) { int end = start + length; TextEdit edit = new MultiTextEdit(); @@ -258,6 +272,7 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy int initialDepth = 0; int replaceStart; int replaceEnd; + boolean endWithNewline = false; if (startNode == null || endNode == null) { // Process the entire document root = domDocument; @@ -267,6 +282,11 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy endNode = root; replaceStart = 0; replaceEnd = document.getLength(); + try { + endWithNewline = replaceEnd > 0 && document.getChar(replaceEnd - 1) == '\n'; + } catch (BadLocationException e) { + // Can't happen + } } else { root = DomUtilities.getCommonAncestor(startNode, endNode); initialDepth = root != null ? DomUtilities.getDepth(root) - 1 : 0; @@ -321,9 +341,10 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy } XmlFormatStyle style = guessStyle(model, domDocument); - XmlFormatPreferences prefs = XmlFormatPreferences.create(); + XmlFormatPreferences prefs = EclipseXmlFormatPreferences.create(); String delimiter = TextUtilities.getDefaultLineDelimiter(document); - XmlPrettyPrinter printer = new XmlPrettyPrinter(prefs, style, delimiter); + XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, delimiter); + printer.setEndWithNewline(endWithNewline); if (indentationLevels != null) { printer.setIndentationLevels(indentationLevels); @@ -360,7 +381,8 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy * adjusted (for example to make the edit smaller if the beginning and/or end is * identical, and so on) */ - private static ReplaceEdit createReplaceEdit(IStructuredDocument document, int replaceStart, + @VisibleForTesting + static ReplaceEdit createReplaceEdit(IDocument document, int replaceStart, int replaceEnd, String formatted, XmlFormatPreferences prefs) { // If replacing a node somewhere in the middle, start the replacement at the // beginning of the current line @@ -399,7 +421,7 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy if (c == '\n') { beginsWithNewline = true; break; - } else if (!Character.isWhitespace(c)) { + } else if (!Character.isWhitespace(c)) { // \r is whitespace so is handled correctly break; } } @@ -411,6 +433,9 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy replaceStart = prevNewlineIndex; } prevNewlineIndex = index; + if (index > 0 && document.getChar(index - 1) == '\r') { + prevNewlineIndex--; + } } else if (!Character.isWhitespace(c)) { break; } @@ -423,16 +448,16 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy } // Search forwards too - prevNewlineIndex = -1; + int nextNewlineIndex = -1; try { int max = document.getLength(); for (index = replaceEnd; index < max; index++) { char c = document.getChar(index); if (c == '\n') { - if (prevNewlineIndex != -1) { - replaceEnd = prevNewlineIndex + 1; + if (nextNewlineIndex != -1) { + replaceEnd = nextNewlineIndex + 1; } - prevNewlineIndex = index; + nextNewlineIndex = index; } else if (!Character.isWhitespace(c)) { break; } @@ -440,7 +465,6 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy } catch (BadLocationException e) { AdtPlugin.log(e, null); } - boolean endsWithNewline = false; for (int i = formatted.length() - 1; i >= 0; i--) { char c = formatted.charAt(i); @@ -452,8 +476,8 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy } } - if (prefs.removeEmptyLines && prevNewlineIndex != -1 && endsWithNewline) { - replaceEnd = prevNewlineIndex + 1; + if (prefs.removeEmptyLines && nextNewlineIndex != -1 && endsWithNewline) { + replaceEnd = nextNewlineIndex + 1; } // Figure out how much of the before and after strings are identical and narrow @@ -503,7 +527,10 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy static XmlFormatStyle guessStyle(IStructuredModel model, Document domDocument) { // The "layout" style is used for most XML resource file types: // layouts, color-lists and state-lists, animations, drawables, menus, etc - XmlFormatStyle style = XmlFormatStyle.LAYOUT; + XmlFormatStyle style = XmlFormatStyle.get(domDocument); + if (style == XmlFormatStyle.FILE) { + style = XmlFormatStyle.LAYOUT; + } // The "resource" style is used for most value-based XML files: // strings, dimensions, booleans, colors, integers, plurals, @@ -532,7 +559,7 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy String[] segments = resourceFolder.split("-"); //$NON-NLS-1$ ResourceType type = ResourceType.getEnum(segments[0]); if (type != null) { - style = XmlFormatStyle.get(type); + style = EclipseXmlPrettyPrinter.get(type); } } } @@ -541,6 +568,50 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy return style; } + private Boolean isAndroid(IDocument document) { + if (mIsAndroid == null) { + // Look up the corresponding IResource for this document. This isn't + // readily available, so take advantage of the structured model's base location + // string and convert it to an IResource to look up its project. + if (document instanceof IStructuredDocument) { + IStructuredDocument structuredDocument = (IStructuredDocument) document; + IModelManager modelManager = StructuredModelManager.getModelManager(); + + IStructuredModel model = modelManager.getModelForRead(structuredDocument); + if (model != null) { + String location = model.getBaseLocation(); + model.releaseFromRead(); + if (location != null) { + if (!location.endsWith(ANDROID_MANIFEST_XML) + && !location.contains("/res/")) { //$NON-NLS-1$ + // See if it looks like a foreign document + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot root = workspace.getRoot(); + IResource member = root.findMember(location); + if (member.exists()) { + IProject project = member.getProject(); + if (project.isAccessible() && + !BaseProjectHelper.isAndroidProject(project)) { + mIsAndroid = false; + return false; + } + } + } + // Ignore Maven POM files even in Android projects + if (location.endsWith("/pom.xml")) { //$NON-NLS-1$ + mIsAndroid = false; + return false; + } + } + } + } + + mIsAndroid = true; + } + + return mIsAndroid.booleanValue(); + } + @Override public void formatterStarts(final IFormattingContext context) { // Use Eclipse XML formatter instead? @@ -563,6 +634,15 @@ public class AndroidXmlFormattingStrategy extends ContextBasedFormattingStrategy IDocument document = (IDocument) context.getProperty(CONTEXT_MEDIUM); mPartitions.offer(partition); mDocuments.offer(document); + + if (!isAndroid(document)) { + // It's some foreign type of project: use default + // formatter + delegate = getDelegate(); + if (delegate != null) { + delegate.formatterStarts(context); + } + } } @Override diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatPreferences.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java index 04441fd..6c00b8e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatPreferences.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlFormatPreferences.java @@ -15,60 +15,41 @@ */ package com.android.ide.eclipse.adt.internal.editors.formatting; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatPreferences; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; -import com.android.ide.eclipse.adt.internal.preferences.AttributeSortOrder; +import com.android.ide.common.xml.XmlAttributeSortOrder; import org.eclipse.core.runtime.Preferences; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.ui.internal.editors.text.EditorsPlugin; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; import org.eclipse.wst.xml.core.internal.XMLCorePlugin; import org.eclipse.wst.xml.core.internal.preferences.XMLCorePreferenceNames; +import org.w3c.dom.Attr; + +import java.util.Comparator; /** * Formatting preferences used by the Android XML formatter. */ -public class XmlFormatPreferences { - /** Use the Eclipse indent (tab/space, indent size) settings? */ - public boolean useEclipseIndent = false; - - /** Remove empty lines in all cases? */ - public boolean removeEmptyLines = false; - - /** Reformat the text and comment blocks? */ - public boolean reflowText = false; - - /** Join lines when reformatting text and comment blocks? */ - public boolean joinLines = false; - - /** Can attributes appear on the same line as the opening line if there is just one of them? */ - public boolean oneAttributeOnFirstLine = true; - - /** The sorting order to use when formatting */ - public AttributeSortOrder sortAttributes = AttributeSortOrder.LOGICAL; - - /** Should there be a space before the closing > or /> ? */ - public boolean spaceBeforeClose = true; - - /** The string to insert for each indentation level */ - private String mOneIndentUnit = " "; //$NON-NLS-1$ - - /** Tab width (number of spaces to display for a tab) */ - private int mTabWidth = -1; // -1: uninitialized - +public class EclipseXmlFormatPreferences extends XmlFormatPreferences { @VisibleForTesting - protected XmlFormatPreferences() { + protected EclipseXmlFormatPreferences() { } /** - * Creates a new {@link XmlFormatPreferences} based on the current settings + * Creates a new {@link EclipseXmlFormatPreferences} based on the current settings * in {@link AdtPrefs} * - * @return an {@link XmlFormatPreferences} object + * @return an {@link EclipseXmlFormatPreferences} object */ - public static XmlFormatPreferences create() { - XmlFormatPreferences p = new XmlFormatPreferences(); + @NonNull + public static EclipseXmlFormatPreferences create() { + EclipseXmlFormatPreferences p = new EclipseXmlFormatPreferences(); AdtPrefs prefs = AdtPrefs.getPrefs(); p.useEclipseIndent = prefs.isUseEclipseIndent(); @@ -80,14 +61,37 @@ public class XmlFormatPreferences { return p; } + @Override + @Nullable + public Comparator<Attr> getAttributeComparator() { + // Can't just skip sorting; the attribute table moves attributes out of order + // due to hashing, so sort by node positions + if (sortAttributes == XmlAttributeSortOrder.NO_SORTING) { + return EXISTING_ORDER_COMPARATOR; + } + return sortAttributes.getAttributeComparator(); + } + + private static final Comparator<Attr> EXISTING_ORDER_COMPARATOR = new Comparator<Attr>() { + @Override + public int compare(Attr attr1, Attr attr2) { + IndexedRegion region1 = (IndexedRegion) attr1; + IndexedRegion region2 = (IndexedRegion) attr2; + + return region1.getStartOffset() - region2.getStartOffset(); + } + }; + // The XML module settings do not have a public API. We should replace this with JDT // settings anyway since that's more likely what users have configured and want applied // to their XML files + /** * Returns the string to use to indent one indentation level * * @return the string used to indent one indentation level */ + @Override @SuppressWarnings({ "restriction", "deprecation" }) @@ -119,6 +123,7 @@ public class XmlFormatPreferences { * * @return the number of spaces used to display a single tab character */ + @Override @SuppressWarnings("restriction") // Editor settings public int getTabWidth() { if (mTabWidth == -1) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java new file mode 100644 index 0000000..d3f7ec8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/EclipseXmlPrettyPrinter.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2011 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.formatting; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.xml.XmlFormatPreferences; +import com.android.ide.common.xml.XmlFormatStyle; +import com.android.ide.common.xml.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; +import com.android.utils.XmlUtils; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jface.text.TextUtilities; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Eclipse customization of the {@link EclipseXmlPrettyPrinter} which takes advantage of the + * Eclipse DOM Api to track additional information, such as whether an element with no children + * was of the open form ({@code <foo></foo>}) or the closed form ({@code <foo/>}), the ability to + * look up the original source (for proper entity handling), the ability to preserve attribute + * source order, etc. + */ +@SuppressWarnings("restriction") // WST XML API +public class EclipseXmlPrettyPrinter extends XmlPrettyPrinter { + + /** + * Creates a new {@link com.android.ide.common.xml.XmlPrettyPrinter} + * + * @param prefs the preferences to format with + * @param style the style to format with + * @param lineSeparator the line separator to use, such as "\n" (can be null, in which case the + * system default is looked up via the line.separator property) + */ + public EclipseXmlPrettyPrinter( + XmlFormatPreferences prefs, + XmlFormatStyle style, + String lineSeparator) { + super(prefs, style, lineSeparator == null ? getDefaultLineSeparator() : lineSeparator); + } + + /** + * Pretty-prints the given XML document, which must be well-formed. If it is not, + * the original unformatted XML document is returned + * + * @param xml the XML content to format + * @param prefs the preferences to format with + * @param style the style to format with + * @param lineSeparator the line separator to use, such as "\n" (can be null, in which + * case the system default is looked up via the line.separator property) + * @return the formatted document (or if a parsing error occurred, returns the + * unformatted document) + */ + @NonNull + public static String prettyPrint( + @NonNull String xml, + @NonNull XmlFormatPreferences prefs, + @NonNull XmlFormatStyle style, + @Nullable String lineSeparator) { + Document document = DomUtilities.parseStructuredDocument(xml); + if (document != null) { + EclipseXmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, + lineSeparator); + if (xml.endsWith("\n")) { //$NON-NLS-1$ + printer.setEndWithNewline(true); + } + + StringBuilder sb = new StringBuilder(3 * xml.length() / 2); + printer.prettyPrint(-1, document, null, null, sb, false /*openTagOnly*/); + return sb.toString(); + } else { + // Parser error: just return the unformatted content + return xml; + } + } + + @NonNull + public static String prettyPrint(@NonNull Node node, boolean endWithNewline) { + return prettyPrint(node, EclipseXmlFormatPreferences.create(), XmlFormatStyle.get(node), + null, endWithNewline); + } + + private static String getDefaultLineSeparator() { + org.eclipse.jface.text.Document blank = new org.eclipse.jface.text.Document(); + String lineSeparator = TextUtilities.getDefaultLineDelimiter(blank); + if (lineSeparator == null) { + lineSeparator = SdkUtils.getLineSeparator(); + } + + return lineSeparator; + } + + /** + * Pretty prints the given node + * + * @param node the node, usually a document, to be printed + * @param prefs the formatting preferences + * @param style the formatting style to use + * @param lineSeparator the line separator to use, or null to use the + * default + * @return a formatted string + */ + @NonNull + public static String prettyPrint( + @NonNull Node node, + @NonNull XmlFormatPreferences prefs, + @NonNull XmlFormatStyle style, + @Nullable String lineSeparator, + boolean endWithNewline) { + XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(prefs, style, lineSeparator); + printer.setEndWithNewline(endWithNewline); + StringBuilder sb = new StringBuilder(1000); + printer.prettyPrint(-1, node, null, null, sb, false /*openTagOnly*/); + String xml = sb.toString(); + if (node.getNodeType() == Node.DOCUMENT_NODE && !xml.startsWith("<?")) { //$NON-NLS-1$ + xml = XmlUtils.XML_PROLOG + xml; + } + return xml; + } + + @Nullable + @Override + protected String getSource(@NonNull Node node) { + // In Eclipse, org.w3c.dom.DocumentType.getTextContent() returns null + if (node instanceof IDOMNode) { + // Get the original source string. This will contain the actual entities + // such as ">" instead of ">" which it gets turned into for the DOM nodes. + // By operating on source we can preserve the user's entities rather than + // having > for example always turned into >. + IDOMNode textImpl = (IDOMNode) node; + return textImpl.getSource(); + } + + return super.getSource(node); + } + + @Override + protected boolean isEmptyTag(Element element) { + if (element instanceof IDOMElement) { + IDOMElement elementImpl = (IDOMElement) element; + if (elementImpl.isEmptyTag()) { + return true; + } + } + + return false; + } + + /** + * Returns the {@link XmlFormatStyle} to use for a resource of the given type + * + * @param resourceType the type of resource to be formatted + * @return the suitable format style to use + */ + public static XmlFormatStyle get(ResourceType resourceType) { + switch (resourceType) { + case ARRAY: + case ATTR: + case BOOL: + case DECLARE_STYLEABLE: + case DIMEN: + case FRACTION: + case ID: + case INTEGER: + case STRING: + case PLURALS: + case STYLE: + case STYLEABLE: + case COLOR: + return XmlFormatStyle.RESOURCE; + + case LAYOUT: + return XmlFormatStyle.LAYOUT; + + case DRAWABLE: + case MENU: + case ANIM: + case ANIMATOR: + case INTERPOLATOR: + default: + return XmlFormatStyle.FILE; + } + } + + /** + * Returns the {@link XmlFormatStyle} to use for resource files in the given resource + * folder + * + * @param folderType the type of folder containing the resource file + * @return the suitable format style to use + */ + public static XmlFormatStyle getForFolderType(ResourceFolderType folderType) { + switch (folderType) { + case LAYOUT: + return XmlFormatStyle.LAYOUT; + case COLOR: + case VALUES: + return XmlFormatStyle.RESOURCE; + case ANIM: + case ANIMATOR: + case DRAWABLE: + case INTERPOLATOR: + case MENU: + default: + return XmlFormatStyle.FILE; + } + } + + /** + * Returns the {@link XmlFormatStyle} to use for resource files of the given path. + * + * @param path the path to the resource file + * @return the suitable format style to use + */ + public static XmlFormatStyle getForFile(IPath path) { + if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(path.lastSegment())) { + return XmlFormatStyle.MANIFEST; + } + + if (path.segmentCount() > 2) { + String parentName = path.segment(path.segmentCount() - 2); + ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName); + return getForFolderType(folderType); + } + + return XmlFormatStyle.FILE; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatStyle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatStyle.java deleted file mode 100644 index cb6ee5d..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlFormatStyle.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2011 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.formatting; - -import com.android.SdkConstants; -import com.android.resources.ResourceFolderType; -import com.android.resources.ResourceType; - -import org.eclipse.core.runtime.IPath; - -/** - * Style to use when printing the XML. Different types of Android XML files use slightly - * different preferred formats. For example, in layout files there is typically always a - * newline between successive elements, whereas in a manifest file there is typically only - * newlines between different types of elements. As another example, in resource files, - * the format is typically much more compact: the text content of {@code <item>} tags is - * included on the same line whereas for other layout styles the children are typically - * placed on a line of their own. - */ -public enum XmlFormatStyle { - /** Layout formatting style: blank lines between elements, attributes on separate lines */ - LAYOUT, - - /** Similar to layout formatting style, but no blank lines inside opening elements */ - FILE, - - /** Resource style: one line per complete element including text child content */ - RESOURCE, - - /** - * Similar to layout style, but no newlines between related elements such as - * successive {@code <uses-permission>} declarations, and no newlines inside - * the second level elements (so an {@code <activity>} declaration appears as a - * single block with no whitespace within it) - */ - MANIFEST; - - /** - * Returns the {@link XmlFormatStyle} to use for a resource of the given type - * - * @param resourceType the type of resource to be formatted - * @return the suitable format style to use - */ - public static XmlFormatStyle get(ResourceType resourceType) { - switch (resourceType) { - case ARRAY: - case ATTR: - case BOOL: - case DECLARE_STYLEABLE: - case DIMEN: - case FRACTION: - case ID: - case INTEGER: - case STRING: - case PLURALS: - case STYLE: - case STYLEABLE: - case COLOR: - return RESOURCE; - - case LAYOUT: - return LAYOUT; - - case DRAWABLE: - case MENU: - case ANIM: - case ANIMATOR: - case INTERPOLATOR: - default: - return FILE; - } - } - - /** - * Returns the {@link XmlFormatStyle} to use for resource files in the given resource - * folder - * - * @param folderType the type of folder containing the resource file - * @return the suitable format style to use - */ - public static XmlFormatStyle getForFolderType(ResourceFolderType folderType) { - switch (folderType) { - case LAYOUT: - return LAYOUT; - case COLOR: - case VALUES: - return RESOURCE; - case ANIM: - case ANIMATOR: - case DRAWABLE: - case INTERPOLATOR: - case MENU: - default: - return FILE; - } - } - - /** - * Returns the {@link XmlFormatStyle} to use for resource files of the given path. - * - * @param path the path to the resource file - * @return the suitable format style to use - */ - public static XmlFormatStyle getForFile(IPath path) { - if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(path.lastSegment())) { - return MANIFEST; - } - - if (path.segmentCount() > 2) { - String parentName = path.segment(path.segmentCount() - 2); - ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName); - return getForFolderType(folderType); - } - - return FILE; - } -}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java deleted file mode 100644 index 1dd32c7..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/formatting/XmlPrettyPrinter.java +++ /dev/null @@ -1,976 +0,0 @@ -/* - * Copyright (C) 2011 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.formatting; - -import static com.android.SdkConstants.TAG_COLOR; -import static com.android.SdkConstants.TAG_DIMEN; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.SdkConstants.XMLNS; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; -import com.android.utils.SdkUtils; -import com.android.utils.XmlUtils; - -import org.eclipse.wst.xml.core.internal.document.DocumentTypeImpl; -import org.eclipse.wst.xml.core.internal.document.ElementImpl; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * Visitor which walks over the subtree of the DOM to be formatted and pretty prints - * the DOM into the given {@link StringBuilder} - */ -@SuppressWarnings("restriction") -public class XmlPrettyPrinter { - private static final String COMMENT_BEGIN = "<!--"; //$NON-NLS-1$ - private static final String COMMENT_END = "-->"; //$NON-NLS-1$ - - /** The style to print the XML in */ - private final XmlFormatStyle mStyle; - /** Formatting preferences to use when formatting the XML */ - private final XmlFormatPreferences mPrefs; - /** Start node to start formatting at */ - private Node mStartNode; - /** Start node to stop formatting after */ - private Node mEndNode; - /** Whether the visitor is currently in range */ - private boolean mInRange; - /** Output builder */ - private StringBuilder mOut; - /** String to insert for a single indentation level */ - private String mIndentString; - /** Line separator to use */ - private String mLineSeparator; - /** If true, we're only formatting an open tag */ - private boolean mOpenTagOnly; - /** List of indentation to use for each given depth */ - private String[] mIndentationLevels; - - /** - * Creates a new {@link XmlPrettyPrinter} - * - * @param prefs the preferences to format with - * @param style the style to format with - * @param lineSeparator the line separator to use, such as "\n" (can be null, in which - * case the system default is looked up via the line.separator property) - */ - public XmlPrettyPrinter(XmlFormatPreferences prefs, XmlFormatStyle style, - String lineSeparator) { - mPrefs = prefs; - mStyle = style; - if (lineSeparator == null) { - lineSeparator = SdkUtils.getLineSeparator(); - } - mLineSeparator = lineSeparator; - } - - /** - * Sets the indentation levels to use (indentation string to use for each depth, - * indexed by depth - * - * @param indentationLevels an array of strings to use for the various indentation - * levels - */ - public void setIndentationLevels(String[] indentationLevels) { - mIndentationLevels = indentationLevels; - } - - /** - * Pretty-prints the given XML document, which must be well-formed. If it is not, - * the original unformatted XML document is returned - * - * @param xml the XML content to format - * @param prefs the preferences to format with - * @param style the style to format with - * @param lineSeparator the line separator to use, such as "\n" (can be null, in which - * case the system default is looked up via the line.separator property) - * @return the formatted document (or if a parsing error occurred, returns the - * unformatted document) - */ - @NonNull - public static String prettyPrint( - @NonNull String xml, - @NonNull XmlFormatPreferences prefs, - @NonNull XmlFormatStyle style, - @Nullable String lineSeparator) { - Document document = DomUtilities.parseStructuredDocument(xml); - if (document != null) { - XmlPrettyPrinter printer = new XmlPrettyPrinter(prefs, style, lineSeparator); - StringBuilder sb = new StringBuilder(3 * xml.length() / 2); - printer.prettyPrint(-1, document, null, null, sb, false /*openTagOnly*/); - return sb.toString(); - } else { - // Parser error: just return the unformatted content - return xml; - } - } - - /** - * Start pretty-printing at the given node, which must either be the - * startNode or contain it as a descendant. - * - * @param rootDepth the depth of the given node, used to determine indentation - * @param root the node to start pretty printing from (which may not itself be - * included in the start to end node range but should contain it) - * @param startNode the node to start formatting at - * @param endNode the node to end formatting at - * @param out the {@link StringBuilder} to pretty print into - * @param openTagOnly if true, only format the open tag of the startNode (and nothing - * else) - */ - public void prettyPrint(int rootDepth, Node root, Node startNode, Node endNode, - StringBuilder out, boolean openTagOnly) { - if (startNode == null) { - startNode = root; - } - if (endNode == null) { - endNode = root; - } - assert !openTagOnly || startNode == endNode; - - mStartNode = startNode; - mOpenTagOnly = openTagOnly; - mEndNode = endNode; - mOut = out; - mInRange = false; - mIndentString = mPrefs.getOneIndentUnit(); - - visitNode(rootDepth, root); - } - - /** Visit the given node at the given depth */ - private void visitNode(int depth, Node node) { - if (node == mStartNode) { - mInRange = true; - } - - if (mInRange) { - visitBeforeChildren(depth, node); - if (mOpenTagOnly && mStartNode == node) { - mInRange = false; - return; - } - } - - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - visitNode(depth + 1, child); - } - - if (mInRange) { - visitAfterChildren(depth, node); - } - - if (node == mEndNode) { - mInRange = false; - } - } - - private void visitBeforeChildren(int depth, Node node) { - short type = node.getNodeType(); - switch (type) { - case Node.DOCUMENT_NODE: - case Node.DOCUMENT_FRAGMENT_NODE: - // Nothing to do - break; - - case Node.ATTRIBUTE_NODE: - // Handled as part of processing elements - break; - - case Node.ELEMENT_NODE: { - printOpenElementTag(depth, node); - break; - } - - case Node.TEXT_NODE: { - printText(node); - break; - } - - case Node.CDATA_SECTION_NODE: - printCharacterData(depth, node); - break; - - case Node.PROCESSING_INSTRUCTION_NODE: - printProcessingInstruction(node); - break; - - case Node.COMMENT_NODE: { - printComment(depth, node); - break; - } - - case Node.DOCUMENT_TYPE_NODE: - printDocType(node); - break; - - case Node.ENTITY_REFERENCE_NODE: - case Node.ENTITY_NODE: - case Node.NOTATION_NODE: - break; - default: - assert false : type; - } - } - - private void visitAfterChildren(int depth, Node node) { - short type = node.getNodeType(); - switch (type) { - case Node.ATTRIBUTE_NODE: - // Handled as part of processing elements - break; - case Node.ELEMENT_NODE: { - printCloseElementTag(depth, node); - break; - } - } - } - - private void printProcessingInstruction(Node node) { - mOut.append("<?xml "); //$NON-NLS-1$ - mOut.append(node.getNodeValue().trim()); - mOut.append('?').append('>').append(mLineSeparator); - } - - private void printDocType(Node node) { - // In Eclipse, org.w3c.dom.DocumentType.getTextContent() returns null - if (node instanceof DocumentTypeImpl) { - String content = ((DocumentTypeImpl) node).getSource(); - mOut.append(content); - mOut.append(mLineSeparator); - } - } - - private void printCharacterData(int depth, Node node) { - String nodeValue = node.getNodeValue(); - boolean separateLine = nodeValue.indexOf('\n') != -1; - if (separateLine && !endsWithLineSeparator()) { - mOut.append(mLineSeparator); - } - mOut.append("<![CDATA["); //$NON-NLS-1$ - mOut.append(nodeValue); - mOut.append("]]>"); //$NON-NLS-1$ - if (separateLine) { - mOut.append(mLineSeparator); - } - } - - private void printText(Node node) { - boolean escape = true; - String text = node.getNodeValue(); - - if (node instanceof IDOMNode) { - // Get the original source string. This will contain the actual entities - // such as ">" instead of ">" which it gets turned into for the DOM nodes. - // By operating on source we can preserve the user's entities rather than - // having > for example always turned into >. - IDOMNode textImpl = (IDOMNode) node; - text = textImpl.getSource(); - escape = false; - } - - // Most text nodes are just whitespace for formatting (which we're replacing) - // so look for actual text content and extract that part out - String trimmed = text.trim(); - if (trimmed.length() > 0) { - // TODO: Reformat the contents if it is too wide? - - // Note that we append the actual text content, NOT the trimmed content, - // since the whitespace may be significant, e.g. - // <string name="toast_sync_error">Sync error: <xliff:g id="error">%1$s</xliff:g>... - - // However, we should remove all blank lines in the prefix and suffix of the - // text node, or we will end up inserting additional blank lines each time you're - // formatting a text node within an outer element (which also adds spacing lines) - int lastPrefixNewline = -1; - for (int i = 0, n = text.length(); i < n; i++) { - char c = text.charAt(i); - if (c == '\n') { - lastPrefixNewline = i; - } else if (!Character.isWhitespace(c)) { - break; - } - } - int firstSuffixNewline = -1; - for (int i = text.length() - 1; i >= 0; i--) { - char c = text.charAt(i); - if (c == '\n') { - firstSuffixNewline = i; - } else if (!Character.isWhitespace(c)) { - break; - } - } - if (lastPrefixNewline != -1 || firstSuffixNewline != -1) { - if (firstSuffixNewline == -1) { - firstSuffixNewline = text.length(); - } - text = text.substring(lastPrefixNewline + 1, firstSuffixNewline); - } - - if (escape) { - XmlUtils.appendXmlTextValue(mOut, text); - } else { - // Text is already escaped - mOut.append(text); - } - - if (mStyle != XmlFormatStyle.RESOURCE) { - mOut.append(mLineSeparator); - } - } - } - - private void printComment(int depth, Node node) { - String comment = node.getNodeValue(); - boolean multiLine = comment.indexOf('\n') != -1; - String trimmed = comment.trim(); - - // See if this is an "end-of-the-line" comment, e.g. it is not a multi-line - // comment and it appears on the same line as an opening or closing element tag; - // if so, continue to place it as a suffix comment - boolean isSuffixComment = false; - if (!multiLine) { - Node previous = node.getPreviousSibling(); - isSuffixComment = true; - while (previous != null) { - short type = previous.getNodeType(); - if (type == Node.TEXT_NODE || type == Node.COMMENT_NODE) { - if (previous.getNodeValue().indexOf('\n') != -1) { - isSuffixComment = false; - break; - } - } else { - break; - } - previous = previous.getPreviousSibling(); - } - if (isSuffixComment) { - // Remove newline added by element open tag or element close tag - if (endsWithLineSeparator()) { - removeLastLineSeparator(); - } - mOut.append(' '); - } - } - - // Put the comment on a line on its own? Only if it was separated by a blank line - // in the previous version of the document. In other words, if the document - // adds blank lines between comments this formatter will preserve that fact, and vice - // versa for a tightly formatted document it will preserve that convention as well. - if (!mPrefs.removeEmptyLines && depth > 0 && !isSuffixComment) { - Node curr = node.getPreviousSibling(); - if (curr == null) { - mOut.append(mLineSeparator); - } else if (curr.getNodeType() == Node.TEXT_NODE) { - String text = curr.getNodeValue(); - // Count how many newlines we find in the trailing whitespace of the - // text node - int newLines = 0; - for (int i = text.length() - 1; i >= 0; i--) { - char c = text.charAt(i); - if (Character.isWhitespace(c)) { - if (c == '\n') { - newLines++; - if (newLines == 2) { - break; - } - } - } else { - break; - } - } - if (newLines >= 2) { - mOut.append(mLineSeparator); - } else if (text.trim().length() == 0 && curr.getPreviousSibling() == null) { - // Comment before first child in node - mOut.append(mLineSeparator); - } - } - } - - - // TODO: Reformat the comment text? - if (!multiLine) { - if (!isSuffixComment) { - indent(depth); - } - mOut.append(COMMENT_BEGIN).append(' '); - mOut.append(trimmed); - mOut.append(' ').append(COMMENT_END); - mOut.append(mLineSeparator); - } else { - // Strip off blank lines at the beginning and end of the comment text. - // Find last newline at the beginning of the text: - int index = 0; - int end = comment.length(); - int recentNewline = -1; - while (index < end) { - char c = comment.charAt(index); - if (c == '\n') { - recentNewline = index; - } - if (!Character.isWhitespace(c)) { - break; - } - index++; - } - - int start = recentNewline + 1; - - // Find last newline at the end of the text - index = end - 1; - recentNewline = -1; - while (index > start) { - char c = comment.charAt(index); - if (c == '\n') { - recentNewline = index; - } - if (!Character.isWhitespace(c)) { - break; - } - index--; - } - - end = recentNewline == -1 ? index + 1 : recentNewline; - if (start >= end) { - // It's a blank comment like <!-- \n\n--> - just clean it up - if (!isSuffixComment) { - indent(depth); - } - mOut.append(COMMENT_BEGIN).append(' ').append(COMMENT_END); - mOut.append(mLineSeparator); - return; - } - - trimmed = comment.substring(start, end); - - // When stripping out prefix and suffix blank lines we might have ended up - // with a single line comment again so check and format single line comments - // without newlines inside the <!-- --> delimiters - multiLine = trimmed.indexOf('\n') != -1; - if (multiLine) { - indent(depth); - mOut.append(COMMENT_BEGIN); - mOut.append(mLineSeparator); - - // See if we need to add extra spacing to keep alignment. Consider a comment - // like this: - // <!-- Deprecated strings - Move the identifiers to this section, - // and remove the actual text. --> - // This String will be - // " Deprecated strings - Move the identifiers to this section,\n" + - // " and remove the actual text. -->" - // where the left side column no longer lines up. - // To fix this, we need to insert some extra whitespace into the first line - // of the string; in particular, the exact number of characters that the - // first line of the comment was indented with! - - // However, if the comment started like this: - // <!-- - // /** Copyright - // --> - // then obviously the align-indent is 0, so we only want to compute an - // align indent when we don't find a newline before the content - boolean startsWithNewline = false; - for (int i = 0; i < start; i++) { - if (comment.charAt(i) == '\n') { - startsWithNewline = true; - break; - } - } - if (!startsWithNewline) { - Node previous = node.getPreviousSibling(); - if (previous != null && previous.getNodeType() == Node.TEXT_NODE) { - String prevText = previous.getNodeValue(); - int indentation = COMMENT_BEGIN.length(); - for (int i = prevText.length() - 1; i >= 0; i--) { - char c = prevText.charAt(i); - if (c == '\n') { - break; - } else { - indentation += (c == '\t') ? mPrefs.getTabWidth() : 1; - } - } - - // See if the next line after the newline has indentation; if it doesn't, - // leave things alone. This fixes a case like this: - // <!-- This is the - // comment block --> - // such that it doesn't turn it into - // <!-- - // This is the - // comment block - // --> - // In this case we instead want - // <!-- - // This is the - // comment block - // --> - int minIndent = Integer.MAX_VALUE; - String[] lines = trimmed.split("\n"); //$NON-NLS-1$ - // Skip line 0 since we know that it doesn't start with a newline - for (int i = 1; i < lines.length; i++) { - int indent = 0; - String line = lines[i]; - for (int j = 0; j < line.length(); j++) { - char c = line.charAt(j); - if (!Character.isWhitespace(c)) { - // Only set minIndent if there's text content on the line; - // blank lines can exist in the comment without affecting - // the overall minimum indentation boundary. - if (indent < minIndent) { - minIndent = indent; - } - break; - } else { - indent += (c == '\t') ? mPrefs.getTabWidth() : 1; - } - } - } - - if (minIndent < indentation) { - indentation = minIndent; - - // Subtract any indentation that is already present on the line - String line = lines[0]; - for (int j = 0; j < line.length(); j++) { - char c = line.charAt(j); - if (!Character.isWhitespace(c)) { - break; - } else { - indentation -= (c == '\t') ? mPrefs.getTabWidth() : 1; - } - } - } - - for (int i = 0; i < indentation; i++) { - mOut.append(' '); - } - - if (indentation < 0) { - boolean prefixIsSpace = true; - for (int i = 0; i < -indentation && i < trimmed.length(); i++) { - if (!Character.isWhitespace(trimmed.charAt(i))) { - prefixIsSpace = false; - break; - } - } - if (prefixIsSpace) { - trimmed = trimmed.substring(-indentation); - } - } - } - } - - mOut.append(trimmed); - mOut.append(mLineSeparator); - indent(depth); - mOut.append(COMMENT_END); - mOut.append(mLineSeparator); - } else { - mOut.append(COMMENT_BEGIN).append(' '); - mOut.append(trimmed); - mOut.append(' ').append(COMMENT_END); - mOut.append(mLineSeparator); - } - } - - // Preserve whitespace after comment: See if the original document had two or - // more newlines after the comment, and if so have a blank line between this - // comment and the next - Node next = node.getNextSibling(); - if (!mPrefs.removeEmptyLines && next != null && next.getNodeType() == Node.TEXT_NODE) { - String text = next.getNodeValue(); - int newLinesBeforeText = 0; - for (int i = 0, n = text.length(); i < n; i++) { - char c = text.charAt(i); - if (c == '\n') { - newLinesBeforeText++; - if (newLinesBeforeText == 2) { - // Yes - mOut.append(mLineSeparator); - break; - } - } else if (!Character.isWhitespace(c)) { - break; - } - } - } - } - - private boolean endsWithLineSeparator() { - int separatorLength = mLineSeparator.length(); - if (mOut.length() >= separatorLength) { - for (int i = 0, j = mOut.length() - separatorLength; i < separatorLength; i++) { - if (mOut.charAt(j) != mLineSeparator.charAt(i)) { - return false; - } - } - } - - return true; - } - - private void removeLastLineSeparator() { - mOut.setLength(mOut.length() - mLineSeparator.length()); - } - - private void printOpenElementTag(int depth, Node node) { - Element element = (Element) node; - if (newlineBeforeElementOpen(element, depth)) { - mOut.append(mLineSeparator); - } - if (indentBeforeElementOpen(element, depth)) { - indent(depth); - } - mOut.append('<').append(element.getTagName()); - - NamedNodeMap attributes = element.getAttributes(); - int attributeCount = attributes.getLength(); - if (attributeCount > 0) { - // Sort the attributes - List<Attr> attributeList = new ArrayList<Attr>(); - for (int i = 0, n = attributeCount; i < n; i++) { - attributeList.add((Attr) attributes.item(i)); - } - Comparator<Attr> comparator = mPrefs.sortAttributes.getAttributeComparator(); - Collections.sort(attributeList, comparator); - - // Put the single attribute on the same line as the element tag? - boolean singleLine = mPrefs.oneAttributeOnFirstLine && attributeCount == 1 - // In resource files we always put all the attributes (which is - // usually just zero, one or two) on the same line - || mStyle == XmlFormatStyle.RESOURCE; - - // We also place the namespace declaration on the same line as the root element, - // but this doesn't also imply singleLine handling; subsequent attributes end up - // on their own lines - boolean indentNextAttribute; - if (singleLine || (depth == 0 && XMLNS.equals(attributeList.get(0).getPrefix()))) { - mOut.append(' '); - indentNextAttribute = false; - } else { - mOut.append(mLineSeparator); - indentNextAttribute = true; - } - - Attr last = attributeList.get(attributeCount - 1); - for (Attr attribute : attributeList) { - if (indentNextAttribute) { - indent(depth + 1); - } - mOut.append(attribute.getName()); - mOut.append('=').append('"'); - XmlUtils.appendXmlAttributeValue(mOut, attribute.getValue()); - mOut.append('"'); - - // Don't add a newline at the last attribute line; the > should - // immediately follow the last attribute - if (attribute != last) { - mOut.append(singleLine ? " " : mLineSeparator); //$NON-NLS-1$ - indentNextAttribute = !singleLine; - } - } - } - - boolean isClosed = isEmptyTag(element); - - // Add a space before the > or /> ? In resource files, only do this when closing the - // element - if (mPrefs.spaceBeforeClose && (mStyle != XmlFormatStyle.RESOURCE || isClosed) - // in <selector> files etc still treat the <item> entries as in resource files - && !TAG_ITEM.equals(element.getTagName()) - && (isClosed || element.getAttributes().getLength() > 0)) { - mOut.append(' '); - } - - if (isClosed) { - mOut.append('/'); - } - - mOut.append('>'); - - if (newlineAfterElementOpen(element, depth, isClosed)) { - mOut.append(mLineSeparator); - } - } - - private void printCloseElementTag(int depth, Node node) { - Element element = (Element) node; - if (isEmptyTag(element)) { - // Empty tag: Already handled as part of opening tag - return; - } - - // Put the closing declaration on its own line - unless it's a compact - // resource file format - // If the element had element children, separate the end tag from them - if (newlineBeforeElementClose(element, depth)) { - mOut.append(mLineSeparator); - } - if (indentBeforeElementClose(element, depth)) { - indent(depth); - } - mOut.append('<').append('/'); - mOut.append(node.getNodeName()); - mOut.append('>'); - - if (newlineAfterElementClose(element, depth)) { - mOut.append(mLineSeparator); - } - } - - private boolean newlineBeforeElementOpen(Element element, int depth) { - if (hasBlankLineAbove()) { - return false; - } - - if (mPrefs.removeEmptyLines || depth <= 0) { - return false; - } - - if (isMarkupElement(element)) { - return false; - } - - // See if this element should be separated from the previous element. - // This is the case if we are not compressing whitespace (checked above), - // or if we are not immediately following a comment (in which case the - // newline would have been added above it), or if we are not in a formatting - // style where - if (mStyle == XmlFormatStyle.LAYOUT) { - // In layouts we always separate elements - return true; - } - - if (mStyle == XmlFormatStyle.MANIFEST || mStyle == XmlFormatStyle.RESOURCE - || mStyle == XmlFormatStyle.FILE) { - Node curr = element.getPreviousSibling(); - - // <style> elements are traditionally separated unless it follows a comment - if (TAG_STYLE.equals(element.getTagName())) { - if (curr == null - || curr.getNodeType() == Node.ELEMENT_NODE - || (curr.getNodeType() == Node.TEXT_NODE - && curr.getNodeValue().trim().length() == 0 - && (curr.getPreviousSibling() == null - || curr.getPreviousSibling().getNodeType() - == Node.ELEMENT_NODE))) { - return true; - } - } - - // In all other styles, we separate elements if they have a different tag than - // the previous one (but we don't insert a newline inside tags) - while (curr != null) { - short nodeType = curr.getNodeType(); - if (nodeType == Node.ELEMENT_NODE) { - Element sibling = (Element) curr; - if (!element.getTagName().equals(sibling.getTagName())) { - return true; - } - break; - } else if (nodeType == Node.TEXT_NODE) { - String text = curr.getNodeValue(); - if (text.trim().length() > 0) { - break; - } - // If there is just whitespace, continue looking for a previous sibling - } else { - // Any other previous node type, such as a comment, means we don't - // continue looking: this element should not be separated - break; - } - curr = curr.getPreviousSibling(); - } - if (curr == null && depth <= 1) { - // Insert new line inside tag if it's the first element inside the root tag - return true; - } - - return false; - } - - return false; - } - - private boolean indentBeforeElementOpen(Element element, int depth) { - if (isMarkupElement(element)) { - return false; - } - - if (element.getParentNode().getNodeType() == Node.ELEMENT_NODE - && keepElementAsSingleLine(depth - 1, (Element) element.getParentNode())) { - return false; - } - - return true; - } - - private boolean indentBeforeElementClose(Element element, int depth) { - if (isMarkupElement(element)) { - return false; - } - - char lastOutChar = mOut.charAt(mOut.length() - 1); - char lastDelimiterChar = mLineSeparator.charAt(mLineSeparator.length() - 1); - return lastOutChar == lastDelimiterChar; - } - - private boolean newlineAfterElementOpen(Element element, int depth, boolean isClosed) { - if (hasBlankLineAbove()) { - return false; - } - - if (isMarkupElement(element)) { - return false; - } - - // In resource files we keep the child content directly on the same - // line as the element (unless it has children). in other files, separate them - return isClosed || !keepElementAsSingleLine(depth, element); - } - - private boolean newlineBeforeElementClose(Element element, int depth) { - if (hasBlankLineAbove()) { - return false; - } - - if (isMarkupElement(element)) { - return false; - } - - return depth == 0 && !mPrefs.removeEmptyLines; - } - - private boolean hasBlankLineAbove() { - if (mOut.length() < 2 * mLineSeparator.length()) { - return false; - } - - return SdkUtils.endsWith(mOut, mLineSeparator) && - SdkUtils.endsWith(mOut, mOut.length() - mLineSeparator.length(), mLineSeparator); - } - - private boolean newlineAfterElementClose(Element element, int depth) { - if (hasBlankLineAbove()) { - return false; - } - - if (isMarkupElement(element)) { - return false; - } - - return element.getParentNode().getNodeType() == Node.ELEMENT_NODE - && !keepElementAsSingleLine(depth - 1, (Element) element.getParentNode()); - } - - private boolean isMarkupElement(Element element) { - // The documentation suggests that the allowed tags are <u>, <b> and <i>: - // developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling - // However, the full set of tags accepted by Html.fromHtml is much larger. Therefore, - // instead consider *any* element nested inside a <string> definition to be a markup - // element. See frameworks/base/core/java/android/text/Html.java and look for - // HtmlToSpannedConverter#handleStartTag. - - if (mStyle != XmlFormatStyle.RESOURCE) { - return false; - } - - Node curr = element.getParentNode(); - while (curr != null) { - if (TAG_STRING.equals(curr.getNodeName())) { - return true; - } - - curr = curr.getParentNode(); - } - - return false; - } - - /** - * TODO: Explain why we need to do per-tag decisions on whether to keep them on the - * same line or not. Show that we can't just do it by depth, or by file type. - * (style versus plurals example) - * @param tag - * @return - */ - private boolean isSingleLineTag(Element element) { - String tag = element.getTagName(); - - return (tag.equals(TAG_ITEM) && mStyle == XmlFormatStyle.RESOURCE) - || tag.equals(TAG_STRING) - || tag.equals(TAG_DIMEN) - || tag.equals(TAG_COLOR); - } - - private boolean keepElementAsSingleLine(int depth, Element element) { - if (depth == 0) { - return false; - } - - return isSingleLineTag(element) - || (mStyle == XmlFormatStyle.RESOURCE - && !DomUtilities.hasElementChildren(element)); - } - - private void indent(int depth) { - int i = 0; - - if (mIndentationLevels != null) { - for (int j = Math.min(depth, mIndentationLevels.length - 1); j >= 0; j--) { - String indent = mIndentationLevels[j]; - if (indent != null) { - mOut.append(indent); - i = j; - break; - } - } - } - - for (; i < depth; i++) { - mOut.append(mIndentString); - } - } - - private boolean isEmptyTag(Element element) { - boolean isClosed = false; - if (element instanceof ElementImpl) { - ElementImpl elementImpl = (ElementImpl) element; - if (elementImpl.isEmptyTag()) { - isClosed = true; - } - } - return isClosed; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java index 90ea2a5..c77c853 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ContextPullParser.java @@ -16,9 +16,15 @@ package com.android.ide.eclipse.adt.internal.editors.layout; +import static com.android.SdkConstants.ATTR_IGNORE; import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; +import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; +import static com.android.SdkConstants.GRID_VIEW; +import static com.android.SdkConstants.LIST_VIEW; +import static com.android.SdkConstants.SPINNER; +import static com.android.SdkConstants.TOOLS_URI; import static com.android.SdkConstants.VALUE_FILL_PARENT; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VIEW_FRAGMENT; @@ -28,12 +34,14 @@ import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMet import com.android.SdkConstants; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.IProjectCallback; -import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.common.resources.ValueResourceParser; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata; +import com.google.common.collect.Maps; import org.kxml2.io.KXmlParser; import java.io.File; +import java.util.Map; /** * Modified {@link KXmlParser} that adds the methods of {@link ILayoutPullParser}, and @@ -80,7 +88,37 @@ public class ContextPullParser extends KXmlParser implements ILayoutPullParser { @Override public Object getViewCookie() { - return null; // never any key to return + String name = super.getName(); + if (name == null) { + return null; + } + + // Store tools attributes if this looks like a layout we'll need adapter view + // bindings for in the ProjectCallback. + if (LIST_VIEW.equals(name) + || EXPANDABLE_LIST_VIEW.equals(name) + || GRID_VIEW.equals(name) + || SPINNER.equals(name)) { + Map<String, String> map = null; + int count = getAttributeCount(); + for (int i = 0; i < count; i++) { + String namespace = getAttributeNamespace(i); + if (namespace != null && namespace.equals(TOOLS_URI)) { + String attribute = getAttributeName(i); + if (attribute.equals(ATTR_IGNORE)) { + continue; + } + if (map == null) { + map = Maps.newHashMapWithExpectedSize(4); + } + map.put(attribute, getAttributeValue(i)); + } + } + + return map; + } + + return null; } // --- KXMLParser override @@ -99,12 +137,13 @@ public class ContextPullParser extends KXmlParser implements ILayoutPullParser { mFragmentLayout = null; } + return name; } @Override public String getAttributeValue(String namespace, String localName) { - if (localName.equals(ATTR_LAYOUT) && mFragmentLayout != null) { + if (ATTR_LAYOUT.equals(localName) && mFragmentLayout != null) { return mFragmentLayout; } @@ -119,10 +158,8 @@ public class ContextPullParser extends KXmlParser implements ILayoutPullParser { return VALUE_FILL_PARENT; } - // Handle unicode escapes - if (value != null && value.indexOf('\\') != -1) { - value = AdtUtils.replaceUnicodeEscapes(value); - } + // Handle unicode escapes etc + value = ValueResourceParser.unescapeResourceString(value, false, false); return value; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java index 7efa34a..99549ab 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutContentAssist.java @@ -16,13 +16,48 @@ package com.android.ide.eclipse.adt.internal.editors.layout; +import static com.android.SdkConstants.ANDROID_PKG_PREFIX; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.CLASS_ACTIVITY; +import static com.android.SdkConstants.CLASS_FRAGMENT; +import static com.android.SdkConstants.CLASS_V4_FRAGMENT; +import static com.android.SdkConstants.CLASS_VIEW; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.SdkConstants.VIEW_TAG; + +import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidContentAssist; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService; +import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CustomViewFinder; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.google.common.collect.Lists; +import com.google.common.collect.ObjectArrays; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.w3c.dom.Node; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * Content Assist Processor for /res/layout XML files */ @@ -55,6 +90,145 @@ public final class LayoutContentAssist extends AndroidContentAssist { } } + if (choices == null && parent.length() >= 1 && Character.isLowerCase(parent.charAt(0))) { + // Custom view prefix? + List<ElementDescriptor> descriptors = getCustomViews(); + if (descriptors != null && !descriptors.isEmpty()) { + List<ElementDescriptor> matches = Lists.newArrayList(); + for (ElementDescriptor descriptor : descriptors) { + if (descriptor.getXmlLocalName().startsWith(parent)) { + matches.add(descriptor); + } + } + if (!matches.isEmpty()) { + return matches.toArray(new ElementDescriptor[matches.size()]); + } + } + } + return choices; } + + @Override + protected ElementDescriptor[] getElementChoicesForTextNode(Node parentNode) { + ElementDescriptor[] choices = super.getElementChoicesForTextNode(parentNode); + + // Add in custom views, if any + List<ElementDescriptor> descriptors = getCustomViews(); + if (descriptors != null && !descriptors.isEmpty()) { + ElementDescriptor[] array = descriptors.toArray( + new ElementDescriptor[descriptors.size()]); + choices = ObjectArrays.concat(choices, array, ElementDescriptor.class); + choices = sort(choices); + } + + return choices; + } + + @Nullable + private List<ElementDescriptor> getCustomViews() { + // Add in custom views, if any + IProject project = mEditor.getProject(); + CustomViewFinder finder = CustomViewFinder.get(project); + Collection<String> views = finder.getAllViews(); + if (views == null) { + finder.refresh(); + views = finder.getAllViews(); + } + if (views != null && !views.isEmpty()) { + List<ElementDescriptor> descriptors = Lists.newArrayListWithExpectedSize(views.size()); + CustomViewDescriptorService customViews = CustomViewDescriptorService.getInstance(); + for (String fqcn : views) { + ViewElementDescriptor descriptor = customViews.getDescriptor(project, fqcn); + if (descriptor != null) { + descriptors.add(descriptor); + } + } + + return descriptors; + } + + return null; + } + + @Override + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, + String parentTagName, String attributeName, Node node, String wordPrefix, + boolean skipEndTag, int replaceLength) { + super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node, + wordPrefix, skipEndTag, replaceLength); + + boolean projectOnly = false; + List<String> superClasses = null; + if (VIEW_FRAGMENT.equals(parentTagName) && (attributeName.endsWith(ATTR_NAME) + || attributeName.equals(ATTR_CLASS))) { + // Insert fragment class matches + superClasses = Arrays.asList(CLASS_V4_FRAGMENT, CLASS_FRAGMENT); + } else if (VIEW_TAG.equals(parentTagName) && attributeName.endsWith(ATTR_CLASS)) { + // Insert custom view matches + superClasses = Collections.singletonList(CLASS_VIEW); + projectOnly = true; + } else if (attributeName.endsWith(ATTR_CONTEXT)) { + // Insert activity matches + superClasses = Collections.singletonList(CLASS_ACTIVITY); + } + + if (superClasses != null) { + IProject project = mEditor.getProject(); + if (project == null) { + return false; + } + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + IType type = javaProject.findType(superClasses.get(0)); + Set<IType> elements = new HashSet<IType>(); + if (type != null) { + ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor()); + IType[] allSubtypes = hierarchy.getAllSubtypes(type); + for (IType subType : allSubtypes) { + if (!projectOnly || subType.getResource() != null) { + elements.add(subType); + } + } + } + assert superClasses.size() <= 2; // If more, need to do additional work below + if (superClasses.size() == 2) { + type = javaProject.findType(superClasses.get(1)); + if (type != null) { + ITypeHierarchy hierarchy = type.newTypeHierarchy( + new NullProgressMonitor()); + IType[] allSubtypes = hierarchy.getAllSubtypes(type); + for (IType subType : allSubtypes) { + if (!projectOnly || subType.getResource() != null) { + elements.add(subType); + } + } + } + } + + List<IType> sorted = new ArrayList<IType>(elements); + Collections.sort(sorted, new Comparator<IType>() { + @Override + public int compare(IType type1, IType type2) { + String fqcn1 = type1.getFullyQualifiedName(); + String fqcn2 = type2.getFullyQualifiedName(); + int category1 = fqcn1.startsWith(ANDROID_PKG_PREFIX) ? 1 : -1; + int category2 = fqcn2.startsWith(ANDROID_PKG_PREFIX) ? 1 : -1; + if (category1 != category2) { + return category1 - category2; + } + return fqcn1.compareTo(fqcn2); + } + }); + addMatchingProposals(proposals, sorted.toArray(), offset, node, wordPrefix, + (char) 0, false /* isAttribute */, false /* isNew */, + false /* skipEndTag */, replaceLength); + return true; + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + return false; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java index fc81ac4..1015d7d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditorDelegate.java @@ -44,6 +44,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElement import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; import com.android.ide.eclipse.adt.internal.lint.EclipseLintRunner; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceFolderType; @@ -260,6 +261,9 @@ public class LayoutEditorDelegate extends CommonXmlDelegate if (input instanceof FileEditorInput) { FileEditorInput fileInput = (FileEditorInput)input; editedFile = fileInput.getFile(); + if (!editedFile.isAccessible()) { + return; + } } else { AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s", //$NON-NLS-1$ @@ -753,7 +757,8 @@ public class LayoutEditorDelegate extends CommonXmlDelegate @Override public String delegateGetPartName() { IEditorInput editorInput = getEditor().getEditorInput(); - if (editorInput instanceof IFileEditorInput) { + if (!AdtPrefs.getPrefs().isSharedLayoutEditor() + && editorInput instanceof IFileEditorInput) { IFileEditorInput fileInput = (IFileEditorInput) editorInput; IFile file = fileInput.getFile(); IContainer parent = file.getParent(); @@ -910,6 +915,7 @@ public class LayoutEditorDelegate extends CommonXmlDelegate if (mGraphicalEditor != null) { mGraphicalEditor.onTargetChange(); mGraphicalEditor.reloadPalette(); + mGraphicalEditor.getCanvasControl().syncPreviewMode(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java index d9e798e..4e4429d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java @@ -175,7 +175,7 @@ public final class LayoutReloadMonitor { */ @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { // This listener only cares about .class files and AndroidManifest.xml files if (!(SdkConstants.EXT_CLASS.equals(extension) || SdkConstants.EXT_XML.equals(extension) @@ -186,15 +186,7 @@ public final class LayoutReloadMonitor { // get the file's project IProject project = file.getProject(); - boolean hasAndroidNature = false; - try { - hasAndroidNature = project.hasNature(AdtConstants.NATURE_DEFAULT); - } catch (CoreException e) { - // do nothing if the nature cannot be queried. - return; - } - - if (hasAndroidNature) { + if (isAndroidProject) { // project is an Android project, it's the one being affected // directly by its own file change. processFileChanged(file, project, extension); @@ -204,16 +196,14 @@ public final class LayoutReloadMonitor { for (IProject p : referencingProjects) { try { - hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT); + boolean hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT); + if (hasAndroidNature) { + // the changed project is a dependency on an Android project, + // update the main project. + processFileChanged(file, p, extension); + } } catch (CoreException e) { // do nothing if the nature cannot be queried. - continue; - } - - if (hasAndroidNature) { - // the changed project is a dependency on an Android project, - // update the main project. - processFileChanged(file, p, extension); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java index 98f5317..74c033c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/ProjectCallback.java @@ -18,11 +18,13 @@ package com.android.ide.eclipse.adt.internal.editors.layout; import static com.android.SdkConstants.ANDROID_PKG_PREFIX; import static com.android.SdkConstants.CALENDAR_VIEW; +import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; import static com.android.SdkConstants.FQCN_GRID_VIEW; import static com.android.SdkConstants.FQCN_SPINNER; import static com.android.SdkConstants.GRID_VIEW; import static com.android.SdkConstants.LIST_VIEW; +import static com.android.SdkConstants.SPINNER; import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; @@ -49,18 +51,22 @@ import com.android.ide.eclipse.adt.internal.resources.manager.ProjectClassLoader import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.resources.ResourceType; import com.android.util.Pair; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import org.eclipse.core.resources.IProject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.StringReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -140,6 +146,11 @@ public final class ProjectCallback extends LegacyCallback { throws ClassNotFoundException, Exception { mUsed = true; + if (className == null) { + // Just make a plain <View> if you specify <view> without a class= attribute. + className = CLASS_VIEW; + } + // look for a cached version Class<?> clazz = mLoadedClasses.get(className); if (clazz != null) { @@ -454,12 +465,15 @@ public final class ProjectCallback extends LegacyCallback { ContextPullParser parser = new ContextPullParser(this, xml); try { parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(new FileInputStream(xml), "UTF-8"); //$NON-NLS-1$ + String xmlText = Files.toString(xml, Charsets.UTF_8); + parser.setInput(new StringReader(xmlText)); return parser; } catch (XmlPullParserException e) { AdtPlugin.log(e, null); } catch (FileNotFoundException e) { // Shouldn't happen since we check isFile() above + } catch (IOException e) { + AdtPlugin.log(e, null); } } @@ -560,8 +574,8 @@ public final class ProjectCallback extends LegacyCallback { } @Override - public AdapterBinding getAdapterBinding(final ResourceReference adapterView, final Object adapterCookie, - final Object viewObject) { + public AdapterBinding getAdapterBinding(final ResourceReference adapterView, + final Object adapterCookie, final Object viewObject) { // Look for user-recorded preference for layout to be used for previews if (adapterCookie instanceof UiViewElementNode) { UiViewElementNode uiNode = (UiViewElementNode) adapterCookie; @@ -569,6 +583,13 @@ public final class ProjectCallback extends LegacyCallback { if (binding != null) { return binding; } + } else if (adapterCookie instanceof Map<?,?>) { + @SuppressWarnings("unchecked") + Map<String, String> map = (Map<String, String>) adapterCookie; + AdapterBinding binding = LayoutMetadata.getNodeBinding(viewObject, map); + if (binding != null) { + return binding; + } } if (viewObject == null) { @@ -598,7 +619,7 @@ public final class ProjectCallback extends LegacyCallback { if (listFqcn.endsWith(EXPANDABLE_LIST_VIEW)) { binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_EXPANDABLE_LIST_ITEM, true /* isFramework */, 1)); - } else if (listFqcn.equals(FQCN_SPINNER)) { + } else if (listFqcn.equals(SPINNER)) { binding.addItem(new DataBindingItem(LayoutMetadata.DEFAULT_SPINNER_ITEM, true /* isFramework */, 1)); } else { 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 9553bc8..e8e0d79 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 @@ -36,7 +36,7 @@ import static com.android.SdkConstants.VIEW_INCLUDE; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.ViewInfo; -import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.common.resources.ValueResourceParser; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.FragmentMenu; @@ -79,7 +79,6 @@ public class UiElementPullParser extends BasePullParser { private boolean mIncreaseExistingPadding = false; private LayoutDescriptors mDescriptors; private final Density mDensity; - private final float mXdpi; /** * Number of pixels to pad views with in exploded-rendering mode. @@ -114,18 +113,16 @@ public class UiElementPullParser extends BasePullParser { * nodes are not individually exploded (but they may all be exploded with the * explodeRendering parameter. * @param density 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, - Density density, float xdpi, IProject project) { + Density density, IProject project) { super(); mRoot = top; mExplodedRendering = explodeRendering; mExplodeNodes = explodeNodes; mDensity = density; - mXdpi = xdpi; if (mExplodedRendering) { // get the layout descriptor IAndroidTarget target = Sdk.getCurrent().getTarget(project); @@ -401,10 +398,8 @@ public class UiElementPullParser extends BasePullParser { return VALUE_FILL_PARENT; } - // Handle unicode escapes - if (value.indexOf('\\') != -1) { - value = AdtUtils.replaceUnicodeEscapes(value); - } + // Handle unicode escapes etc + value = ValueResourceParser.unescapeResourceString(value, false, false); return value; } @@ -631,13 +626,13 @@ public class UiElementPullParser extends BasePullParser { f *= (float)mDensity.getDpiValue() / Density.DEFAULT_DENSITY; break; case COMPLEX_UNIT_PT: - f *= mXdpi * (1.0f / 72); + f *= mDensity.getDpiValue() * (1.0f / 72); break; case COMPLEX_UNIT_IN: - f *= mXdpi; + f *= mDensity.getDpiValue(); break; case COMPLEX_UNIT_MM: - f *= mXdpi * (1.0f / 25.4f); + f *= mDensity.getDpiValue() * (1.0f / 25.4f); break; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java index 1f85a32..36cd0fb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ActivityMenuListener.java @@ -88,7 +88,7 @@ class ActivityMenuListener extends SelectionAdapter { if (current != null) { MenuItem item = new MenuItem(menu, SWT.PUSH); - String label = ConfigurationChooser.getActivityLabel(current, true);; + String label = ConfigurationChooser.getActivityLabel(current, true); item.setText( String.format("Open %1$s...", label)); Image image = sharedImages.getImage(ISharedImages.IMG_OBJS_CUNIT); item.setImage(image); @@ -154,7 +154,7 @@ class ActivityMenuListener extends SelectionAdapter { } item.addSelectionListener(new ActivityMenuListener(chooser, - ACTION_OPEN_ACTIVITY, fqcn)); + ACTION_SELECT_ACTIVITY, fqcn)); } return current; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java index 2106f8d..8ca0c26 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/Configuration.java @@ -17,44 +17,84 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.ide.common.api.Rect; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.DeviceConfigHelper; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.LanguageQualifier; import com.android.ide.common.resources.configuration.NightModeQualifier; import com.android.ide.common.resources.configuration.RegionQualifier; -import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; -import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.common.resources.configuration.UiModeQualifier; import com.android.ide.common.resources.configuration.VersionQualifier; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.Density; import com.android.resources.NightMode; -import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenSize; import com.android.resources.UiMode; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.devices.Device; import com.android.sdklib.devices.State; import com.android.utils.Pair; +import com.google.common.base.Objects; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; import java.util.List; +import java.util.Map; /** * A {@linkplain Configuration} is a selection of device, orientation, theme, * etc for use when rendering a layout. */ public class Configuration { + /** The {@link FolderConfiguration} in change flags or override flags */ + public static final int CFG_FOLDER = 1 << 0; + /** The {@link Device} in change flags or override flags */ + public static final int CFG_DEVICE = 1 << 1; + /** The {@link State} in change flags or override flags */ + public static final int CFG_DEVICE_STATE = 1 << 2; + /** The theme in change flags or override flags */ + public static final int CFG_THEME = 1 << 3; + /** The locale in change flags or override flags */ + public static final int CFG_LOCALE = 1 << 4; + /** The rendering {@link IAndroidTarget} in change flags or override flags */ + public static final int CFG_TARGET = 1 << 5; + /** The {@link NightMode} in change flags or override flags */ + public static final int CFG_NIGHT_MODE = 1 << 6; + /** The {@link UiMode} in change flags or override flags */ + public static final int CFG_UI_MODE = 1 << 7; + /** The {@link UiMode} in change flags or override flags */ + public static final int CFG_ACTIVITY = 1 << 8; + + /** References all attributes */ + public static final int MASK_ALL = 0xFFFF; + + /** Attributes which affect which best-layout-file selection */ + public static final int MASK_FILE_ATTRS = + CFG_DEVICE|CFG_DEVICE_STATE|CFG_LOCALE|CFG_TARGET|CFG_NIGHT_MODE|CFG_UI_MODE; + + /** Attributes which affect rendering appearance */ + public static final int MASK_RENDERING = MASK_FILE_ATTRS|CFG_THEME; + /** * Setting name for project-wide setting controlling rendering target and locale which * is shared for all files @@ -68,7 +108,7 @@ public class Configuration { private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ @NonNull - protected final ConfigurationChooser mConfigChooser; + protected ConfigurationChooser mConfigChooser; /** The {@link FolderConfiguration} representing the state of the UI controls */ @NonNull @@ -113,6 +153,9 @@ public class Configuration { @NonNull private NightMode mNightMode = NightMode.NOTNIGHT; + /** The display name */ + private String mDisplayName; + /** * Creates a new {@linkplain Configuration} * @@ -123,6 +166,30 @@ public class Configuration { } /** + * Sets the associated configuration chooser + * + * @param chooser the chooser + */ + void setChooser(@NonNull ConfigurationChooser chooser) { + // TODO: We should get rid of the binding between configurations + // and configuration choosers. This is currently needed because + // the choosers contain vital data such as the set of available + // rendering targets, the set of available locales etc, which + // also doesn't belong inside the configuration but is needed by it. + mConfigChooser = chooser; + } + + /** + * Gets the associated configuration chooser + * + * @return the chooser + */ + @NonNull + ConfigurationChooser getChooser() { + return mConfigChooser; + } + + /** * Creates a new {@linkplain Configuration} * * @param chooser the associated chooser @@ -134,6 +201,60 @@ public class Configuration { } /** + * Creates a configuration suitable for the given file + * + * @param base the base configuration to base the file configuration off of + * @param file the file to look up a configuration for + * @return a suitable configuration + */ + @NonNull + public static Configuration create( + @NonNull Configuration base, + @NonNull IFile file) { + Configuration configuration = copy(base); + ConfigurationChooser chooser = base.getChooser(); + ProjectResources resources = chooser.getResources(); + ConfigurationMatcher matcher = new ConfigurationMatcher(chooser, configuration, file, + resources, false); + + ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file); + configuration.mEditedConfig = new FolderConfiguration(); + configuration.mEditedConfig.set(resFolder.getConfiguration()); + + matcher.adaptConfigSelection(true /*needBestMatch*/); + configuration.syncFolderConfig(); + + return configuration; + } + + /** + * Creates a new {@linkplain Configuration} that is a copy from a different configuration + * + * @param original the original to copy from + * @return a new configuration copied from the original + */ + @NonNull + public static Configuration copy(@NonNull Configuration original) { + Configuration copy = create(original.mConfigChooser); + copy.mFullConfig.set(original.mFullConfig); + if (original.mEditedConfig != null) { + copy.mEditedConfig = new FolderConfiguration(); + copy.mEditedConfig.set(original.mEditedConfig); + } + copy.mTarget = original.getTarget(); + copy.mTheme = original.getTheme(); + copy.mDevice = original.getDevice(); + copy.mState = original.getDeviceState(); + copy.mActivity = original.getActivity(); + copy.mLocale = original.getLocale(); + copy.mUiMode = original.getUiMode(); + copy.mNightMode = original.getNightMode(); + copy.mDisplayName = original.getDisplayName(); + + return copy; + } + + /** * Returns the associated activity * * @return the activity @@ -214,6 +335,16 @@ public class Configuration { } /** + * Returns the display name to show for this configuration + * + * @return the display name, or null if none has been assigned + */ + @Nullable + public String getDisplayName() { + return mDisplayName; + } + + /** * Returns whether the configuration's theme is a project theme. * <p/> * The returned value is meaningless if {@link #getTheme()} returns @@ -359,6 +490,15 @@ public class Configuration { } /** + * Sets the display name to be shown for this configuration. + * + * @param displayName the new display name + */ + public void setDisplayName(@Nullable String displayName) { + mDisplayName = displayName; + } + + /** * Sets the night mode * * @param night the night mode @@ -397,6 +537,7 @@ public class Configuration { */ public void setTheme(String theme) { mTheme = theme; + checkThemePrefix(); } /** @@ -507,6 +648,67 @@ public class Configuration { return sb.toString(); } + /** Returns the preferred theme, or null */ + @Nullable + String computePreferredTheme() { + IProject project = mConfigChooser.getProject(); + ManifestInfo manifest = ManifestInfo.get(project); + + // Look up the screen size for the current state + ScreenSize screenSize = null; + Device device = getDevice(); + if (device != null) { + List<State> states = device.getAllStates(); + for (State state : states) { + FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state); + if (folderConfig != null) { + ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); + screenSize = qualifier.getValue(); + break; + } + } + } + + // Look up the default/fallback theme to use for this project (which + // depends on the screen size when no particular theme is specified + // in the manifest) + String defaultTheme = manifest.getDefaultTheme(getTarget(), screenSize); + + String preferred = defaultTheme; + if (getTheme() == null) { + // If we are rendering a layout in included context, pick the theme + // from the outer layout instead + + String activity = getActivity(); + if (activity != null) { + Map<String, String> activityThemes = manifest.getActivityThemes(); + preferred = activityThemes.get(activity); + } + if (preferred == null) { + preferred = defaultTheme; + } + setTheme(preferred); + } + + return preferred; + } + + private void checkThemePrefix() { + if (mTheme != null && !mTheme.startsWith(PREFIX_RESOURCE_REF)) { + if (mTheme.isEmpty()) { + computePreferredTheme(); + return; + } + ResourceRepository frameworkRes = mConfigChooser.getClient().getFrameworkResources(); + if (frameworkRes != null + && frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + mTheme)) { + mTheme = ANDROID_STYLE_RESOURCE_PREFIX + mTheme; + } else { + mTheme = STYLE_RESOURCE_PREFIX + mTheme; + } + } + } + /** * Initializes a string previously created with * {@link #toPersistentString()} @@ -555,6 +757,8 @@ public class Configuration { } else if (mTheme.startsWith(MARKER_PROJECT)) { mTheme = STYLE_RESOURCE_PREFIX + mTheme.substring(MARKER_PROJECT.length()); + } else { + checkThemePrefix(); } mUiMode = UiMode.getEnum(values[4]); @@ -604,7 +808,7 @@ public class Configuration { @Nullable static Pair<Locale, IAndroidTarget> loadRenderState(ConfigurationChooser chooser) { IProject project = chooser.getProject(); - if (!project.isAccessible()) { + if (project == null || !project.isAccessible()) { return null; } @@ -629,24 +833,31 @@ public class Configuration { } locale = Locale.create(language, region); - target = stringToTarget(chooser, values[1]); - - // See if we should "correct" the rendering target to a better version. - // If you're using a pre-release version of the render target, and a - // final release is available and installed, we should switch to that - // one instead. - if (target != null) { - AndroidVersion version = target.getVersion(); - List<IAndroidTarget> targetList = chooser.getTargetList(); - if (version.getCodename() != null && targetList != null) { - int targetApiLevel = version.getApiLevel() + 1; - for (IAndroidTarget t : targetList) { - if (t.getVersion().getApiLevel() == targetApiLevel - && t.isPlatform()) { - target = t; - break; + if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) { + target = ConfigurationMatcher.findDefaultRenderTarget(chooser); + } else { + String targetString = values[1]; + target = stringToTarget(chooser, targetString); + // See if we should "correct" the rendering target to a + // better version. If you're using a pre-release version + // of the render target, and a final release is + // available and installed, we should switch to that + // one instead. + if (target != null) { + AndroidVersion version = target.getVersion(); + List<IAndroidTarget> targetList = chooser.getTargetList(); + if (version.getCodename() != null && targetList != null) { + int targetApiLevel = version.getApiLevel() + 1; + for (IAndroidTarget t : targetList) { + if (t.getVersion().getApiLevel() == targetApiLevel + && t.isPlatform()) { + target = t; + break; + } } } + } else { + target = ConfigurationMatcher.findDefaultRenderTarget(chooser); } } } @@ -654,7 +865,7 @@ public class Configuration { return Pair.of(locale, target); } - return Pair.of(Locale.ANY, ConfigurationMatcher.findDefaultRenderTarget(project)); + return Pair.of(Locale.ANY, ConfigurationMatcher.findDefaultRenderTarget(chooser)); } catch (CoreException e) { AdtPlugin.log(e, null); } @@ -668,9 +879,12 @@ public class Configuration { */ void saveRenderState() { IProject project = mConfigChooser.getProject(); + if (project == null) { + return; + } try { // Generate a persistent string from locale+target - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(32); Locale locale = getLocale(); if (locale != null) { // locale[0]/[1] can be null sometimes when starting Eclipse @@ -700,7 +914,7 @@ public class Configuration { * @return an id for the given target; never null */ @NonNull - private static String targetToString(@NonNull IAndroidTarget target) { + public static String targetToString(@NonNull IAndroidTarget target) { return target.getFullName().replace(SEP, ""); //$NON-NLS-1$ } @@ -715,7 +929,7 @@ public class Configuration { * @return an {@link IAndroidTarget} that matches the given id, or null */ @Nullable - private static IAndroidTarget stringToTarget( + public static IAndroidTarget stringToTarget( @NonNull ConfigurationChooser chooser, @NonNull String id) { List<IAndroidTarget> targetList = chooser.getTargetList(); @@ -731,6 +945,30 @@ public class Configuration { } /** + * Returns an {@link IAndroidTarget} that corresponds to the given id that was + * originally returned by {@link #targetToString}. May be null, if the platform is no + * longer available, or if the platform list has not yet been initialized. + * + * @param id the id that corresponds to the desired platform + * @return an {@link IAndroidTarget} that matches the given id, or null + */ + @Nullable + public static IAndroidTarget stringToTarget( + @NonNull String id) { + Sdk currentSdk = Sdk.getCurrent(); + if (currentSdk != null) { + IAndroidTarget[] targets = currentSdk.getTargets(); + for (IAndroidTarget target : targets) { + if (id.equals(targetToString(target))) { + return target; + } + } + } + + return null; + } + + /** * Returns the {@link State} by the given name for the given {@link Device} * * @param device the device @@ -773,96 +1011,6 @@ public class Configuration { } /** - * Returns the current device xdpi. - * - * @return the x dpi as a float - */ - public float getXDpi() { - Device device = getDevice(); - if (device != null) { - State currState = getDeviceState(); - if (currState == null) { - currState = device.getDefaultState(); - } - float dpi = (float) currState.getHardware().getScreen().getXdpi(); - if (!Float.isNaN(dpi)) { - return dpi; - } - } - - // get the pixel density as the density. - return getDensity().getDpiValue(); - } - - /** - * Returns the current device ydpi. - * - * @return the y dpi as a float - */ - public float getYDpi() { - Device device = getDevice(); - if (device != null) { - State currState = getDeviceState(); - if (currState == null) { - currState = device.getDefaultState(); - } - float dpi = (float) currState.getHardware().getScreen().getYdpi(); - if (!Float.isNaN(dpi)) { - return dpi; - } - } - - // get the pixel density as the density. - return getDensity().getDpiValue(); - } - - /** - * Returns the bounds of the screen - * - * @return the screen bounds - */ - public Rect getScreenBounds() { - return getScreenBounds(mFullConfig); - } - - /** - * Gets the orientation from the given configuration - * - * @param config the configuration to look up - * @return the bounds - */ - @NonNull - public static Rect getScreenBounds(FolderConfiguration config) { - // get the orientation from the given device config - ScreenOrientationQualifier qual = config.getScreenOrientationQualifier(); - ScreenOrientation orientation = ScreenOrientation.PORTRAIT; - if (qual != null) { - orientation = qual.getValue(); - } - - // get the device screen dimension - ScreenDimensionQualifier qual2 = config.getScreenDimensionQualifier(); - int s1, s2; - if (qual2 != null) { - s1 = qual2.getValue1(); - s2 = qual2.getValue2(); - } else { - s1 = 480; - s2 = 320; - } - - switch (orientation) { - default: - case PORTRAIT: - return new Rect(0, 0, s2, s1); - case LANDSCAPE: - return new Rect(0, 0, s1, s2); - case SQUARE: - return new Rect(0, 0, s1, s1); - } - } - - /** * Get the next cyclical state after the given state * * @param from the state to start with @@ -884,8 +1032,27 @@ public class Configuration { return null; } + /** + * Returns true if this configuration supports the given rendering + * capability + * + * @param capability the capability to check + * @return true if the capability is supported + */ + public boolean supports(Capability capability) { + IAndroidTarget target = getTarget(); + if (target != null) { + return RenderService.supports(target, capability); + } + + return false; + } + @Override public String toString() { - return toPersistentString(); + return Objects.toStringHelper(this.getClass()) + .add("display", getDisplayName()) //$NON-NLS-1$ + .add("persistent", toPersistentString()) //$NON-NLS-1$ + .toString(); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java index b512bcc..d4cc6df 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationChooser.java @@ -19,24 +19,25 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.ATTR_CONTEXT; -import static com.android.SdkConstants.FD_RES_LAYOUT; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.RES_QUALIFIER_SEP; import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; import static com.android.SdkConstants.TOOLS_URI; import static com.android.ide.eclipse.adt.AdtUtils.isUiThread; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_ALL; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_DEVICE; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_DEVICE_CONFIG; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_FOLDER; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_LOCALE; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_RENDER_TARGET; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient.CHANGED_THEME; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_LOCALE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_THEME; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL; +import static com.google.common.base.Objects.equal; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.ide.common.resources.ResourceFile; import com.android.ide.common.resources.ResourceFolder; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.configuration.DeviceConfigHelper; @@ -44,12 +45,17 @@ import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.LanguageQualifier; import com.android.ide.common.resources.configuration.RegionQualifier; import com.android.ide.common.resources.configuration.ResourceQualifier; -import com.android.ide.common.resources.configuration.ScreenSizeQualifier; import com.android.ide.common.sdk.LoadStatus; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlDelegate; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; @@ -58,12 +64,11 @@ import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceType; import com.android.resources.ScreenOrientation; -import com.android.resources.ScreenSize; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.devices.Device; import com.android.sdklib.devices.DeviceManager; -import com.android.sdklib.devices.DeviceManager.DevicesChangeListener; +import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; import com.android.sdklib.devices.State; import com.android.utils.Pair; import com.google.common.base.Objects; @@ -72,7 +77,6 @@ import com.google.common.base.Strings; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.QualifiedName; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; @@ -87,10 +91,12 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.ui.IEditorPart; import org.w3c.dom.Document; import org.w3c.dom.Element; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; @@ -102,14 +108,7 @@ import java.util.SortedSet; * {@link Configuration} by configuring various constraints. */ public class ConfigurationChooser extends Composite - implements DevicesChangeListener, DisposeListener { - /** - * Settings name for file-specific configuration preferences, such as which theme or - * device to render the current layout with - */ - public final static QualifiedName NAME_CONFIG_STATE = - new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ - + implements DevicesChangedListener, DisposeListener { private static final String ICON_SQUARE = "square"; //$NON-NLS-1$ private static final String ICON_LANDSCAPE = "landscape"; //$NON-NLS-1$ private static final String ICON_PORTRAIT = "portrait"; //$NON-NLS-1$ @@ -205,8 +204,8 @@ public class ConfigurationChooser extends Composite ToolBar toolBar = new ToolBar(this, SWT.WRAP | SWT.FLAT | SWT.RIGHT | SWT.HORIZONTAL); toolBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); - mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN | SWT.BOLD); - mConfigCombo.setImage(null); + mConfigCombo = new ToolItem(toolBar, SWT.DROP_DOWN ); + mConfigCombo.setImage(icons.getIcon("android_file")); //$NON-NLS-1$ mConfigCombo.setToolTipText("Configuration to render this layout with in Eclipse"); @SuppressWarnings("unused") @@ -296,21 +295,47 @@ public class ConfigurationChooser extends Composite mOrientationCombo.addSelectionListener(listener); addDisposeListener(this); + + initDevices(); + initTargets(); } - IFile getEditedFile() { + /** + * Returns the edited file + * + * @return the file + */ + @Nullable + public IFile getEditedFile() { return mEditedFile; } - IProject getProject() { - return mEditedFile.getProject(); + /** + * Returns the project of the edited file + * + * @return the project + */ + @Nullable + public IProject getProject() { + if (mEditedFile != null) { + return mEditedFile.getProject(); + } else { + return null; + } } ConfigurationClient getClient() { return mClient; } - ProjectResources getResources() { + /** + * Returns the project resources for the project being configured by this + * chooser + * + * @return the project resources + */ + @Nullable + public ProjectResources getResources() { return mResources; } @@ -328,7 +353,7 @@ public class ConfigurationChooser extends Composite * * @return the project target */ - IAndroidTarget getProjectTarget() { + public IAndroidTarget getProjectTarget() { return mProjectTarget; } @@ -462,6 +487,7 @@ public class ConfigurationChooser extends Composite */ public void setFile(IFile file) { mEditedFile = file; + ensureInitialized(); } /** @@ -478,7 +504,7 @@ public class ConfigurationChooser extends Composite return; } - mEditedFile = file; + setFile(file); IProject project = mEditedFile.getProject(); mResources = ResourceManager.getInstance().getProjectResources(project); @@ -491,6 +517,7 @@ public class ConfigurationChooser extends Composite try { // only attempt to do anything if the SDK and targets are loaded. LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); + if (sdkStatus == LoadStatus.LOADED) { setVisible(true); @@ -509,6 +536,8 @@ public class ConfigurationChooser extends Composite selectConfiguration(mConfiguration.getEditedConfig()); updateActivity(); } + } else if (sdkStatus == LoadStatus.FAILED) { + setVisible(true); } } finally { mDisableUpdates--; @@ -522,7 +551,7 @@ public class ConfigurationChooser extends Composite * @see #replaceFile(IFile) */ public void changeFileOnNewConfig(IFile file) { - mEditedFile = file; + setFile(file); IProject project = mEditedFile.getProject(); mResources = ResourceManager.getInstance().getProjectResources(project); @@ -561,8 +590,13 @@ public class ConfigurationChooser extends Composite if (resFolder != null) { mConfiguration.setEditedConfig(resFolder.getConfiguration()); } else { - mConfiguration.setEditedConfig(FolderConfiguration.getConfig( - parent.getName().split(RES_QUALIFIER_SEP))); + FolderConfiguration config = FolderConfiguration.getConfig( + parent.getName().split(RES_QUALIFIER_SEP)); + if (config != null) { + mConfiguration.setEditedConfig(config); + } else { + mConfiguration.setEditedConfig(new FolderConfiguration()); + } } onXmlModelLoaded(); @@ -577,15 +611,12 @@ public class ConfigurationChooser extends Composite */ public void setConfiguration(@NonNull Configuration configuration) { if (mClient != null) { - mClient.aboutToChange(CHANGED_ALL); + mClient.aboutToChange(MASK_ALL); } Configuration oldConfiguration = mConfiguration; mConfiguration = configuration; - - if (mClient != null) { - mClient.changed(CHANGED_ALL); - } + mConfiguration.setChooser(this); selectTheme(configuration.getTheme()); selectLocale(configuration.getLocale()); @@ -596,7 +627,13 @@ public class ConfigurationChooser extends Composite // This may be a second refresh after triggered by theme above if (mClient != null) { - boolean accepted = mClient.changed(CHANGED_ALL); + LayoutCanvas canvas = mClient.getCanvas(); + if (canvas != null) { + assert mConfiguration != oldConfiguration; + canvas.getPreviewManager().updateChooserConfig(oldConfiguration, mConfiguration); + } + + boolean accepted = mClient.changed(MASK_ALL); if (!accepted) { configuration = oldConfiguration; selectTheme(configuration.getTheme()); @@ -605,7 +642,22 @@ public class ConfigurationChooser extends Composite selectDeviceState(configuration.getDeviceState()); selectTarget(configuration.getTarget()); selectActivity(configuration.getActivity()); + if (canvas != null && mConfiguration != oldConfiguration) { + canvas.getPreviewManager().updateChooserConfig(mConfiguration, + oldConfiguration); + } return; + } else { + int changed = 0; + if (!equal(oldConfiguration.getTheme(), mConfiguration.getTheme())) { + changed |= CFG_THEME; + } + if (!equal(oldConfiguration.getDevice(), mConfiguration.getDevice())) { + changed |= CFG_DEVICE | CFG_DEVICE_STATE; + } + if (changed != 0) { + syncToVariations(changed, mEditedFile, mConfiguration, false, true); + } } } @@ -627,10 +679,9 @@ public class ConfigurationChooser extends Composite mDisableUpdates++; // we do not want to trigger onXXXChange when setting // new values in the widgets. try { - // this is going to be followed by a call to onTargetLoaded. - // So we can only care about the layout devices in this case. - initDevices(); - initTargets(); + updateDevices(); + updateTargets(); + ensureInitialized(); } finally { mDisableUpdates--; } @@ -668,8 +719,9 @@ public class ConfigurationChooser extends Composite try { // init the devices if needed (new SDK or first time going through here) if (mSdkChanged) { - initDevices(); - initTargets(); + updateDevices(); + updateTargets(); + ensureInitialized(); mSdkChanged = false; } @@ -683,7 +735,8 @@ public class ConfigurationChooser extends Composite LoadStatus targetStatus = LoadStatus.FAILED; if (mProjectTarget != null) { targetStatus = Sdk.getCurrent().checkAndLoadTargetData(mProjectTarget, null); - initTargets(); + updateTargets(); + ensureInitialized(); } if (targetStatus == LoadStatus.LOADED) { @@ -697,24 +750,22 @@ public class ConfigurationChooser extends Composite if (resFolder != null) { mConfiguration.setEditedConfig(resFolder.getConfiguration()); } else { - mConfiguration.setEditedConfig(FolderConfiguration.getConfig( - parent.getName().split(RES_QUALIFIER_SEP))); + FolderConfiguration config = FolderConfiguration.getConfig( + parent.getName().split(RES_QUALIFIER_SEP)); + if (config != null) { + mConfiguration.setEditedConfig(config); + } else { + mConfiguration.setEditedConfig(new FolderConfiguration()); + } } } targetData = Sdk.getCurrent().getTargetData(mProjectTarget); // get the file stored state - boolean loadedConfigData = false; - String data = AdtPlugin.getFileProperty(mEditedFile, NAME_CONFIG_STATE); - if (mInitialState != null) { - data = mInitialState; - mInitialState = null; - } - - if (data != null) { - loadedConfigData = mConfiguration.initialize(data); - } + ensureInitialized(); + boolean loadedConfigData = mConfiguration.getDevice() != null && + mConfiguration.getDeviceState() != null; // Load locale list. This must be run after we initialize the // configuration above, since it attempts to sync the UI with @@ -741,11 +792,11 @@ public class ConfigurationChooser extends Composite matcher.findAndSetCompatibleConfig(false); // Default to modern layout lib - IProject p = mEditedFile.getProject(); - IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(p); + IAndroidTarget target = ConfigurationMatcher.findDefaultRenderTarget(this); if (target != null) { targetData = Sdk.getCurrent().getTargetData(target); selectTarget(target); + mConfiguration.setTarget(target, true); } } @@ -768,6 +819,8 @@ public class ConfigurationChooser extends Composite // compute the final current config mConfiguration.syncFolderConfig(); + } else if (targetStatus == LoadStatus.FAILED) { + setVisible(true); } } finally { mDisableUpdates--; @@ -778,6 +831,19 @@ public class ConfigurationChooser extends Composite } /** + * This is a temporary workaround for a infrequently happening bug; apparently + * there are cases where the configuration chooser isn't shown + */ + public void ensureVisible() { + if (!isVisible()) { + LoadStatus sdkStatus = AdtPlugin.getDefault().getSdkLoadStatus(); + if (sdkStatus == LoadStatus.LOADED) { + onXmlModelLoaded(); + } + } + } + + /** * An alternate layout for this layout has been created. This means that the * current layout may no longer be a best fit. However, since we support multiple * layouts being open at the same time, we need to adjust the current configuration @@ -790,7 +856,7 @@ public class ConfigurationChooser extends Composite matcher.adaptConfigSelection(true /*needBestMatch*/); mConfiguration.syncFolderConfig(); if (mClient != null) { - mClient.changed(CHANGED_ALL); + mClient.changed(MASK_ALL); } } } @@ -801,69 +867,93 @@ public class ConfigurationChooser extends Composite private void initDevices() { final Sdk sdk = Sdk.getCurrent(); if (sdk != null) { - mDeviceList = sdk.getDevices(); DeviceManager manager = sdk.getDeviceManager(); // This method can be called more than once, so avoid duplicate entries manager.unregisterListener(this); manager.registerListener(this); + mDeviceList = manager.getDevices(DeviceManager.ALL_DEVICES); } else { mDeviceList = new ArrayList<Device>(); } - - // fill with the devices - if (!mDeviceList.isEmpty()) { - Device first = mDeviceList.get(0); - selectDevice(first); - List<State> states = first.getAllStates(); - selectDeviceState(states.get(0)); - } else { - selectDevice(null); - } } /** * Loads the list of {@link IAndroidTarget} and inits the UI with it. */ - private void initTargets() { + private boolean initTargets() { mTargetList.clear(); - IAndroidTarget renderingTarget = mConfiguration.getTarget(); - Sdk currentSdk = Sdk.getCurrent(); if (currentSdk != null) { IAndroidTarget[] targets = currentSdk.getTargets(); - IAndroidTarget match = null; for (int i = 0 ; i < targets.length; i++) { - // FIXME: add check based on project minSdkVersion if (targets[i].hasRenderingLibrary()) { mTargetList.add(targets[i]); - - if (renderingTarget != null) { - // use equals because the rendering could be from a previous SDK, so - // it may not be the same instance. - if (renderingTarget.equals(targets[i])) { - match = targets[i]; - } - } else if (mProjectTarget == targets[i]) { - match = targets[i]; - } } } - if (match == null) { - selectTarget(null); + return true; + } - // the rendering target is the same as the project. - renderingTarget = mProjectTarget; - } else { - selectTarget(match); + return false; + } - // set the rendering target to the new object. - renderingTarget = match; + /** Ensures that the configuration has been initialized */ + public void ensureInitialized() { + if (mConfiguration.getDevice() == null && mEditedFile != null) { + String data = ConfigurationDescription.getDescription(mEditedFile); + if (mInitialState != null) { + data = mInitialState; + mInitialState = null; + } + if (data != null) { + mConfiguration.initialize(data); + mConfiguration.syncFolderConfig(); } } } + private void updateDevices() { + if (mDeviceList.size() == 0) { + initDevices(); + } + } + + private void updateTargets() { + if (mTargetList.size() == 0) { + if (!initTargets()) { + return; + } + } + + IAndroidTarget renderingTarget = mConfiguration.getTarget(); + + IAndroidTarget match = null; + for (IAndroidTarget target : mTargetList) { + if (renderingTarget != null) { + // use equals because the rendering could be from a previous SDK, so + // it may not be the same instance. + if (renderingTarget.equals(target)) { + match = target; + } + } else if (mProjectTarget == target) { + match = target; + } + + } + + if (match == null) { + // the rendering target is the same as the project. + renderingTarget = mProjectTarget; + } else { + // set the rendering target to the new object. + renderingTarget = match; + } + + mConfiguration.setTarget(renderingTarget, true); + selectTarget(renderingTarget); + } + /** Update the toolbar whenever a label has changed, to not only * cause the layout in the current toolbar to update, but to possibly * wrap the toolbars and update the layout of the surrounding area. @@ -925,7 +1015,9 @@ public class ConfigurationChooser extends Composite */ public void saveConstraints() { String description = mConfiguration.toPersistentString(); - AdtPlugin.setFileProperty(mEditedFile, NAME_CONFIG_STATE, description); + if (description != null && !description.isEmpty()) { + ConfigurationDescription.setDescription(mEditedFile, description); + } } // ---- Setting the current UI state ---- @@ -1043,6 +1135,11 @@ public class ConfigurationChooser extends Composite } private void selectConfiguration(FolderConfiguration fileConfig) { + /* For now, don't show any text in the configuration combo, use just an + icon. This has the advantage that the configuration contents don't + shift around, so you can for example click back and forth between + portrait and landscape without the icon moving under the mouse. + If this works well, remove this whole method post ADT 21. assert isUiThread(); try { String current = mEditedFile.getParent().getName(); @@ -1059,6 +1156,7 @@ public class ConfigurationChooser extends Composite } finally { mDisableUpdates--; } + */ } /** @@ -1264,12 +1362,16 @@ public class ConfigurationChooser extends Composite } } - // ---- Implements DevicesChangeListener ---- + // ---- Implements DevicesChangedListener ---- @Override - public void onDevicesChange() { + public void onDevicesChanged() { final Sdk sdk = Sdk.getCurrent(); - mDeviceList = sdk.getDevices(); + if (sdk != null) { + mDeviceList = sdk.getDeviceManager().getDevices(DeviceManager.ALL_DEVICES); + } else { + mDeviceList = new ArrayList<Device>(); + } } // ---- Reacting to UI changes ---- @@ -1303,7 +1405,8 @@ public class ConfigurationChooser extends Composite mConfiguration.syncFolderConfig(); // Notify - boolean accepted = mClient.changed(CHANGED_DEVICE | CHANGED_DEVICE_CONFIG); + IFile file = mEditedFile; + boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE); if (!accepted) { mConfiguration.setDevice(prevDevice, true); mConfiguration.setDeviceState(prevState, true); @@ -1311,12 +1414,75 @@ public class ConfigurationChooser extends Composite selectDevice(prevDevice); selectDeviceState(prevState); return; + } else { + syncToVariations(CFG_DEVICE | CFG_DEVICE_STATE, file, mConfiguration, false, true); } saveConstraints(); } /** + * Synchronizes changes to the given attributes (indicated by the mask + * referencing the {@code CFG_} configuration attribute bit flags in + * {@link Configuration} to the layout variations of the given updated file. + * + * @param flags the attributes which were updated + * @param updatedFile the file which was updated + * @param base the base configuration to base the chooser off of + * @param includeSelf whether the updated file itself should be updated + * @param async whether the updates should be performed asynchronously + */ + public void syncToVariations( + final int flags, + final @NonNull IFile updatedFile, + final @NonNull Configuration base, + final boolean includeSelf, + boolean async) { + if (async) { + getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + doSyncToVariations(flags, updatedFile, includeSelf, base); + } + }); + } else { + doSyncToVariations(flags, updatedFile, includeSelf, base); + } + } + + private void doSyncToVariations(int flags, IFile updatedFile, boolean includeSelf, + Configuration base) { + // Synchronize the given changes to other configurations as well + List<IFile> files = AdtUtils.getResourceVariations(updatedFile, includeSelf); + for (IFile file : files) { + Configuration configuration = Configuration.create(base, file); + configuration.setTheme(base.getTheme()); + configuration.setActivity(base.getActivity()); + Collection<IEditorPart> editors = AdtUtils.findEditorsFor(file, false); + boolean found = false; + for (IEditorPart editor : editors) { + if (editor instanceof CommonXmlEditor) { + CommonXmlDelegate delegate = ((CommonXmlEditor) editor).getDelegate(); + if (delegate instanceof LayoutEditorDelegate) { + editor = ((LayoutEditorDelegate) delegate).getGraphicalEditor(); + } + } + if (editor instanceof GraphicalEditorPart) { + ConfigurationChooser chooser = + ((GraphicalEditorPart) editor).getConfigurationChooser(); + chooser.setConfiguration(configuration); + found = true; + } + } + if (!found) { + // Just update the file persistence + String description = configuration.toPersistentString(); + ConfigurationDescription.setDescription(file, description); + } + } + } + + /** * Called when the device config selection changes. */ void onDeviceConfigChange() { @@ -1331,7 +1497,7 @@ public class ConfigurationChooser extends Composite mConfiguration.setDeviceState(state, false); if (mClient != null) { - boolean accepted = mClient.changed(CHANGED_DEVICE | CHANGED_DEVICE_CONFIG); + boolean accepted = mClient.changed(CFG_DEVICE | CFG_DEVICE_STATE); if (!accepted) { mConfiguration.setDeviceState(prev, false); selectDeviceState(prev); @@ -1360,7 +1526,7 @@ public class ConfigurationChooser extends Composite mConfiguration.setLocale(locale, false); if (mClient != null) { - boolean accepted = mClient.changed(CHANGED_LOCALE); + boolean accepted = mClient.changed(CFG_LOCALE); if (!accepted) { mConfiguration.setLocale(prev, false); selectLocale(prev); @@ -1381,11 +1547,14 @@ public class ConfigurationChooser extends Composite mConfiguration.setTheme((String) mThemeCombo.getData()); if (mClient != null) { - boolean accepted = mClient.changed(CHANGED_THEME); + boolean accepted = mClient.changed(CFG_THEME); if (!accepted) { mConfiguration.setTheme(prev); selectTheme(prev); return; + } else { + syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, mEditedFile, mConfiguration, + false, true); } } @@ -1397,7 +1566,7 @@ public class ConfigurationChooser extends Composite return; } - if (mClient.changed(CHANGED_FOLDER)) { + if (mClient.changed(CFG_FOLDER)) { saveConstraints(); } } @@ -1450,7 +1619,7 @@ public class ConfigurationChooser extends Composite // tell the listener a new rendering target is being set. Need to do this before updating // mRenderingTarget. if (prevTarget != null) { - changeFlags |= CHANGED_RENDER_TARGET; + changeFlags |= CFG_TARGET; mClient.aboutToChange(changeFlags); } @@ -1465,12 +1634,12 @@ public class ConfigurationChooser extends Composite // updateThemes may change the theme (based on theme availability in the new rendering // target) so mark theme change if necessary if (!Objects.equal(oldTheme, mConfiguration.getTheme())) { - changeFlags |= CHANGED_THEME; + changeFlags |= CFG_THEME; } if (target != null) { - changeFlags |= CHANGED_RENDER_TARGET; - changeFlags |= CHANGED_FOLDER; // In case we added a -vNN qualifier + changeFlags |= CFG_TARGET; + changeFlags |= CFG_FOLDER; // In case we added a -vNN qualifier } // Store project-wide render-target setting @@ -1518,7 +1687,7 @@ public class ConfigurationChooser extends Composite if (locale != null) { boolean localeChanged = setLocale(locale); if (localeChanged) { - changeFlags |= CHANGED_LOCALE; + changeFlags |= CFG_LOCALE; } } else { locale = Locale.ANY; @@ -1531,7 +1700,7 @@ public class ConfigurationChooser extends Composite IAndroidTarget target = pair != null ? pair.getSecond() : configurationTarget; if (target != null && configurationTarget != target) { if (mClient != null && configurationTarget != null) { - changeFlags |= CHANGED_RENDER_TARGET; + changeFlags |= CFG_TARGET; mClient.aboutToChange(changeFlags); } @@ -1552,7 +1721,7 @@ public class ConfigurationChooser extends Composite // Compute the new configuration; we want to do this both for locale changes // and for render targets. mConfiguration.syncFolderConfig(); - changeFlags |= CHANGED_FOLDER; // in case we added/remove a -v<NN> qualifier + changeFlags |= CFG_FOLDER; // in case we added/remove a -v<NN> qualifier if (renderTargetChanged) { // force a theme update to reflect the new rendering target. @@ -1588,7 +1757,7 @@ public class ConfigurationChooser extends Composite String theme = mConfiguration.getTheme(); if (theme == null || theme.isEmpty() || mClient.getIncludedWithin() != null) { mConfiguration.setTheme(null); - computePreferredTheme(); + mConfiguration.computePreferredTheme(); } assert mConfiguration.getTheme() != null; } @@ -1680,6 +1849,14 @@ public class ConfigurationChooser extends Composite break; } } + if (!theme.startsWith(PREFIX_RESOURCE_REF)) { + // Arbitrary guess + if (theme.startsWith("Theme.")) { + theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; + } else { + theme = STYLE_RESOURCE_PREFIX + theme; + } + } } // TODO: Handle the case where you have a theme persisted that isn't available?? @@ -1748,55 +1925,6 @@ public class ConfigurationChooser extends Composite } } - /** Returns the preferred theme, or null */ - @Nullable - String computePreferredTheme() { - if (mClient == null) { - return null; - } - - IProject project = mEditedFile.getProject(); - ManifestInfo manifest = ManifestInfo.get(project); - - // Look up the screen size for the current state - ScreenSize screenSize = null; - Device device = mConfiguration.getDevice(); - if (device != null) { - List<State> states = device.getAllStates(); - for (State state : states) { - FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(state); - if (folderConfig != null) { - ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); - screenSize = qualifier.getValue(); - break; - } - } - } - - // Look up the default/fallback theme to use for this project (which - // depends on the screen size when no particular theme is specified - // in the manifest) - String defaultTheme = manifest.getDefaultTheme(mConfiguration.getTarget(), screenSize); - - String preferred = defaultTheme; - if (mConfiguration.getTheme() == null) { - // If we are rendering a layout in included context, pick the theme - // from the outer layout instead - - String activity = mConfiguration.getActivity(); - if (activity != null) { - Map<String, String> activityThemes = manifest.getActivityThemes(); - preferred = activityThemes.get(activity); - } - if (preferred == null) { - preferred = defaultTheme; - } - mConfiguration.setTheme(preferred); - } - - return preferred; - } - @Nullable private String getPreferredActivity(@NonNull IFile file) { // Store/restore the activity context in the config state to help with @@ -1942,4 +2070,22 @@ public class ConfigurationChooser extends Composite return false; } + + /** + * Returns true if this configuration chooser represents the best match for + * the given file + * + * @param file the file to test + * @param config the config to test + * @return true if the given config is the best match for the given file + */ + public boolean isBestMatchFor(IFile file, FolderConfiguration config) { + ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), + ResourceType.LAYOUT, config); + if (match != null) { + return match.getFile().equals(mEditedFile); + } + + return false; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java index a7c26d4..3df2fed 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationClient.java @@ -19,14 +19,10 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceRepository; -import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; -import com.android.resources.NightMode; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.resources.ResourceType; -import com.android.resources.UiMode; import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.devices.Device; -import com.android.sdklib.devices.State; import java.util.Map; @@ -34,31 +30,13 @@ import java.util.Map; * Interface implemented by clients who embed a {@link ConfigurationChooser}. */ public interface ConfigurationClient { - /** The {@link FolderConfiguration} in the configuration has changed */ - public static final int CHANGED_FOLDER = 1 << 0; - /** The {@link Device} in the configuration has changed */ - public static final int CHANGED_DEVICE = 1 << 1; - /** The {@link State} in the configuration has changed */ - public static final int CHANGED_DEVICE_CONFIG = 1 << 2; - /** The theme in the configuration has changed */ - public static final int CHANGED_THEME = 1 << 3; - /** The locale in the configuration has changed */ - public static final int CHANGED_LOCALE = 1 << 4; - /** The rendering {@link IAndroidTarget} in the configuration has changed */ - public static final int CHANGED_RENDER_TARGET = 1 << 5; - /** The {@link NightMode} in the configuration has changed */ - public static final int CHANGED_NIGHT_MODE = 1 << 6; - /** The {@link UiMode} in the configuration has changed */ - public static final int CHANGED_UI_MODE = 1 << 7; - - /** Everything has changed */ - public static final int CHANGED_ALL = 0xFFFF; - /** * The configuration is about to be changed. * - * @param flags details about what changed; consult the {@code CHANGED_} flags - * such as {@link #CHANGED_DEVICE}, {@link #CHANGED_LOCALE}, etc. + * @param flags details about what changed; consult the {@code CFG_} flags + * in {@link Configuration} such as + * {@link Configuration#CFG_DEVICE}, + * {@link Configuration#CFG_LOCALE}, etc. */ void aboutToChange(int flags); @@ -70,8 +48,9 @@ public interface ConfigurationClient { * file to edit the new configuration -- and the current configuration * should go back to editing the state prior to this change. * - * @param flags details about what changed; consult the {@code CHANGED_} flags - * such as {@link #CHANGED_DEVICE}, {@link #CHANGED_LOCALE}, etc. + * @param flags details about what changed; consult the {@code CFG_} flags + * such as {@link Configuration#CFG_DEVICE}, + * {@link Configuration#CFG_LOCALE}, etc. * @return true if the change was accepted, false if it was rejected. */ boolean changed(int flags); @@ -139,4 +118,12 @@ public interface ConfigurationClient { * @param fqcn the fully qualified class name for the associated activity context */ void setActivity(@NonNull String fqcn); + + /** + * Returns the associated layout canvas, if any + * + * @return the canvas, if any + */ + @Nullable + LayoutCanvas getCanvas(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java new file mode 100644 index 0000000..7141f94 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationDescription.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2012 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.configuration; + +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_THEME; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.configuration.DeviceConfigHelper; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.RegionQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.NightMode; +import com.android.resources.ResourceFolderType; +import com.android.resources.ScreenSize; +import com.android.resources.UiMode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.State; +import com.google.common.base.Splitter; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.QualifiedName; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** A description of a configuration, used for persistence */ +public class ConfigurationDescription { + private static final String TAG_PREVIEWS = "previews"; //$NON-NLS-1$ + private static final String TAG_PREVIEW = "preview"; //$NON-NLS-1$ + private static final String ATTR_TARGET = "target"; //$NON-NLS-1$ + private static final String ATTR_CONFIG = "config"; //$NON-NLS-1$ + private static final String ATTR_LOCALE = "locale"; //$NON-NLS-1$ + private static final String ATTR_ACTIVITY = "activity"; //$NON-NLS-1$ + private static final String ATTR_DEVICE = "device"; //$NON-NLS-1$ + private static final String ATTR_STATE = "devicestate"; //$NON-NLS-1$ + private static final String ATTR_UIMODE = "ui"; //$NON-NLS-1$ + private static final String ATTR_NIGHTMODE = "night"; //$NON-NLS-1$ + private final static String SEP_LOCALE = "-"; //$NON-NLS-1$ + + /** + * Settings name for file-specific configuration preferences, such as which theme or + * device to render the current layout with + */ + public final static QualifiedName NAME_CONFIG_STATE = + new QualifiedName(AdtPlugin.PLUGIN_ID, "state");//$NON-NLS-1$ + + /** The project corresponding to this configuration's description */ + public final IProject project; + + /** The display name */ + public String displayName; + + /** The theme */ + public String theme; + + /** The target */ + public IAndroidTarget target; + + /** The display name */ + public FolderConfiguration folder; + + /** The locale */ + public Locale locale = Locale.ANY; + + /** The device */ + public Device device; + + /** The device state */ + public State state; + + /** The activity */ + public String activity; + + /** UI mode */ + @NonNull + public UiMode uiMode = UiMode.NORMAL; + + /** Night mode */ + @NonNull + public NightMode nightMode = NightMode.NOTNIGHT; + + private ConfigurationDescription(@Nullable IProject project) { + this.project = project; + } + + /** + * Returns the persistent configuration description from the given file + * + * @param file the file to look up a description from + * @return the description or null if never written + */ + @Nullable + public static String getDescription(@NonNull IFile file) { + return AdtPlugin.getFileProperty(file, NAME_CONFIG_STATE); + } + + /** + * Sets the persistent configuration description data for the given file + * + * @param file the file to associate the description with + * @param description the description + */ + public static void setDescription(@NonNull IFile file, @NonNull String description) { + AdtPlugin.setFileProperty(file, NAME_CONFIG_STATE, description); + } + + /** + * Creates a description from a given configuration + * + * @param project the project for this configuration's description + * @param configuration the configuration to describe + * @return a new configuration + */ + public static ConfigurationDescription fromConfiguration( + @Nullable IProject project, + @NonNull Configuration configuration) { + ConfigurationDescription description = new ConfigurationDescription(project); + description.displayName = configuration.getDisplayName(); + description.theme = configuration.getTheme(); + description.target = configuration.getTarget(); + description.folder = new FolderConfiguration(); + description.folder.set(configuration.getFullConfig()); + description.locale = configuration.getLocale(); + description.device = configuration.getDevice(); + description.state = configuration.getDeviceState(); + description.activity = configuration.getActivity(); + return description; + } + + /** + * Initializes a string previously created with + * {@link #toXml(Document)} + * + * @param project the project for this configuration's description + * @param element the element to read back from + * @param deviceList list of available devices + * @return true if the configuration was initialized + */ + @Nullable + public static ConfigurationDescription fromXml( + @Nullable IProject project, + @NonNull Element element, + @NonNull List<Device> deviceList) { + ConfigurationDescription description = new ConfigurationDescription(project); + + if (!TAG_PREVIEW.equals(element.getTagName())) { + return null; + } + + String displayName = element.getAttribute(ATTR_NAME); + if (!displayName.isEmpty()) { + description.displayName = displayName; + } + + String config = element.getAttribute(ATTR_CONFIG); + Iterable<String> segments = Splitter.on('-').split(config); + description.folder = FolderConfiguration.getConfig(segments); + + String theme = element.getAttribute(ATTR_THEME); + if (!theme.isEmpty()) { + description.theme = theme; + } + + String targetId = element.getAttribute(ATTR_TARGET); + if (!targetId.isEmpty()) { + IAndroidTarget target = Configuration.stringToTarget(targetId); + description.target = target; + } + + String localeString = element.getAttribute(ATTR_LOCALE); + if (!localeString.isEmpty()) { + // Load locale. Note that this can get overwritten by the + // project-wide settings read below. + LanguageQualifier language = Locale.ANY_LANGUAGE; + RegionQualifier region = Locale.ANY_REGION; + String locales[] = localeString.split(SEP_LOCALE); + if (locales[0].length() > 0) { + language = new LanguageQualifier(locales[0]); + } + if (locales.length > 1 && locales[1].length() > 0) { + region = new RegionQualifier(locales[1]); + } + description.locale = Locale.create(language, region); + } + + String activity = element.getAttribute(ATTR_ACTIVITY); + if (activity.isEmpty()) { + activity = null; + } + + String deviceString = element.getAttribute(ATTR_DEVICE); + if (!deviceString.isEmpty()) { + for (Device d : deviceList) { + if (d.getName().equals(deviceString)) { + description.device = d; + String stateName = element.getAttribute(ATTR_STATE); + if (stateName.isEmpty() || stateName.equals("null")) { + description.state = Configuration.getState(d, stateName); + } else if (d.getAllStates().size() > 0) { + description.state = d.getAllStates().get(0); + } + break; + } + } + } + + String uiModeString = element.getAttribute(ATTR_UIMODE); + if (!uiModeString.isEmpty()) { + description.uiMode = UiMode.getEnum(uiModeString); + if (description.uiMode == null) { + description.uiMode = UiMode.NORMAL; + } + } + + String nightModeString = element.getAttribute(ATTR_NIGHTMODE); + if (!nightModeString.isEmpty()) { + description.nightMode = NightMode.getEnum(nightModeString); + if (description.nightMode == null) { + description.nightMode = NightMode.NOTNIGHT; + } + } + + + // Should I really be storing the FULL configuration? Might be trouble if + // you bring a different device + + return description; + } + + /** + * Write this description into the given document as a new element. + * + * @param document the document to add the description to + * @return the newly inserted element + */ + @NonNull + public Element toXml(Document document) { + Element element = document.createElement(TAG_PREVIEW); + + element.setAttribute(ATTR_NAME, displayName); + FolderConfiguration fullConfig = folder; + String folderName = fullConfig.getFolderName(ResourceFolderType.LAYOUT); + element.setAttribute(ATTR_CONFIG, folderName); + if (theme != null) { + element.setAttribute(ATTR_THEME, theme); + } + if (target != null) { + element.setAttribute(ATTR_TARGET, Configuration.targetToString(target)); + } + + if (locale != null && (locale.hasLanguage() || locale.hasRegion())) { + String value; + if (locale.hasRegion()) { + value = locale.language.getValue() + SEP_LOCALE + locale.region.getValue(); + } else { + value = locale.language.getValue(); + } + element.setAttribute(ATTR_LOCALE, value); + } + + if (device != null) { + element.setAttribute(ATTR_DEVICE, device.getName()); + if (state != null) { + element.setAttribute(ATTR_STATE, state.getName()); + } + } + + if (activity != null) { + element.setAttribute(ATTR_ACTIVITY, activity); + } + + if (uiMode != null && uiMode != UiMode.NORMAL) { + element.setAttribute(ATTR_UIMODE, uiMode.getResourceValue()); + } + + if (nightMode != null && nightMode != NightMode.NOTNIGHT) { + element.setAttribute(ATTR_NIGHTMODE, nightMode.getResourceValue()); + } + + Element parent = document.getDocumentElement(); + if (parent == null) { + parent = document.createElement(TAG_PREVIEWS); + document.appendChild(parent); + } + parent.appendChild(element); + + return element; + } + + /** Returns the preferred theme, or null */ + @Nullable + String computePreferredTheme() { + if (project == null) { + return "Theme"; + } + ManifestInfo manifest = ManifestInfo.get(project); + + // Look up the screen size for the current state + ScreenSize screenSize = null; + if (device != null) { + List<State> states = device.getAllStates(); + for (State s : states) { + FolderConfiguration folderConfig = DeviceConfigHelper.getFolderConfig(s); + if (folderConfig != null) { + ScreenSizeQualifier qualifier = folderConfig.getScreenSizeQualifier(); + screenSize = qualifier.getValue(); + break; + } + } + } + + // Look up the default/fallback theme to use for this project (which + // depends on the screen size when no particular theme is specified + // in the manifest) + String defaultTheme = manifest.getDefaultTheme(target, screenSize); + + String preferred = defaultTheme; + if (theme == null) { + // If we are rendering a layout in included context, pick the theme + // from the outer layout instead + + if (activity != null) { + Map<String, String> activityThemes = manifest.getActivityThemes(); + preferred = activityThemes.get(activity); + } + if (preferred == null) { + preferred = defaultTheme; + } + theme = preferred; + } + + return preferred; + } + + private void checkThemePrefix() { + if (theme != null && !theme.startsWith(PREFIX_RESOURCE_REF)) { + if (theme.isEmpty()) { + computePreferredTheme(); + return; + } + + if (target != null) { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + AndroidTargetData data = sdk.getTargetData(target); + + if (data != null) { + ResourceRepository resources = data.getFrameworkResources(); + if (resources != null + && resources.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { + theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; + return; + } + } + } + } + + theme = STYLE_RESOURCE_PREFIX + theme; + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java index dc64b36..5dfcdb8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMatcher.java @@ -38,7 +38,7 @@ import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFileWrapper; import com.android.resources.Density; import com.android.resources.NightMode; -import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; import com.android.resources.ScreenOrientation; import com.android.resources.ScreenSize; import com.android.resources.UiMode; @@ -59,12 +59,37 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -/** Produces matches for configurations */ +/** + * Produces matches for configurations + * <p> + * See algorithm described here: + * http://developer.android.com/guide/topics/resources/providing-resources.html + */ public class ConfigurationMatcher { + private static final boolean PREFER_RECENT_RENDER_TARGETS = true; + private final ConfigurationChooser mConfigChooser; + private final Configuration mConfiguration; + private final IFile mEditedFile; + private final ProjectResources mResources; + private final boolean mUpdateUi; ConfigurationMatcher(ConfigurationChooser chooser) { + this(chooser, chooser.getConfiguration(), chooser.getEditedFile(), + chooser.getResources(), true); + } + + ConfigurationMatcher( + @NonNull ConfigurationChooser chooser, + @NonNull Configuration configuration, + @Nullable IFile editedFile, + @Nullable ProjectResources resources, + boolean updateUi) { mConfigChooser = chooser; + mConfiguration = configuration; + mEditedFile = editedFile; + mResources = resources; + mUpdateUi = updateUi; } // ---- Finding matching configurations ---- @@ -118,14 +143,12 @@ public class ConfigurationMatcher { * @return true if the current edited file is the best match in the project for the * given config. */ - boolean isCurrentFileBestMatchFor(FolderConfiguration config) { - ProjectResources resources = mConfigChooser.getResources(); - IFile editedFile = mConfigChooser.getEditedFile(); - ResourceFile match = resources.getMatchingFile(editedFile.getName(), - ResourceFolderType.LAYOUT, config); + public boolean isCurrentFileBestMatchFor(FolderConfiguration config) { + ResourceFile match = mResources.getMatchingFile(mEditedFile.getName(), + ResourceType.LAYOUT, config); if (match != null) { - return match.getFile().equals(editedFile); + return match.getFile().equals(mEditedFile); } else { // if we stop here that means the current file is not even a match! AdtPlugin.log(IStatus.ERROR, "Current file is not a match for the given config."); @@ -149,9 +172,8 @@ public class ConfigurationMatcher { // check the device config (ie sans locale) boolean needConfigChange = true; // if still true, we need to find another config. boolean currentConfigIsCompatible = false; - Configuration configuration = mConfigChooser.getConfiguration(); - State selectedState = configuration.getDeviceState(); - FolderConfiguration editedConfig = configuration.getEditedConfig(); + State selectedState = mConfiguration.getDeviceState(); + FolderConfiguration editedConfig = mConfiguration.getEditedConfig(); if (selectedState != null) { FolderConfiguration currentConfig = DeviceConfigHelper.getFolderConfig(selectedState); if (currentConfig != null && editedConfig.isMatchFor(currentConfig)) { @@ -172,7 +194,7 @@ public class ConfigurationMatcher { // first look in the current device. State matchState = null; int localeIndex = -1; - Device device = configuration.getDevice(); + Device device = mConfiguration.getDevice(); if (device != null) { mainloop: for (State state : device.getAllStates()) { testConfig.set(DeviceConfigHelper.getFolderConfig(state)); @@ -196,12 +218,14 @@ public class ConfigurationMatcher { } if (matchState != null) { - configuration.setDeviceState(matchState, true); + mConfiguration.setDeviceState(matchState, true); Locale locale = localeList.get(localeIndex); - configuration.setLocale(locale, true); - mConfigChooser.selectDeviceState(matchState); - mConfigChooser.selectLocale(locale); - configuration.syncFolderConfig(); + mConfiguration.setLocale(locale, true); + if (mUpdateUi) { + mConfigChooser.selectDeviceState(matchState); + mConfigChooser.selectLocale(locale); + } + mConfiguration.syncFolderConfig(); } else { // no match in current device with any state/locale // attempt to find another device that can display this @@ -225,9 +249,8 @@ public class ConfigurationMatcher { void findAndSetCompatibleConfig(boolean favorCurrentConfig) { List<Locale> localeList = mConfigChooser.getLocaleList(); List<Device> deviceList = mConfigChooser.getDeviceList(); - Configuration configuration = mConfigChooser.getConfiguration(); - FolderConfiguration editedConfig = configuration.getEditedConfig(); - FolderConfiguration currentConfig = configuration.getFullConfig(); + FolderConfiguration editedConfig = mConfiguration.getEditedConfig(); + FolderConfiguration currentConfig = mConfiguration.getFullConfig(); // list of compatible device/state/locale List<ConfigMatch> anyMatches = new ArrayList<ConfigMatch>(); @@ -316,7 +339,7 @@ public class ConfigurationMatcher { } // just display the warning - AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + AdtPlugin.printErrorToConsole(mEditedFile.getProject(), String.format( "'%1$s' is not a best match for any device/locale combination.", editedConfig.toDisplayString()), @@ -326,21 +349,23 @@ public class ConfigurationMatcher { } else if (anyMatches.size() > 0) { // select the best device anyway. ConfigMatch match = selectConfigMatch(anyMatches); - configuration.setDevice(match.device, true); - configuration.setDeviceState(match.state, true); - configuration.setLocale(localeList.get(match.bundle.localeIndex), true); - configuration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); - configuration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), + mConfiguration.setDevice(match.device, true); + mConfiguration.setDeviceState(match.state, true); + mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true); + mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); + mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true); - mConfigChooser.selectDevice(configuration.getDevice()); - mConfigChooser.selectDeviceState(configuration.getDeviceState()); - mConfigChooser.selectLocale(configuration.getLocale()); + if (mUpdateUi) { + mConfigChooser.selectDevice(mConfiguration.getDevice()); + mConfigChooser.selectDeviceState(mConfiguration.getDeviceState()); + mConfigChooser.selectLocale(mConfiguration.getLocale()); + } - configuration.syncFolderConfig(); + mConfiguration.syncFolderConfig(); // TODO: display a better warning! - AdtPlugin.printErrorToConsole(mConfigChooser.getProject(), + AdtPlugin.printErrorToConsole(mEditedFile.getProject(), String.format( "'%1$s' is not a best match for any device/locale combination.", editedConfig.toDisplayString()), @@ -357,17 +382,19 @@ public class ConfigurationMatcher { } } else { ConfigMatch match = selectConfigMatch(bestMatches); - configuration.setDevice(match.device, true); - configuration.setDeviceState(match.state, true); - configuration.setLocale(localeList.get(match.bundle.localeIndex), true); - configuration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); - configuration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true); - - configuration.syncFolderConfig(); - - mConfigChooser.selectDevice(configuration.getDevice()); - mConfigChooser.selectDeviceState(configuration.getDeviceState()); - mConfigChooser.selectLocale(configuration.getLocale()); + mConfiguration.setDevice(match.device, true); + mConfiguration.setDeviceState(match.state, true); + mConfiguration.setLocale(localeList.get(match.bundle.localeIndex), true); + mConfiguration.setUiMode(UiMode.getByIndex(match.bundle.dockModeIndex), true); + mConfiguration.setNightMode(NightMode.getByIndex(match.bundle.nightModeIndex), true); + + mConfiguration.syncFolderConfig(); + + if (mUpdateUi) { + mConfigChooser.selectDevice(mConfiguration.getDevice()); + mConfigChooser.selectDeviceState(mConfiguration.getDeviceState()); + mConfigChooser.selectLocale(mConfiguration.getLocale()); + } } } @@ -455,15 +482,24 @@ public class ConfigurationMatcher { private ConfigMatch selectConfigMatch(List<ConfigMatch> matches) { // API 11-13: look for a x-large device - int apiLevel = mConfigChooser.getProjectTarget().getVersion().getApiLevel(); - if (apiLevel >= 11 && apiLevel < 14) { - // TODO: Maybe check the compatible-screen tag in the manifest to figure out - // what kind of device should be used for display. - Collections.sort(matches, new TabletConfigComparator()); - } else { + Comparator<ConfigMatch> comparator = null; + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + IAndroidTarget projectTarget = sdk.getTarget(mEditedFile.getProject()); + if (projectTarget != null) { + int apiLevel = projectTarget.getVersion().getApiLevel(); + if (apiLevel >= 11 && apiLevel < 14) { + // TODO: Maybe check the compatible-screen tag in the manifest to figure out + // what kind of device should be used for display. + comparator = new TabletConfigComparator(); + } + } + } + if (comparator == null) { // lets look for a high density device - Collections.sort(matches, new PhoneConfigComparator()); + comparator = new PhoneConfigComparator(); } + Collections.sort(matches, comparator); // Look at the currently active editor to see if it's a layout editor, and if so, // look up its configuration and if the configuration is in our match list, @@ -473,7 +509,7 @@ public class ConfigurationMatcher { LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); if (delegate != null // (Only do this when the two files are in the same project) - && delegate.getEditor().getProject() == mConfigChooser.getProject()) { + && delegate.getEditor().getProject() == mEditedFile.getProject()) { FolderConfiguration configuration = delegate.getGraphicalEditor().getConfiguration(); if (configuration != null) { for (ConfigMatch match : matches) { @@ -490,7 +526,16 @@ public class ConfigurationMatcher { /** Return the default render target to use, or null if no strong preference */ @Nullable - static IAndroidTarget findDefaultRenderTarget(@NonNull IProject project) { + static IAndroidTarget findDefaultRenderTarget(ConfigurationChooser chooser) { + if (PREFER_RECENT_RENDER_TARGETS) { + // Use the most recent target + List<IAndroidTarget> targetList = chooser.getTargetList(); + if (!targetList.isEmpty()) { + return targetList.get(targetList.size() - 1); + } + } + + IProject project = chooser.getProject(); // Default to layoutlib version 5 Sdk current = Sdk.getCurrent(); if (current != null) { @@ -636,9 +681,13 @@ public class ConfigurationMatcher { } // From the resources, look for a matching file - String name = chooser.getEditedFile().getName(); + IFile editedFile = chooser.getEditedFile(); + if (editedFile == null) { + return null; + } + String name = editedFile.getName(); FolderConfiguration config = chooser.getConfiguration().getFullConfig(); - ResourceFile match = resources.getMatchingFile(name, ResourceFolderType.LAYOUT, config); + ResourceFile match = resources.getMatchingFile(name, ResourceType.LAYOUT, config); if (match != null) { // In Eclipse, the match's file is always an instance of IFileWrapper diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java index 30f7dc2..a791c63 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ConfigurationMenuListener.java @@ -16,21 +16,33 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; -import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.LOCALES; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.VARIATIONS; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.resources.ResourceFolder; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; -import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.resources.IProject; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; @@ -39,9 +51,9 @@ import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.ui.IEditorPart; import org.eclipse.ui.PartInitException; -import java.util.ArrayList; import java.util.List; /** @@ -52,18 +64,24 @@ class ConfigurationMenuListener extends SelectionAdapter { private static final String ICON_NEW_CONFIG = "newConfig"; //$NON-NLS-1$ private static final int ACTION_SELECT_CONFIG = 1; private static final int ACTION_CREATE_CONFIG_FILE = 2; + private static final int ACTION_ADD = 3; + private static final int ACTION_DELETE_ALL = 4; + private static final int ACTION_PREVIEW_MODE = 5; private final ConfigurationChooser mConfigChooser; private final int mAction; private final IFile mResource; + private final RenderPreviewMode mMode; ConfigurationMenuListener( @NonNull ConfigurationChooser configChooser, int action, - @Nullable IFile resource) { + @Nullable IFile resource, + @Nullable RenderPreviewMode mode) { mConfigChooser = configChooser; mAction = action; mResource = resource; + mMode = mode; } @Override @@ -75,77 +93,165 @@ class ConfigurationMenuListener extends SelectionAdapter { } catch (PartInitException ex) { AdtPlugin.log(ex, null); } - break; + return; } case ACTION_CREATE_CONFIG_FILE: { ConfigurationClient client = mConfigChooser.getClient(); if (client != null) { client.createConfigFile(); } + return; + } + } + + IEditorPart activeEditor = AdtUtils.getActiveEditor(); + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); + IFile editedFile = mConfigChooser.getEditedFile(); + + if (delegate == null || editedFile == null) { + return; + } + // (Only do this when the two files are in the same project) + IProject project = delegate.getEditor().getProject(); + if (project == null || + !project.equals(editedFile.getProject())) { + return; + } + LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl(); + RenderPreviewManager previewManager = canvas.getPreviewManager(); + + switch (mAction) { + case ACTION_ADD: { + previewManager.addAsThumbnail(); + break; + } + case ACTION_PREVIEW_MODE: { + previewManager.selectMode(mMode); + break; + } + case ACTION_DELETE_ALL: { + previewManager.deleteManualPreviews(); break; } default: assert false : mAction; } + canvas.setFitScale(true /*onlyZoomOut*/, false /*allowZoomIn*/); + canvas.redraw(); } static void show(ConfigurationChooser chooser, ToolItem combo) { Menu menu = new Menu(chooser.getShell(), SWT.POP_UP); + RenderPreviewMode mode = AdtPrefs.getPrefs().getRenderPreviewMode(); - // Compute the set of layout files defining this layout resource - IFile file = chooser.getEditedFile(); - String name = file.getName(); - IContainer resFolder = file.getParent().getParent(); - List<IFile> variations = new ArrayList<IFile>(); - try { - for (IResource resource : resFolder.members()) { - if (resource.getName().startsWith(FD_RES_LAYOUT) - && resource instanceof IContainer) { - IContainer layoutFolder = (IContainer) resource; - IResource variation = layoutFolder.findMember(name); - if (variation instanceof IFile) { - variations.add((IFile) variation); - } + // Configuration Previews + create(menu, "Add As Thumbnail...", + new ConfigurationMenuListener(chooser, ACTION_ADD, null, null), + SWT.PUSH, false); + if (mode == RenderPreviewMode.CUSTOM) { + MenuItem item = create(menu, "Delete All Thumbnails", + new ConfigurationMenuListener(chooser, ACTION_DELETE_ALL, null, null), + SWT.PUSH, false); + IEditorPart activeEditor = AdtUtils.getActiveEditor(); + LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); + if (delegate != null) { + LayoutCanvas canvas = delegate.getGraphicalEditor().getCanvasControl(); + RenderPreviewManager previewManager = canvas.getPreviewManager(); + if (!previewManager.hasManualPreviews()) { + item.setEnabled(false); } } - } catch (CoreException e1) { - AdtPlugin.log(e1, null); } - ResourceManager manager = ResourceManager.getInstance(); - for (final IFile resource : variations) { - MenuItem item = new MenuItem(menu, SWT.CHECK); + @SuppressWarnings("unused") + MenuItem configSeparator = new MenuItem(menu, SWT.SEPARATOR); - IFolder parent = (IFolder) resource.getParent(); - ResourceFolder parentResource = manager.getResourceFolder(parent); - FolderConfiguration configuration = parentResource.getConfiguration(); - String title = configuration.toDisplayString(); - item.setText(title); + create(menu, "Preview Representative Sample", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + DEFAULT), SWT.RADIO, mode == DEFAULT); + create(menu, "Preview All Screen Sizes", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + SCREENS), SWT.RADIO, mode == SCREENS); - boolean selected = file.equals(resource); - if (selected) { - item.setSelection(true); - item.setEnabled(false); - } + MenuItem localeItem = create(menu, "Preview All Locales", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + LOCALES), SWT.RADIO, mode == LOCALES); + if (chooser.getLocaleList().size() <= 1) { + localeItem.setEnabled(false); + } - item.addSelectionListener(new ConfigurationMenuListener(chooser, - ACTION_SELECT_CONFIG, resource)); + boolean canPreviewIncluded = false; + IProject project = chooser.getProject(); + if (project != null) { + IncludeFinder finder = IncludeFinder.get(project); + final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile()); + canPreviewIncluded = includedBy != null && !includedBy.isEmpty(); + } + //if (!graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) { + // canPreviewIncluded = false; + //} + MenuItem includedItem = create(menu, "Preview Included", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + INCLUDES), SWT.RADIO, mode == INCLUDES); + if (!canPreviewIncluded) { + includedItem.setEnabled(false); + } + + IFile file = chooser.getEditedFile(); + List<IFile> variations = AdtUtils.getResourceVariations(file, true); + MenuItem variationsItem = create(menu, "Preview Layout Versions", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + VARIATIONS), SWT.RADIO, mode == VARIATIONS); + if (variations.size() <= 1) { + variationsItem.setEnabled(false); + } + + create(menu, "Manual Previews", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + CUSTOM), SWT.RADIO, mode == CUSTOM); + create(menu, "None", + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, + NONE), SWT.RADIO, mode == NONE); + + if (variations.size() > 1) { + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + + ResourceManager manager = ResourceManager.getInstance(); + for (final IFile resource : variations) { + IFolder parent = (IFolder) resource.getParent(); + ResourceFolder parentResource = manager.getResourceFolder(parent); + FolderConfiguration configuration = parentResource.getConfiguration(); + String title = configuration.toDisplayString(); + + MenuItem item = create(menu, title, + new ConfigurationMenuListener(chooser, ACTION_SELECT_CONFIG, + resource, null), + SWT.CHECK, false); + + if (file != null) { + boolean selected = file.equals(resource); + if (selected) { + item.setSelection(true); + item.setEnabled(false); + } + } + } } Configuration configuration = chooser.getConfiguration(); - if (!configuration.getEditedConfig().equals(configuration.getFullConfig())) { + if (configuration.getEditedConfig() != null && + !configuration.getEditedConfig().equals(configuration.getFullConfig())) { if (variations.size() > 0) { @SuppressWarnings("unused") MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); } // Add action for creating a new configuration - MenuItem item = new MenuItem(menu, SWT.PUSH); - item.setText("Create New..."); + MenuItem item = create(menu, "Create New...", + new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE, + null, null), + SWT.PUSH, false); item.setImage(IconFactory.getInstance().getIcon(ICON_NEW_CONFIG)); - //item.setToolTipText("Duplicate: Create new configuration for this layout"); - - item.addSelectionListener( - new ConfigurationMenuListener(chooser, ACTION_CREATE_CONFIG_FILE, null)); } Rectangle bounds = combo.getBounds(); @@ -154,4 +260,31 @@ class ConfigurationMenuListener extends SelectionAdapter { menu.setLocation(location.x, location.y); menu.setVisible(true); } + + @NonNull + public static MenuItem create(@NonNull Menu menu, String title, + ConfigurationMenuListener listener, int style, boolean selected) { + MenuItem item = new MenuItem(menu, style); + item.setText(title); + item.addSelectionListener(listener); + if (selected) { + item.setSelection(true); + } + return item; + } + + @NonNull + static MenuItem addTogglePreviewModeAction( + @NonNull Menu menu, + @NonNull String title, + @NonNull ConfigurationChooser chooser, + @NonNull RenderPreviewMode mode) { + boolean selected = AdtPrefs.getPrefs().getRenderPreviewMode() == mode; + if (selected) { + mode = RenderPreviewMode.NONE; + } + return create(menu, title, + new ConfigurationMenuListener(chooser, ACTION_PREVIEW_MODE, null, mode), + SWT.CHECK, selected); + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java index 32f8e9d..4489b52 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/DeviceMenuListener.java @@ -16,11 +16,18 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; +import static com.android.ide.common.rendering.HardwareConfigHelper.MANUFACTURER_GENERIC; +import static com.android.ide.common.rendering.HardwareConfigHelper.getGenericLabel; +import static com.android.ide.common.rendering.HardwareConfigHelper.getNexusLabel; +import static com.android.ide.common.rendering.HardwareConfigHelper.isGeneric; +import static com.android.ide.common.rendering.HardwareConfigHelper.isNexus; +import static com.android.ide.common.rendering.HardwareConfigHelper.sortNexusList; + import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.devices.Device; -import com.android.sdklib.devices.Screen; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; @@ -35,23 +42,15 @@ import org.eclipse.swt.widgets.ToolItem; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * The {@linkplain DeviceMenuListener} class is responsible for generating the device * menu in the {@link ConfigurationChooser}. */ class DeviceMenuListener extends SelectionAdapter { - private static final String NEXUS = "Nexus"; //$NON-NLS-1$ - private static final String GENERIC = "Generic"; //$NON-NLS-1$ - private static Pattern PATTERN = Pattern.compile( - "(\\d+\\.?\\d*)in (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$ - private final ConfigurationChooser mConfigChooser; private final Device mDevice; @@ -147,7 +146,7 @@ class DeviceMenuListener extends SelectionAdapter { for (List<Device> devices : manufacturers.values()) { for (Device device : devices) { if (isNexus(device)) { - if (device.getManufacturer().equals(GENERIC)) { + if (device.getManufacturer().equals(MANUFACTURER_GENERIC)) { generic.add(device); } else { nexus.add(device); @@ -183,94 +182,17 @@ class DeviceMenuListener extends SelectionAdapter { } } + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + + ConfigurationMenuListener.addTogglePreviewModeAction(menu, + "Preview All Screens", chooser, RenderPreviewMode.SCREENS); + + Rectangle bounds = combo.getBounds(); Point location = new Point(bounds.x, bounds.y + bounds.height); location = combo.getParent().toDisplay(location); menu.setLocation(location.x, location.y); menu.setVisible(true); } - - private static String getNexusLabel(Device d) { - String name = d.getName(); - Screen screen = d.getDefaultHardware().getScreen(); - float length = (float) screen.getDiagonalLength(); - return String.format(java.util.Locale.US, "%1$s (%3$s\", %2$s)", - name, getResolutionString(d), Float.toString(length)); - } - - private static String getGenericLabel(Device d) { - // * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA) - // * Use the same precision for all devices (all but one specify decimals) - // * Add some leading space such that the dot ends up roughly in the - // same space - // * Add in screen resolution and density - String name = d.getName(); - if (name.equals("3.7 FWVGA slider")) { - // Fix metadata: this one entry doesn't have "in" like the rest of them - name = "3.7in FWVGA slider"; - } - - Matcher matcher = PATTERN.matcher(name); - if (matcher.matches()) { - String size = matcher.group(1); - String n = matcher.group(2); - int dot = size.indexOf('.'); - if (dot == -1) { - size = size + ".0"; - dot = size.length() - 2; - } - for (int i = 0; i < 2 - dot; i++) { - size = ' ' + size; - } - name = size + "\" " + n; - } - - return String.format(java.util.Locale.US, "%1$s (%2$s)", name, - getResolutionString(d)); - } - - @Nullable - private static String getResolutionString(Device device) { - Screen screen = device.getDefaultHardware().getScreen(); - return String.format(java.util.Locale.US, - "%1$d \u00D7 %2$d: %3$s", // U+00D7: Unicode multiplication sign - screen.getXDimension(), - screen.getYDimension(), - screen.getPixelDensity().getResourceValue()); - } - - private static boolean isGeneric(Device device) { - return device.getManufacturer().equals(GENERIC); - } - - private static boolean isNexus(Device device) { - return device.getName().contains(NEXUS); - } - - private static void sortNexusList(List<Device> list) { - Collections.sort(list, new Comparator<Device>() { - @Override - public int compare(Device device1, Device device2) { - // Descending order of age - return nexusRank(device2) - nexusRank(device1); - } - private int nexusRank(Device device) { - String name = device.getName(); - if (name.endsWith(" One")) { //$NON-NLS-1$ - return 1; - } - if (name.endsWith(" S")) { //$NON-NLS-1$ - return 2; - } - if (name.startsWith("Galaxy")) { //$NON-NLS-1$ - return 3; - } - if (name.endsWith(" 7")) { //$NON-NLS-1$ - return 4; - } - - return 5; - } - }); - } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleManager.java index 0d30011..43c90d9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleManager.java @@ -487,7 +487,7 @@ public class LocaleManager { // "gn": Guaranà -> Paraguay sLanguageToCountry.put("gn", "PY"); //$NON-NLS-1$ //$NON-NLS-2$ - sLanguageNames.put("gn", "GuaranÃ"); //$NON-NLS-1$ + sLanguageNames.put("gn", "Guaran\u00ed" /*GuaranÃ*/); //$NON-NLS-1$ // "gu": Gujarati -> India sLanguageToCountry.put("gu", "IN"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -708,7 +708,7 @@ public class LocaleManager { // "nb": Norwegian -> Norway sLanguageToCountry.put("nb", "NO"); //$NON-NLS-1$ //$NON-NLS-2$ - sLanguageNames.put("nb", "Norwegian BokmÃ¥l"); //$NON-NLS-1$ + sLanguageNames.put("nb", "Norwegian Bokm\u00e5l" /*Norwegian BokmÃ¥l*/); //$NON-NLS-1$ // "nd": North Ndebele -> Zimbabwe sLanguageToCountry.put("nd", "ZW"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -976,7 +976,7 @@ public class LocaleManager { // "yo": Yorùbá -> Nigeria, Togo, Benin sLanguageToCountry.put("yo", "NG"); //$NON-NLS-1$ //$NON-NLS-2$ - sLanguageNames.put("yo", "Yorùbá"); //$NON-NLS-1$ + sLanguageNames.put("yo", "Yor\u00f9b\u00e1" /*Yorùbá*/); //$NON-NLS-1$ // "za": Zhuang -> China sLanguageToCountry.put("za", "CN"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -1005,7 +1005,7 @@ public class LocaleManager { sRegionNames.put("AT", "Austria"); //$NON-NLS-1$ sRegionNames.put("AU", "Australia"); //$NON-NLS-1$ sRegionNames.put("AW", "Aruba"); //$NON-NLS-1$ - sRegionNames.put("AX", "Ã…land Islands"); //$NON-NLS-1$ + sRegionNames.put("AX", "\u00c5land Islands" /*Ã…land Islands*/); //$NON-NLS-1$ sRegionNames.put("AZ", "Azerbaijan"); //$NON-NLS-1$ sRegionNames.put("BA", "Bosnia and Herzegovina"); //$NON-NLS-1$ sRegionNames.put("BB", "Barbados"); //$NON-NLS-1$ @@ -1016,7 +1016,7 @@ public class LocaleManager { sRegionNames.put("BH", "Bahrain"); //$NON-NLS-1$ sRegionNames.put("BI", "Burundi"); //$NON-NLS-1$ sRegionNames.put("BJ", "Benin"); //$NON-NLS-1$ - sRegionNames.put("BL", "Saint Barthélemy"); //$NON-NLS-1$ + sRegionNames.put("BL", "Saint Barth\u00e9lemy" /*Saint Barthélemy*/); //$NON-NLS-1$ sRegionNames.put("BM", "Bermuda"); //$NON-NLS-1$ sRegionNames.put("BN", "Brunei Darussalam"); //$NON-NLS-1$ sRegionNames.put("BO", "Bolivia, Plurinational State of"); //$NON-NLS-1$ @@ -1034,7 +1034,7 @@ public class LocaleManager { sRegionNames.put("CF", "Central African Republic"); //$NON-NLS-1$ sRegionNames.put("CG", "Congo"); //$NON-NLS-1$ sRegionNames.put("CH", "Switzerland"); //$NON-NLS-1$ - sRegionNames.put("CI", "Côte d'Ivoire"); //$NON-NLS-1$ + sRegionNames.put("CI", "C\u00f4te d'Ivoire" /*Côte d'Ivoire*/); //$NON-NLS-1$ sRegionNames.put("CK", "Cook Islands"); //$NON-NLS-1$ sRegionNames.put("CL", "Chile"); //$NON-NLS-1$ sRegionNames.put("CM", "Cameroon"); //$NON-NLS-1$ @@ -1043,7 +1043,7 @@ public class LocaleManager { sRegionNames.put("CR", "Costa Rica"); //$NON-NLS-1$ sRegionNames.put("CU", "Cuba"); //$NON-NLS-1$ sRegionNames.put("CV", "Cape Verde"); //$NON-NLS-1$ - sRegionNames.put("CW", "Curaçao"); //$NON-NLS-1$ + sRegionNames.put("CW", "Cura\u00e7ao" /*Curaçao*/); //$NON-NLS-1$ sRegionNames.put("CX", "Christmas Island"); //$NON-NLS-1$ sRegionNames.put("CY", "Cyprus"); //$NON-NLS-1$ sRegionNames.put("CZ", "Czech Republic"); //$NON-NLS-1$ @@ -1178,7 +1178,7 @@ public class LocaleManager { sRegionNames.put("PW", "Palau"); //$NON-NLS-1$ sRegionNames.put("PY", "Paraguay"); //$NON-NLS-1$ sRegionNames.put("QA", "Qatar"); //$NON-NLS-1$ - sRegionNames.put("RE", "Réunion"); //$NON-NLS-1$ + sRegionNames.put("RE", "R\u00e9union" /*Réunion*/); //$NON-NLS-1$ sRegionNames.put("RO", "Romania"); //$NON-NLS-1$ sRegionNames.put("RS", "Serbia"); //$NON-NLS-1$ sRegionNames.put("RU", "Russian Federation"); //$NON-NLS-1$ @@ -1257,7 +1257,7 @@ public class LocaleManager { // in sLanguageToCountry, since they are either extinct or constructed or // only in literary use: sLanguageNames.put("pi", "Pali"); //$NON-NLS-1$ - sLanguageNames.put("vo", "Volapük"); //$NON-NLS-1$ + sLanguageNames.put("vo", "Volap\u00fck" /*Volapük*/); //$NON-NLS-1$ sLanguageNames.put("eo", "Esperanto"); //$NON-NLS-1$ sLanguageNames.put("la", "Latin"); //$NON-NLS-1$ sLanguageNames.put("ia", "Interlingua"); //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java index e85f21d..2bc5417 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/LocaleMenuListener.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode; import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.AddTranslationDialog; import org.eclipse.core.resources.IProject; @@ -97,6 +98,14 @@ class LocaleMenuListener extends SelectionAdapter { item.addSelectionListener(listener); } + if (locales.size() > 1) { + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + + ConfigurationMenuListener.addTogglePreviewModeAction(menu, + "Preview All Locales", chooser, RenderPreviewMode.LOCALES); + } + @SuppressWarnings("unused") MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java new file mode 100644 index 0000000..50778e2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/NestedConfiguration.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2012 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.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.NightMode; +import com.android.resources.UiMode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.State; +import com.google.common.base.Objects; + +/** + * An {@linkplain NestedConfiguration} is a {@link Configuration} which inherits + * all of its values from a different configuration, except for one or more + * attributes where it overrides a custom value. + * <p> + * Unlike a {@link VaryingConfiguration}, a {@linkplain NestedConfiguration} + * will always return the same overridden value, regardless of the inherited + * value. + * <p> + * For example, an {@linkplain NestedConfiguration} may fix the locale to always + * be "en", but otherwise inherit everything else. + */ +public class NestedConfiguration extends Configuration { + /** The configuration we are inheriting non-overridden values from */ + protected Configuration mParent; + + /** Bitmask of attributes to be overridden in this configuration */ + private int mOverride; + + /** + * Constructs a new {@linkplain NestedConfiguration}. + * Construct via + * + * @param chooser the associated chooser + * @param configuration the configuration to inherit from + */ + protected NestedConfiguration( + @NonNull ConfigurationChooser chooser, + @NonNull Configuration configuration) { + super(chooser); + mParent = configuration; + + mFullConfig.set(mParent.mFullConfig); + if (mParent.getEditedConfig() != null) { + mEditedConfig = new FolderConfiguration(); + mEditedConfig.set(mParent.mEditedConfig); + } + } + + /** + * Returns the override flags for this configuration. Corresponds to + * the {@code CFG_} flags in {@link ConfigurationClient}. + * + * @return the bitmask + */ + public int getOverrideFlags() { + return mOverride; + } + + /** + * Creates a new {@linkplain NestedConfiguration} that has the same overriding + * attributes as the given other {@linkplain NestedConfiguration}, and gets + * its values from the given {@linkplain Configuration}. + * + * @param other the configuration to copy overrides from + * @param values the configuration to copy values from + * @param parent the parent to tie the configuration to for inheriting values + * @return a new configuration + */ + @NonNull + public static NestedConfiguration create( + @NonNull NestedConfiguration other, + @NonNull Configuration values, + @NonNull Configuration parent) { + NestedConfiguration configuration = + new NestedConfiguration(other.mConfigChooser, parent); + initFrom(configuration, other, values, true /*sync*/); + return configuration; + } + + /** + * Initializes a new {@linkplain NestedConfiguration} with the overriding + * attributes as the given other {@linkplain NestedConfiguration}, and gets + * its values from the given {@linkplain Configuration}. + * + * @param configuration the configuration to initialize + * @param other the configuration to copy overrides from + * @param values the configuration to copy values from + * @param sync if true, sync the folder configuration from + */ + protected static void initFrom(NestedConfiguration configuration, + NestedConfiguration other, Configuration values, boolean sync) { + configuration.mOverride = other.mOverride; + configuration.setDisplayName(values.getDisplayName()); + configuration.setActivity(values.getActivity()); + + if (configuration.isOverridingLocale()) { + configuration.setLocale(values.getLocale(), true); + } + if (configuration.isOverridingTarget()) { + configuration.setTarget(values.getTarget(), true); + } + if (configuration.isOverridingDevice()) { + configuration.setDevice(values.getDevice(), true); + } + if (configuration.isOverridingDeviceState()) { + configuration.setDeviceState(values.getDeviceState(), true); + } + if (configuration.isOverridingNightMode()) { + configuration.setNightMode(values.getNightMode(), true); + } + if (configuration.isOverridingUiMode()) { + configuration.setUiMode(values.getUiMode(), true); + } + if (sync) { + configuration.syncFolderConfig(); + } + } + + /** + * Sets the parent configuration that this configuration is inheriting from. + * + * @param parent the parent configuration + */ + public void setParent(@NonNull Configuration parent) { + mParent = parent; + } + + /** + * Creates a new {@linkplain Configuration} which inherits values from the + * given parent {@linkplain Configuration}, possibly overriding some as + * well. + * + * @param chooser the associated chooser + * @param parent the configuration to inherit values from + * @return a new configuration + */ + @NonNull + public static NestedConfiguration create(@NonNull ConfigurationChooser chooser, + @NonNull Configuration parent) { + return new NestedConfiguration(chooser, parent); + } + + @Override + @Nullable + public String getTheme() { + // Never overridden: this is a static attribute of a layout, not something which + // varies by configuration or at runtime + return mParent.getTheme(); + } + + @Override + public void setTheme(String theme) { + // Never overridden + mParent.setTheme(theme); + } + + /** + * Sets whether the locale should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideLocale(boolean override) { + mOverride |= CFG_LOCALE; + } + + /** + * Returns true if the locale is overridden + * + * @return true if the locale is overridden + */ + public final boolean isOverridingLocale() { + return (mOverride & CFG_LOCALE) != 0; + } + + @Override + @NonNull + public Locale getLocale() { + if (isOverridingLocale()) { + return super.getLocale(); + } else { + return mParent.getLocale(); + } + } + + @Override + public void setLocale(@NonNull Locale locale, boolean skipSync) { + if (isOverridingLocale()) { + super.setLocale(locale, skipSync); + } else { + mParent.setLocale(locale, skipSync); + } + } + + /** + * Sets whether the rendering target should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideTarget(boolean override) { + mOverride |= CFG_TARGET; + } + + /** + * Returns true if the target is overridden + * + * @return true if the target is overridden + */ + public final boolean isOverridingTarget() { + return (mOverride & CFG_TARGET) != 0; + } + + @Override + @Nullable + public IAndroidTarget getTarget() { + if (isOverridingTarget()) { + return super.getTarget(); + } else { + return mParent.getTarget(); + } + } + + @Override + public void setTarget(IAndroidTarget target, boolean skipSync) { + if (isOverridingTarget()) { + super.setTarget(target, skipSync); + } else { + mParent.setTarget(target, skipSync); + } + } + + /** + * Sets whether the device should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideDevice(boolean override) { + mOverride |= CFG_DEVICE; + } + + /** + * Returns true if the device is overridden + * + * @return true if the device is overridden + */ + public final boolean isOverridingDevice() { + return (mOverride & CFG_DEVICE) != 0; + } + + @Override + @Nullable + public Device getDevice() { + if (isOverridingDevice()) { + return super.getDevice(); + } else { + return mParent.getDevice(); + } + } + + @Override + public void setDevice(Device device, boolean skipSync) { + if (isOverridingDevice()) { + super.setDevice(device, skipSync); + } else { + mParent.setDevice(device, skipSync); + } + } + + /** + * Sets whether the device state should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideDeviceState(boolean override) { + mOverride |= CFG_DEVICE_STATE; + } + + /** + * Returns true if the device state is overridden + * + * @return true if the device state is overridden + */ + public final boolean isOverridingDeviceState() { + return (mOverride & CFG_DEVICE_STATE) != 0; + } + + @Override + @Nullable + public State getDeviceState() { + if (isOverridingDeviceState()) { + return super.getDeviceState(); + } else { + State state = mParent.getDeviceState(); + if (isOverridingDevice()) { + // If the device differs, I need to look up a suitable equivalent state + // on our device + if (state != null) { + Device device = super.getDevice(); + if (device != null) { + return device.getState(state.getName()); + } + } + } + + return state; + } + } + + @Override + public void setDeviceState(State state, boolean skipSync) { + if (isOverridingDeviceState()) { + super.setDeviceState(state, skipSync); + } else { + if (isOverridingDevice()) { + Device device = super.getDevice(); + if (device != null) { + State equivalentState = device.getState(state.getName()); + if (equivalentState != null) { + state = equivalentState; + } + } + } + mParent.setDeviceState(state, skipSync); + } + } + + /** + * Sets whether the night mode should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideNightMode(boolean override) { + mOverride |= CFG_NIGHT_MODE; + } + + /** + * Returns true if the night mode is overridden + * + * @return true if the night mode is overridden + */ + public final boolean isOverridingNightMode() { + return (mOverride & CFG_NIGHT_MODE) != 0; + } + + @Override + @NonNull + public NightMode getNightMode() { + if (isOverridingNightMode()) { + return super.getNightMode(); + } else { + return mParent.getNightMode(); + } + } + + @Override + public void setNightMode(@NonNull NightMode night, boolean skipSync) { + if (isOverridingNightMode()) { + super.setNightMode(night, skipSync); + } else { + mParent.setNightMode(night, skipSync); + } + } + + /** + * Sets whether the UI mode should be overridden by this configuration + * + * @param override if true, override the inherited value + */ + public void setOverrideUiMode(boolean override) { + mOverride |= CFG_UI_MODE; + } + + /** + * Returns true if the UI mode is overridden + * + * @return true if the UI mode is overridden + */ + public final boolean isOverridingUiMode() { + return (mOverride & CFG_UI_MODE) != 0; + } + + @Override + @NonNull + public UiMode getUiMode() { + if (isOverridingUiMode()) { + return super.getUiMode(); + } else { + return mParent.getUiMode(); + } + } + + @Override + public void setUiMode(@NonNull UiMode uiMode, boolean skipSync) { + if (isOverridingUiMode()) { + super.setUiMode(uiMode, skipSync); + } else { + mParent.setUiMode(uiMode, skipSync); + } + } + + /** + * Returns the configuration this {@linkplain NestedConfiguration} is + * inheriting from + * + * @return the configuration this configuration is inheriting from + */ + @NonNull + public Configuration getParent() { + return mParent; + } + + @Override + @Nullable + public String getActivity() { + return mParent.getActivity(); + } + + @Override + public void setActivity(String activity) { + super.setActivity(activity); + } + + /** + * Returns a computed display name (ignoring the value stored by + * {@link #setDisplayName(String)}) by looking at the override flags + * and picking a suitable name. + * + * @return a suitable display name + */ + @Nullable + public String computeDisplayName() { + return computeDisplayName(mOverride, this); + } + + /** + * Computes a display name for the given configuration, using the given + * override flags (which correspond to the {@code CFG_} constants in + * {@link ConfigurationClient} + * + * @param flags the override bitmask + * @param configuration the configuration to fetch values from + * @return a suitable display name + */ + @Nullable + public static String computeDisplayName(int flags, @NonNull Configuration configuration) { + if ((flags & CFG_LOCALE) != 0) { + return ConfigurationChooser.getLocaleLabel(configuration.mConfigChooser, + configuration.getLocale(), false); + } + + if ((flags & CFG_TARGET) != 0) { + return ConfigurationChooser.getRenderingTargetLabel(configuration.getTarget(), false); + } + + if ((flags & CFG_DEVICE) != 0) { + return ConfigurationChooser.getDeviceLabel(configuration.getDevice(), true); + } + + if ((flags & CFG_DEVICE_STATE) != 0) { + State deviceState = configuration.getDeviceState(); + if (deviceState != null) { + return deviceState.getName(); + } + } + + if ((flags & CFG_NIGHT_MODE) != 0) { + return configuration.getNightMode().getLongDisplayValue(); + } + + if ((flags & CFG_UI_MODE) != 0) { + configuration.getUiMode().getLongDisplayValue(); + } + + return null; + } + + @Override + public String toString() { + return Objects.toStringHelper(this.getClass()) + .add("parent", mParent.getDisplayName()) //$NON-NLS-1$ + .add("display", getDisplayName()) //$NON-NLS-1$ + .add("overrideLocale", isOverridingLocale()) //$NON-NLS-1$ + .add("overrideTarget", isOverridingTarget()) //$NON-NLS-1$ + .add("overrideDevice", isOverridingDevice()) //$NON-NLS-1$ + .add("overrideDeviceState", isOverridingDeviceState()) //$NON-NLS-1$ + .add("persistent", toPersistentString()) //$NON-NLS-1$ + .toString(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java index cae6596..71905f7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/TargetMenuListener.java @@ -18,6 +18,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.configuration; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import org.eclipse.swt.SWT; @@ -30,6 +32,7 @@ import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.ToolItem; import java.util.List; +import java.util.RandomAccess; /** * The {@linkplain TargetMenuListener} class is responsible for @@ -38,17 +41,36 @@ import java.util.List; class TargetMenuListener extends SelectionAdapter { private final ConfigurationChooser mConfigChooser; private final IAndroidTarget mTarget; + private final boolean mPickBest; TargetMenuListener( @NonNull ConfigurationChooser configChooser, - @Nullable IAndroidTarget target) { + @Nullable IAndroidTarget target, + boolean pickBest) { mConfigChooser = configChooser; mTarget = target; + mPickBest = pickBest; } @Override public void widgetSelected(SelectionEvent e) { - mConfigChooser.selectTarget(mTarget); + IAndroidTarget target = mTarget; + AdtPrefs prefs = AdtPrefs.getPrefs(); + if (mPickBest) { + boolean autoPick = prefs.isAutoPickRenderTarget(); + autoPick = !autoPick; + prefs.setAutoPickRenderTarget(autoPick); + if (autoPick) { + target = ConfigurationMatcher.findDefaultRenderTarget(mConfigChooser); + } else { + // Turn it off, but keep current target until another one is chosen + return; + } + } else { + // Manually picked some other target: turn off auto-pick + prefs.setAutoPickRenderTarget(false); + } + mConfigChooser.selectTarget(target); mConfigChooser.onRenderingTargetChange(); } @@ -57,8 +79,32 @@ class TargetMenuListener extends SelectionAdapter { Configuration configuration = chooser.getConfiguration(); IAndroidTarget current = configuration.getTarget(); List<IAndroidTarget> targets = chooser.getTargetList(); + boolean haveRecent = false; + + MenuItem menuItem = new MenuItem(menu, SWT.CHECK); + menuItem.setText("Automatically Pick Best"); + menuItem.addSelectionListener(new TargetMenuListener(chooser, null, true)); + if (AdtPrefs.getPrefs().isAutoPickRenderTarget()) { + menuItem.setSelection(true); + } + + @SuppressWarnings("unused") + MenuItem separator = new MenuItem(menu, SWT.SEPARATOR); + + // Process in reverse order: most important targets first + assert targets instanceof RandomAccess; + for (int i = targets.size() - 1; i >= 0; i--) { + IAndroidTarget target = targets.get(i); + + AndroidVersion version = target.getVersion(); + if (version.getApiLevel() >= 7) { + haveRecent = true; + } else if (haveRecent) { + // Don't show ancient rendering targets; they're pretty broken + // (unless of course all you have are ancient targets) + break; + } - for (final IAndroidTarget target : targets) { String title = ConfigurationChooser.getRenderingTargetLabel(target, false); MenuItem item = new MenuItem(menu, SWT.CHECK); item.setText(title); @@ -68,7 +114,7 @@ class TargetMenuListener extends SelectionAdapter { item.setSelection(true); } - item.addSelectionListener(new TargetMenuListener(chooser, target)); + item.addSelectionListener(new TargetMenuListener(chooser, target, false)); } Rectangle bounds = combo.getBounds(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java index 239f396..0f6c9eb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/ThemeMenuAction.java @@ -106,7 +106,7 @@ class ThemeMenuAction extends SubmenuAction { manager.add(new Separator()); } - String preferred = configChooser.computePreferredTheme(); + String preferred = configuration.computePreferredTheme(); if (preferred != null && !preferred.equals(currentTheme)) { manager.add(new SelectThemeAction(configChooser, ResourceHelper.styleToTheme(preferred), diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java new file mode 100644 index 0000000..c6e6407 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/configuration/VaryingConfiguration.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2012 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.configuration; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.resources.Density; +import com.android.resources.NightMode; +import com.android.resources.UiMode; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.State; + +import java.util.List; + +/** + * An {@linkplain VaryingConfiguration} is a {@link Configuration} which + * inherits all of its values from a different configuration, except for one or + * more attributes where it overrides a custom value, and the overridden value + * will always <b>differ</b> from the inherited value! + * <p> + * For example, a {@linkplain VaryingConfiguration} may state that it + * overrides the locale, and if the inherited locale is "en", then the returned + * locale from the {@linkplain VaryingConfiguration} may be for example "nb", + * but never "en". + * <p> + * The configuration will attempt to make its changed inherited value to be as + * different as possible from the inherited value. Thus, a configuration which + * overrides the device will probably return a phone-sized screen if the + * inherited device is a tablet, or vice versa. + */ +public class VaryingConfiguration extends NestedConfiguration { + /** Variation version; see {@link #setVariation(int)} */ + private int mVariation; + + /** Variation version count; see {@link #setVariationCount(int)} */ + private int mVariationCount; + + /** Bitmask of attributes to be varied/alternated from the parent */ + private int mAlternate; + + /** + * Constructs a new {@linkplain VaryingConfiguration}. + * Construct via + * + * @param chooser the associated chooser + * @param configuration the configuration to inherit from + */ + private VaryingConfiguration( + @NonNull ConfigurationChooser chooser, + @NonNull Configuration configuration) { + super(chooser, configuration); + } + + /** + * Creates a new {@linkplain Configuration} which inherits values from the + * given parent {@linkplain Configuration}, possibly overriding some as + * well. + * + * @param chooser the associated chooser + * @param parent the configuration to inherit values from + * @return a new configuration + */ + @NonNull + public static VaryingConfiguration create(@NonNull ConfigurationChooser chooser, + @NonNull Configuration parent) { + return new VaryingConfiguration(chooser, parent); + } + + /** + * Creates a new {@linkplain VaryingConfiguration} that has the same overriding + * attributes as the given other {@linkplain VaryingConfiguration}. + * + * @param other the configuration to copy overrides from + * @param parent the parent to tie the configuration to for inheriting values + * @return a new configuration + */ + @NonNull + public static VaryingConfiguration create( + @NonNull VaryingConfiguration other, + @NonNull Configuration parent) { + VaryingConfiguration configuration = + new VaryingConfiguration(other.mConfigChooser, parent); + initFrom(configuration, other, other, false); + configuration.mAlternate = other.mAlternate; + configuration.mVariation = other.mVariation; + configuration.mVariationCount = other.mVariationCount; + configuration.syncFolderConfig(); + + return configuration; + } + + /** + * Returns the alternate flags for this configuration. Corresponds to + * the {@code CFG_} flags in {@link ConfigurationClient}. + * + * @return the bitmask + */ + public int getAlternateFlags() { + return mAlternate; + } + + @Override + public void syncFolderConfig() { + super.syncFolderConfig(); + updateDisplayName(); + } + + /** + * Sets the variation version for this + * {@linkplain VaryingConfiguration}. There might be multiple + * {@linkplain VaryingConfiguration} instances inheriting from a + * {@link Configuration}. The variation version allows them to choose + * different complementing values, so they don't all flip to the same other + * (out of multiple choices) value. The {@link #setVariationCount(int)} + * value can be used to determine how to partition the buckets of values. + * Also updates the variation count if necessary. + * + * @param variation variation version + */ + public void setVariation(int variation) { + mVariation = variation; + mVariationCount = Math.max(mVariationCount, variation + 1); + } + + /** + * Sets the number of {@link VaryingConfiguration} variations mapped + * to the same parent configuration as this one. See + * {@link #setVariation(int)} for details. + * + * @param count the total number of variation versions + */ + public void setVariationCount(int count) { + mVariationCount = count; + } + + /** + * Updates the display name in this configuration based on the values and override settings + */ + public void updateDisplayName() { + setDisplayName(computeDisplayName()); + } + + @Override + @NonNull + public Locale getLocale() { + if (isOverridingLocale()) { + return super.getLocale(); + } + Locale locale = mParent.getLocale(); + if (isAlternatingLocale() && locale != null) { + List<Locale> locales = mConfigChooser.getLocaleList(); + for (Locale l : locales) { + // TODO: Try to be smarter about which one we pick; for example, try + // to pick a language that is substantially different from the inherited + // language, such as either with the strings of the largest or shortest + // length, or perhaps based on some geography or population metrics + if (!l.equals(locale)) { + locale = l; + break; + } + } + } + + return locale; + } + + @Override + @Nullable + public IAndroidTarget getTarget() { + if (isOverridingTarget()) { + return super.getTarget(); + } + IAndroidTarget target = mParent.getTarget(); + if (isAlternatingTarget() && target != null) { + List<IAndroidTarget> targets = mConfigChooser.getTargetList(); + if (!targets.isEmpty()) { + // Pick a different target: if you're showing the most recent render target, + // then pick the lowest supported target, and vice versa + IAndroidTarget mostRecent = targets.get(targets.size() - 1); + if (target.equals(mostRecent)) { + // Find oldest supported + ManifestInfo info = ManifestInfo.get(mConfigChooser.getProject()); + int minSdkVersion = info.getMinSdkVersion(); + for (IAndroidTarget t : targets) { + if (t.getVersion().getApiLevel() >= minSdkVersion) { + target = t; + break; + } + } + } else { + target = mostRecent; + } + } + } + + return target; + } + + // Cached values, key=parent's device, cached value=device + private Device mPrevParentDevice; + private Device mPrevDevice; + + @Override + @Nullable + public Device getDevice() { + if (isOverridingDevice()) { + return super.getDevice(); + } + Device device = mParent.getDevice(); + if (isAlternatingDevice() && device != null) { + if (device == mPrevParentDevice) { + return mPrevDevice; + } + + mPrevParentDevice = device; + + // Pick a different device + List<Device> devices = mConfigChooser.getDeviceList(); + + // Divide up the available devices into {@link #mVariationCount} + 1 buckets + // (the + 1 is for the bucket now taken up by the inherited value). + // Then assign buckets to each {@link #mVariation} version, and pick one + // from the bucket assigned to this current configuration's variation version. + + // I could just divide up the device list count, but that would treat a lot of + // very similar phones as having the same kind of variety as the 7" and 10" + // tablets which are sitting right next to each other in the device list. + // Instead, do this by screen size. + + + double smallest = 100; + double biggest = 1; + for (Device d : devices) { + double size = getScreenSize(d); + if (size < 0) { + continue; // no data + } + if (size >= biggest) { + biggest = size; + } + if (size <= smallest) { + smallest = size; + } + } + + int bucketCount = mVariationCount + 1; + double inchesPerBucket = (biggest - smallest) / bucketCount; + + double overriddenSize = getScreenSize(device); + int overriddenBucket = (int) ((overriddenSize - smallest) / inchesPerBucket); + int bucket = (mVariation < overriddenBucket) ? mVariation : mVariation + 1; + double from = inchesPerBucket * bucket + smallest; + double to = from + inchesPerBucket; + if (biggest - to < 0.1) { + to = biggest + 0.1; + } + + boolean canScaleNinePatch = supports(Capability.FIXED_SCALABLE_NINE_PATCH); + for (Device d : devices) { + double size = getScreenSize(d); + if (size >= from && size < to) { + if (!canScaleNinePatch) { + Density density = getDensity(d); + if (density == Density.TV || density == Density.LOW) { + continue; + } + } + + device = d; + break; + } + } + + mPrevDevice = device; + } + + return device; + } + + /** + * Returns the density of the given device + * + * @param device the device to check + * @return the density or null + */ + @Nullable + private static Density getDensity(@NonNull Device device) { + Hardware hardware = device.getDefaultHardware(); + if (hardware != null) { + Screen screen = hardware.getScreen(); + if (screen != null) { + return screen.getPixelDensity(); + } + } + + return null; + } + + /** + * Returns the diagonal length of the given device + * + * @param device the device to check + * @return the diagonal length or -1 + */ + private static double getScreenSize(@NonNull Device device) { + Hardware hardware = device.getDefaultHardware(); + if (hardware != null) { + Screen screen = hardware.getScreen(); + if (screen != null) { + return screen.getDiagonalLength(); + } + } + + return -1; + } + + @Override + @Nullable + public State getDeviceState() { + if (isOverridingDeviceState()) { + return super.getDeviceState(); + } + State state = mParent.getDeviceState(); + if (isAlternatingDeviceState() && state != null) { + State alternate = getNextDeviceState(state); + + return alternate; + } else { + if ((isAlternatingDevice() || isOverridingDevice()) && state != null) { + // If the device differs, I need to look up a suitable equivalent state + // on our device + Device device = getDevice(); + if (device != null) { + return device.getState(state.getName()); + } + } + + return state; + } + } + + @Override + @NonNull + public NightMode getNightMode() { + if (isOverridingNightMode()) { + return super.getNightMode(); + } + NightMode nightMode = mParent.getNightMode(); + if (isAlternatingNightMode() && nightMode != null) { + nightMode = nightMode == NightMode.NIGHT ? NightMode.NOTNIGHT : NightMode.NIGHT; + return nightMode; + } else { + return nightMode; + } + } + + @Override + @NonNull + public UiMode getUiMode() { + if (isOverridingUiMode()) { + return super.getUiMode(); + } + UiMode uiMode = mParent.getUiMode(); + if (isAlternatingUiMode() && uiMode != null) { + // TODO: Use manifest's supports screen to decide which are most relevant + // (as well as which available configuration qualifiers are present in the + // layout) + UiMode[] values = UiMode.values(); + uiMode = values[(uiMode.ordinal() + 1) % values.length]; + return uiMode; + } else { + return uiMode; + } + } + + @Override + @Nullable + public String computeDisplayName() { + return computeDisplayName(getOverrideFlags() | mAlternate, this); + } + + /** + * Sets whether the locale should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateLocale(boolean alternate) { + mAlternate |= CFG_LOCALE; + } + + /** + * Returns true if the locale is alternated + * + * @return true if the locale is alternated + */ + public final boolean isAlternatingLocale() { + return (mAlternate & CFG_LOCALE) != 0; + } + + /** + * Sets whether the rendering target should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateTarget(boolean alternate) { + mAlternate |= CFG_TARGET; + } + + /** + * Returns true if the target is alternated + * + * @return true if the target is alternated + */ + public final boolean isAlternatingTarget() { + return (mAlternate & CFG_TARGET) != 0; + } + + /** + * Sets whether the device should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateDevice(boolean alternate) { + mAlternate |= CFG_DEVICE; + } + + /** + * Returns true if the device is alternated + * + * @return true if the device is alternated + */ + public final boolean isAlternatingDevice() { + return (mAlternate & CFG_DEVICE) != 0; + } + + /** + * Sets whether the device state should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateDeviceState(boolean alternate) { + mAlternate |= CFG_DEVICE_STATE; + } + + /** + * Returns true if the device state is alternated + * + * @return true if the device state is alternated + */ + public final boolean isAlternatingDeviceState() { + return (mAlternate & CFG_DEVICE_STATE) != 0; + } + + /** + * Sets whether the night mode should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateNightMode(boolean alternate) { + mAlternate |= CFG_NIGHT_MODE; + } + + /** + * Returns true if the night mode is alternated + * + * @return true if the night mode is alternated + */ + public final boolean isAlternatingNightMode() { + return (mAlternate & CFG_NIGHT_MODE) != 0; + } + + /** + * Sets whether the UI mode should be alternated by this configuration + * + * @param alternate if true, alternate the inherited value + */ + public void setAlternateUiMode(boolean alternate) { + mAlternate |= CFG_UI_MODE; + } + + /** + * Returns true if the UI mode is alternated + * + * @return true if the UI mode is alternated + */ + public final boolean isAlternatingUiMode() { + return (mAlternate & CFG_UI_MODE) != 0; + } + +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java index 15dc356..7b2fe84 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/LayoutDescriptors.java @@ -21,11 +21,13 @@ import static com.android.SdkConstants.ATTR_CLASS; import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_TAG; +import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.FQCN_GESTURE_OVERLAY_VIEW; import static com.android.SdkConstants.REQUEST_FOCUS; import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; import static com.android.SdkConstants.VIEW_MERGE; +import static com.android.SdkConstants.VIEW_TAG; import com.android.SdkConstants; import com.android.ide.common.api.IAttributeInfo.Format; @@ -168,6 +170,14 @@ public final class LayoutDescriptors implements IDescriptorProvider { newDescriptors.addAll(newLayouts); newDescriptors.addAll(newViews); + ViewElementDescriptor viewTag = createViewTag(frameLayoutAttrs); + newViews.add(viewTag); + newDescriptors.add(viewTag); + + ViewElementDescriptor requestFocus = createRequestFocus(); + newViews.add(requestFocus); + newDescriptors.add(requestFocus); + // Link all layouts to everything else here.. recursively for (ViewElementDescriptor layoutDesc : newLayouts) { layoutDesc.setChildren(newDescriptors); @@ -184,10 +194,6 @@ public final class LayoutDescriptors implements IDescriptorProvider { fixSuperClasses(infoDescMap); - ViewElementDescriptor requestFocus = createRequestFocus(); - newViews.add(requestFocus); - newDescriptors.add(requestFocus); - // The <merge> tag can only be a root tag, so it is added at the end. // It gets everything else as children but it is not made a child itself. ViewElementDescriptor mergeTag = createMerge(frameLayoutAttrs); @@ -305,7 +311,7 @@ public final class LayoutDescriptors implements IDescriptorProvider { } /** - * Creates a new <include> descriptor and adds it to the list of view descriptors. + * Creates a new {@code <include>} descriptor and adds it to the list of view descriptors. * * @param knownViews A list of view descriptors being populated. Also used to find the * View descriptor and extract its layout attributes. @@ -316,6 +322,18 @@ public final class LayoutDescriptors implements IDescriptorProvider { // Create the include custom attributes ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>(); + // Find View and inherit all its layout attributes + AttributeDescriptor[] viewLayoutAttribs; + AttributeDescriptor[] viewAttributes = null; + ViewElementDescriptor viewDesc = findDescriptorByClass(SdkConstants.CLASS_VIEW); + if (viewDesc != null) { + viewAttributes = viewDesc.getAttributes(); + attributes = new ArrayList<AttributeDescriptor>(viewAttributes.length + 1); + viewLayoutAttribs = viewDesc.getLayoutAttributes(); + } else { + viewLayoutAttribs = new AttributeDescriptor[0]; + } + // Note that the "layout" attribute does NOT have the Android namespace DescriptorsUtils.appendAttribute(attributes, null, //elementXmlName @@ -326,18 +344,11 @@ public final class LayoutDescriptors implements IDescriptorProvider { true, //required null); //overrides - DescriptorsUtils.appendAttribute(attributes, - null, //elementXmlName - ANDROID_URI, //nsUri - new AttributeInfo( - "id", //$NON-NLS-1$ - Format.REFERENCE_SET ), - true, //required - null); //overrides - - // Find View and inherit all its layout attributes - AttributeDescriptor[] viewLayoutAttribs = findViewLayoutAttributes( - SdkConstants.CLASS_VIEW); + if (viewAttributes != null) { + for (AttributeDescriptor descriptor : viewAttributes) { + attributes.add(descriptor); + } + } // Create the include descriptor ViewElementDescriptor desc = new ViewElementDescriptor(xmlName, @@ -451,6 +462,37 @@ public final class LayoutDescriptors implements IDescriptorProvider { } /** + * Creates and returns a new {@code <view>} descriptor. + * @param viewLayoutAttribs The layout attributes to use for the new descriptor + * @param styleMap The style map provided by the SDK + */ + private ViewElementDescriptor createViewTag(AttributeDescriptor[] viewLayoutAttribs) { + String xmlName = VIEW_TAG; + + TextAttributeDescriptor classAttribute = new ClassAttributeDescriptor( + CLASS_VIEW, + ATTR_CLASS, null /* namespace */, + new AttributeInfo(ATTR_CLASS, Format.STRING_SET), + true /*mandatory*/) + .setTooltip("Supply the name of the view class to instantiate"); + + // Create the include descriptor + ViewElementDescriptor desc = new ViewElementDescriptor(xmlName, + xmlName, // ui_name + xmlName, // "class name"; the GLE only treats this as an element tag + "A view tag whose class attribute names the class to be instantiated", // tooltip + null, // sdk_url + new AttributeDescriptor[] { // attributes + classAttribute + }, + viewLayoutAttribs, // layout attributes + null, // children + false /* mandatory */); + + return desc; + } + + /** * Creates and returns a new {@code <requestFocus>} descriptor. */ private ViewElementDescriptor createRequestFocus() { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java index 466720a..7999524 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/ViewElementDescriptor.java @@ -19,6 +19,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.descriptors; import static com.android.SdkConstants.ANDROID_VIEW_PKG; import static com.android.SdkConstants.ANDROID_WEBKIT_PKG; import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; +import static com.android.SdkConstants.VIEW; +import static com.android.SdkConstants.VIEW_TAG; import com.android.ide.common.resources.platform.AttributeInfo; import com.android.ide.eclipse.adt.AdtPlugin; @@ -188,6 +190,10 @@ public class ViewElementDescriptor extends ElementDescriptor { // "android.gesture.GestureOverlayView" in their XML, we need to look up // only by basename name = name.substring(name.lastIndexOf('.') + 1); + } else if (VIEW_TAG.equals(name)) { + // Can't have both view.png and View.png; issues on case sensitive vs + // case insensitive file systems + name = VIEW; } Image icon = factory.getIcon(name); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java new file mode 100644 index 0000000..9fc2e09 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/BinPacker.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2012 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 com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.imageio.ImageIO; + +/** + * This class implements 2D bin packing: packing rectangles into a given area as + * tightly as "possible" (bin packing in general is NP hard, so this class uses + * heuristics). + * <p> + * The algorithm implemented is to keep a set of (possibly overlapping) + * available areas for placement. For each newly inserted rectangle, we first + * pick which available space to occupy, and we then subdivide the + * current rectangle into all the possible remaining unoccupied sub-rectangles. + * We also remove any other space rectangles which are no longer eligible if + * they are intersecting the newly placed rectangle. + * <p> + * This algorithm is not very fast, so should not be used for a large number of + * rectangles. + */ +class BinPacker { + /** + * When enabled, the successive passes are dumped as PNG images showing the + * various available and occupied rectangles) + */ + private static final boolean DEBUG = false; + + private final List<Rect> mSpace = new ArrayList<Rect>(); + private final int mMinHeight; + private final int mMinWidth; + + /** + * Creates a new {@linkplain BinPacker}. To use it, first add one or more + * initial available space rectangles with {@link #addSpace(Rect)}, and then + * place the rectangles with {@link #occupy(int, int)}. The returned + * {@link Rect} from {@link #occupy(int, int)} gives the coordinates of the + * positioned rectangle. + * + * @param minWidth the smallest width of any rectangle placed into this bin + * @param minHeight the smallest height of any rectangle placed into this bin + */ + BinPacker(int minWidth, int minHeight) { + mMinWidth = minWidth; + mMinHeight = minHeight; + + if (DEBUG) { + mAllocated = new ArrayList<Rect>(); + sLayoutId++; + sRectId = 1; + } + } + + /** Adds more available space */ + void addSpace(Rect rect) { + if (rect.w >= mMinWidth && rect.h >= mMinHeight) { + mSpace.add(rect); + } + } + + /** Attempts to place a rectangle of the given dimensions, if possible */ + @Nullable + Rect occupy(int width, int height) { + int index = findBest(width, height); + if (index == -1) { + return null; + } + + return split(index, width, height); + } + + /** + * Finds the best available space rectangle to position a new rectangle of + * the given size in. + */ + private int findBest(int width, int height) { + if (mSpace.isEmpty()) { + return -1; + } + + // Try to pack as far up as possible first + int bestIndex = -1; + boolean multipleAtSameY = false; + int minY = Integer.MAX_VALUE; + for (int i = 0, n = mSpace.size(); i < n; i++) { + Rect rect = mSpace.get(i); + if (rect.y <= minY) { + if (rect.w >= width && rect.h >= height) { + if (rect.y < minY) { + minY = rect.y; + multipleAtSameY = false; + bestIndex = i; + } else if (minY == rect.y) { + multipleAtSameY = true; + } + } + } + } + + if (!multipleAtSameY) { + return bestIndex; + } + + bestIndex = -1; + + // Pick a rectangle. This currently tries to find the rectangle whose shortest + // side most closely matches the placed rectangle's size. + // Attempt to find the best short side fit + int bestShortDistance = Integer.MAX_VALUE; + int bestLongDistance = Integer.MAX_VALUE; + + for (int i = 0, n = mSpace.size(); i < n; i++) { + Rect rect = mSpace.get(i); + if (rect.y != minY) { // Only comparing elements at same y + continue; + } + if (rect.w >= width && rect.h >= height) { + if (width < height) { + int distance = rect.w - width; + if (distance < bestShortDistance || + distance == bestShortDistance && + (rect.h - height) < bestLongDistance) { + bestShortDistance = distance; + bestLongDistance = rect.h - height; + bestIndex = i; + } + } else { + int distance = rect.w - width; + if (distance < bestShortDistance || + distance == bestShortDistance && + (rect.h - height) < bestLongDistance) { + bestShortDistance = distance; + bestLongDistance = rect.h - height; + bestIndex = i; + } + } + } + } + + return bestIndex; + } + + /** + * Removes the rectangle at the given index. Since the rectangles are in an + * {@link ArrayList}, removing a rectangle in the normal way is slow (it + * would involve shifting all elements), but since we don't care about + * order, this always swaps the to-be-deleted element to the last position + * in the array first, <b>then</b> it deletes it (which should be + * immediate). + * + * @param index the index in the {@link #mSpace} list to remove a rectangle + * from + */ + private void removeRect(int index) { + assert !mSpace.isEmpty(); + int lastIndex = mSpace.size() - 1; + if (index != lastIndex) { + // Swap before remove to make deletion faster since we don't + // care about order + Rect temp = mSpace.get(index); + mSpace.set(index, mSpace.get(lastIndex)); + mSpace.set(lastIndex, temp); + } + + mSpace.remove(lastIndex); + } + + /** + * Splits the rectangle at the given rectangle index such that it can contain + * a rectangle of the given width and height. */ + private Rect split(int index, int width, int height) { + Rect rect = mSpace.get(index); + assert rect.w >= width && rect.h >= height : rect; + + Rect r = new Rect(rect); + r.w = width; + r.h = height; + + // Remove all rectangles that intersect my rectangle + for (int i = 0; i < mSpace.size(); i++) { + Rect other = mSpace.get(i); + if (other.intersects(r)) { + removeRect(i); + i--; + } + } + + + // Split along vertical line x = rect.x + width: + // (rect.x,rect.y) + // +-------------+-------------------------+ + // | | | + // | | | + // | | height | + // | | | + // | | | + // +-------------+ B | rect.h + // | width | + // | | | + // | A | + // | | | + // | | + // +---------------------------------------+ + // rect.w + int remainingHeight = rect.h - height; + int remainingWidth = rect.w - width; + if (remainingHeight >= mMinHeight) { + mSpace.add(new Rect(rect.x, rect.y + height, width, remainingHeight)); + } + if (remainingWidth >= mMinWidth) { + mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, rect.h)); + } + + // Split along horizontal line y = rect.y + height: + // +-------------+-------------------------+ + // | | | + // | | height | + // | | A | + // | | | + // | | | rect.h + // +-------------+ - - - - - - - - - - - - | + // | width | + // | | + // | B | + // | | + // | | + // +---------------------------------------+ + // rect.w + if (remainingHeight >= mMinHeight) { + mSpace.add(new Rect(rect.x, rect.y + height, rect.w, remainingHeight)); + } + if (remainingWidth >= mMinWidth) { + mSpace.add(new Rect(rect.x + width, rect.y, remainingWidth, height)); + } + + // Remove redundant rectangles. This is not very efficient. + for (int i = 0; i < mSpace.size() - 1; i++) { + for (int j = i + 1; j < mSpace.size(); j++) { + Rect iRect = mSpace.get(i); + Rect jRect = mSpace.get(j); + if (jRect.contains(iRect)) { + removeRect(i); + i--; + break; + } + if (iRect.contains(jRect)) { + removeRect(j); + j--; + } + } + } + + if (DEBUG) { + mAllocated.add(r); + dumpImage(); + } + + return r; + } + + // DEBUGGING CODE: Enable with DEBUG + + private List<Rect> mAllocated; + private static int sLayoutId; + private static int sRectId; + private void dumpImage() { + if (DEBUG) { + int width = 100; + int height = 100; + for (Rect rect : mSpace) { + width = Math.max(width, rect.w); + height = Math.max(height, rect.h); + } + width += 10; + height += 10; + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.setColor(Color.BLACK); + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + + Color[] colors = new Color[] { + Color.blue, Color.cyan, + Color.green, Color.magenta, Color.orange, + Color.pink, Color.red, Color.white, Color.yellow, Color.darkGray, + Color.lightGray, Color.gray, + }; + + char allocated = 'A'; + for (Rect rect : mAllocated) { + Color color = new Color(0x9FFFFFFF, true); + g.setColor(color); + g.setBackground(color); + g.fillRect(rect.x, rect.y, rect.w, rect.h); + g.setColor(Color.WHITE); + g.drawRect(rect.x, rect.y, rect.w, rect.h); + g.drawString("" + (allocated++), + rect.x + rect.w / 2, rect.y + rect.h / 2); + } + + int colorIndex = 0; + for (Rect rect : mSpace) { + Color color = colors[colorIndex]; + colorIndex = (colorIndex + 1) % colors.length; + + color = new Color(color.getRed(), color.getGreen(), color.getBlue(), 128); + g.setColor(color); + + g.fillRect(rect.x, rect.y, rect.w, rect.h); + g.setColor(Color.WHITE); + g.drawString(Integer.toString(colorIndex), + rect.x + rect.w / 2, rect.y + rect.h / 2); + } + + + g.dispose(); + + File file = new File("/tmp/layout" + sLayoutId + "_pass" + sRectId + ".png"); + try { + ImageIO.write(image, "PNG", file); + System.out.println("Wrote diagnostics image " + file); + } catch (IOException e) { + e.printStackTrace(); + } + sRectId++; + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java index 5650772..ad5bd52 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CanvasTransform.java @@ -40,6 +40,9 @@ public class CanvasTransform { /** Canvas image size (original, before zoom), in pixels. */ private int mImgSize; + /** Full size being scrolled (after zoom), in pixels */ + private int mFullSize;; + /** Client size, in pixels. */ private int mClientSize; @@ -83,6 +86,11 @@ public class CanvasTransform { } } + /** Recomputes the scrollbar and view port settings */ + public void refresh() { + resizeScrollbar(); + } + /** * Returns current scaling factor. * @@ -110,14 +118,17 @@ public class CanvasTransform { return (int) (mImgSize * mScale); } - /** Changes the size of the canvas image and the client size. Recomputes scrollbars. */ - public void setSize(int imgSize, int clientSize) { + /** + * Changes the size of the canvas image and the client size. Recomputes + * scrollbars. + * + * @param imgSize the size of the image being scaled + * @param fullSize the size of the full view area being scrolled + * @param clientSize the size of the view port + */ + public void setSize(int imgSize, int fullSize, int clientSize) { mImgSize = imgSize; - setClientSize(clientSize); - } - - /** Changes the size of the client size. Recomputes scrollbars. */ - public void setClientSize(int clientSize) { + mFullSize = fullSize; mClientSize = clientSize; mScrollbar.setPageIncrement(clientSize); resizeScrollbar(); @@ -125,7 +136,7 @@ public class CanvasTransform { private void resizeScrollbar() { // scaled image size - int sx = (int) (mImgSize * mScale); + int sx = (int) (mScale * mFullSize); // Adjust margin such that for zoomed out views // we don't waste space (unless the viewport is @@ -150,6 +161,11 @@ public class CanvasTransform { mMargin = DEFAULT_MARGIN; } + if (mCanvas.getPreviewManager().hasPreviews()) { + // Make more room for the previews + mMargin = 2; + } + // actual client area is always reduced by the margins int cx = mClientSize - 2 * mMargin; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java index ae2737b..2634569 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java @@ -304,7 +304,7 @@ public class ClipboardSupport { * In case of success, the new element will have some default attributes set (xmlns:android, * layout_width and height). The edit is wrapped in a proper undo. * <p/> - * Implementation is similar to {@link #createDocumentRoot(String)} except we also + * Implementation is similar to {@link #createDocumentRoot} except we also * copy all the attributes and inner elements recursively. */ private void pasteInEmptyDocument(final IDragElement pastedElement) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java index f0698e6..44cd081 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/CreateNewConfigJob.java @@ -22,6 +22,7 @@ import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.resources.ResourceFolderType; +import com.google.common.base.Charsets; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; @@ -35,8 +36,8 @@ import org.eclipse.core.runtime.jobs.Job; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PartInitException; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; /** Job which creates a new layout file for a given configuration */ class CreateNewConfigJob extends Job { @@ -64,19 +65,17 @@ class CreateNewConfigJob extends Job { IFolder res = (IFolder) mFromFile.getParent().getParent(); IFolder newParentFolder = res.getFolder(folderName); - if (newParentFolder.exists()) { - // this should not happen since aapt would have complained - // before, but if one disables the automatic build, this could - // happen. - String message = String.format("File 'res/%1$s' already exists!", - folderName); - return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); - } - AdtUtils.ensureExists(newParentFolder); final IFile file = newParentFolder.getFile(mFromFile.getName()); + if (file.exists()) { + String message = String.format("File 'res/%1$s/%2$s' already exists!", + folderName, mFromFile.getName()); + return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); + } - InputStream input = mFromFile.getContents(); + // Read current document contents instead of from file: mFromFile.getContents() + String text = mEditor.getEditorDelegate().getEditor().getStructuredDocument().get(); + ByteArrayInputStream input = new ByteArrayInputStream(text.getBytes(Charsets.UTF_8)); file.create(input, false, monitor); input.close(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java index 1625195..145036b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java @@ -15,19 +15,17 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.TOOLS_URI; - - import static org.eclipse.wst.xml.core.internal.provisional.contenttype.ContentTypeIdForXML.ContentTypeID_XML; -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.utils.Pair; @@ -61,6 +59,7 @@ import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; /** * Various utility methods for manipulating DOM nodes. @@ -186,6 +185,31 @@ public class DomUtilities { } /** + * Returns the DOM document for the given editor + * + * @param editor the XML editor + * @return the document, or null if not found or not parsed properly (no + * errors are generated/thrown) + */ + @Nullable + public static Document getDocument(@NonNull AndroidXmlEditor editor) { + IStructuredModel model = editor.getModelForRead(); + try { + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + return domModel.getDocument(); + } + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return null; + } + + + /** * Returns the XML DOM node corresponding to the given offset of the given * document. * @@ -828,6 +852,33 @@ public class DomUtilities { } /** + * Creates an empty non-Eclipse XML document. + * This is used when you need to use XML operations not supported by + * the Eclipse XML model (such as serialization). + * <p> + * The new document will not validate, will ignore comments, and will + * support namespace. + * + * @return the new document + */ + @Nullable + public static Document createEmptyPlainDocument() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setIgnoringComments(true); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + return builder.newDocument(); + } catch (ParserConfigurationException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** * Parses the given XML string as a DOM document, using the JDK parser. * The parser does not validate, and is namespace aware. * diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java index 6829c40..ac3328d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExportScreenshotAction.java @@ -42,7 +42,7 @@ class ExportScreenshotAction extends Action { @Override public void run() { - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); ImageOverlay imageOverlay = mCanvas.getImageOverlay(); BufferedImage image = imageOverlay.getAwtImage(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java index 0dbd152..f7085fc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/FragmentMenu.java @@ -16,28 +16,24 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_CLASS; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMetadata.KEY_FRAGMENT_LAYOUT; - -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator; -import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; -import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; import com.android.resources.ResourceType; import com.android.utils.Pair; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.core.IJavaProject; @@ -46,10 +42,8 @@ import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.Separator; -import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.window.Window; import org.eclipse.swt.widgets.Menu; -import org.eclipse.swt.widgets.Shell; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -292,33 +286,12 @@ public class FragmentMenu extends SubmenuAction { @Override public void run() { LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); - IProject project = delegate.getEditor().getProject(); - // get the resource repository for this project and the system resources. - ResourceRepository projectRepository = ResourceManager.getInstance() - .getProjectResources(project); - Shell shell = mCanvas.getShell(); - - AndroidTargetData data = delegate.getEditor().getTargetData(); - ResourceRepository systemRepository = data.getFrameworkResources(); - - ResourceChooser dlg = new ResourceChooser(project, - ResourceType.LAYOUT, projectRepository, - systemRepository, shell); - - IInputValidator validator = - CyclicDependencyValidator.create(delegate.getEditor().getInputFile()); - - if (validator != null) { - // Ensure wide enough to accommodate validator error message - dlg.setSize(85, 10); - dlg.setInputValidator(validator); - } - - String layout = getSelectedLayout(); - if (layout != null) { - dlg.setCurrentResource(layout); - } - + IFile file = delegate.getEditor().getInputFile(); + GraphicalEditorPart editor = delegate.getGraphicalEditor(); + ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT) + .setInputValidator(CyclicDependencyValidator.create(file)) + .setInitialSize(85, 10) + .setCurrentResource(getSelectedLayout()); int result = dlg.open(); if (result == ResourceChooser.CLEAR_RETURN_CODE) { setNewLayout(null); 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 468d159..98bc25e 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 @@ -39,6 +39,7 @@ import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseTrackListener; import org.eclipse.swt.events.TypedEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Device; @@ -433,7 +434,8 @@ public class GestureManager { * Helper class which implements the {@link MouseMoveListener}, * {@link MouseListener} and {@link KeyListener} interfaces. */ - private class Listener implements MouseMoveListener, MouseListener, KeyListener { + private class Listener implements MouseMoveListener, MouseListener, MouseTrackListener, + KeyListener { // --- MouseMoveListener --- @@ -443,15 +445,16 @@ public class GestureManager { mLastMouseY = e.y; mLastStateMask = e.stateMask; + ControlPoint controlPoint = ControlPoint.create(mCanvas, e); if ((e.stateMask & SWT.BUTTON_MASK) != 0) { if (mCurrentGesture != null) { - ControlPoint controlPoint = ControlPoint.create(mCanvas, e); updateMouse(controlPoint, e); mCanvas.redraw(); } } else { - updateCursor(ControlPoint.create(mCanvas, e)); + updateCursor(controlPoint); mCanvas.hover(e); + mCanvas.getPreviewManager().moved(controlPoint); } } @@ -460,7 +463,13 @@ public class GestureManager { @Override public void mouseUp(MouseEvent e) { ControlPoint mousePos = ControlPoint.create(mCanvas, e); + if (mCurrentGesture == null) { + // If clicking on a configuration preview, just process it there + if (mCanvas.getPreviewManager().click(mousePos)) { + return; + } + // Just a click, select Pair<SelectionItem, SelectionHandle> handlePair = mCanvas.getSelectionManager().findHandle(mousePos); @@ -507,6 +516,24 @@ public class GestureManager { } } + // --- MouseTrackListener --- + + @Override + public void mouseEnter(MouseEvent e) { + ControlPoint mousePos = ControlPoint.create(mCanvas, e); + mCanvas.getPreviewManager().enter(mousePos); + } + + @Override + public void mouseExit(MouseEvent e) { + ControlPoint mousePos = ControlPoint.create(mCanvas, e); + mCanvas.getPreviewManager().exit(mousePos); + } + + @Override + public void mouseHover(MouseEvent e) { + } + // --- KeyListener --- @Override 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 726824f..051c568 100644 --- 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 @@ -30,8 +30,10 @@ import static com.android.SdkConstants.STRING_PREFIX; import static com.android.SdkConstants.VALUE_FILL_PARENT; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; -import static com.android.ide.eclipse.adt.AdtUtils.isUiThread; -import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser.NAME_CONFIG_STATE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_FOLDER; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_TARGET; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor.viewNeedsPackage; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_EAST; import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.DOCK_WEST; @@ -41,7 +43,6 @@ import static org.eclipse.wb.core.controls.flyout.IFlyoutPreferences.STATE_OPEN; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.ide.common.api.Rect; import com.android.ide.common.layout.BaseLayoutRule; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.StaticRenderSession; @@ -71,6 +72,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationMatcher; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LayoutCreatorDialog; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors; @@ -643,7 +645,7 @@ public class GraphicalEditorPart extends EditorPart // ---- Implements ConfigurationClient ---- @Override public void aboutToChange(int flags) { - if ((flags & CHANGED_RENDER_TARGET) != 0) { + if ((flags & CFG_TARGET) != 0) { IAndroidTarget oldTarget = mConfigChooser.getConfiguration().getTarget(); preRenderingTargetChangeCleanUp(oldTarget); } @@ -671,8 +673,12 @@ public class GraphicalEditorPart extends EditorPart if (mEditorDelegate.getEditor().isCreatingPages()) { recomputeLayout(); } else { + boolean affectsFileSelection = (flags & Configuration.MASK_FILE_ATTRS) != 0; + IFile best = null; // get the resources of the file's project. - IFile best = ConfigurationMatcher.getBestFileMatch(mConfigChooser); + if (affectsFileSelection) { + best = ConfigurationMatcher.getBestFileMatch(mConfigChooser); + } if (best != null) { if (!best.equals(mEditedFile)) { try { @@ -682,12 +688,12 @@ public class GraphicalEditorPart extends EditorPart boolean reuseEditor = AdtPrefs.getPrefs().isSharedLayoutEditor(); if (!reuseEditor) { - String data = AdtPlugin.getFileProperty(best, NAME_CONFIG_STATE); + String data = ConfigurationDescription.getDescription(best); if (data == null) { // Not previously opened: duplicate the current state as // much as possible data = mConfigChooser.getConfiguration().toPersistentString(); - AdtPlugin.setFileProperty(best, NAME_CONFIG_STATE, data); + ConfigurationDescription.setDescription(best, data); } } @@ -710,7 +716,7 @@ public class GraphicalEditorPart extends EditorPart // Even though the layout doesn't change, the config changed, and referenced // resources need to be updated. recomputeLayout(); - } else { + } else if (affectsFileSelection) { // display the error. Configuration configuration = mConfigChooser.getConfiguration(); FolderConfiguration currentConfig = configuration.getFullConfig(); @@ -728,10 +734,15 @@ public class GraphicalEditorPart extends EditorPart currentConfig.toDisplayString(), currentConfig.getFolderName(ResourceFolderType.LAYOUT), mEditedFile.getName()); + } else { + // Something else changed, such as the theme - just recompute existing + // layout + mConfigChooser.saveConstraints(); + recomputeLayout(); } } - if ((flags & CHANGED_RENDER_TARGET) != 0) { + if ((flags & CFG_TARGET) != 0) { Configuration configuration = mConfigChooser.getConfiguration(); IAndroidTarget target = configuration.getTarget(); Sdk current = Sdk.getCurrent(); @@ -741,17 +752,19 @@ public class GraphicalEditorPart extends EditorPart } } - if ((flags & (CHANGED_DEVICE | CHANGED_DEVICE_CONFIG)) != 0) { + if ((flags & (CFG_DEVICE | CFG_DEVICE_STATE)) != 0) { // When the device changes, zoom the view to fit, but only up to 100% (e.g. zoom // out to fit the content, or zoom back in if we were zoomed out more from the // previous view, but only up to 100% such that we never blow up pixels if (mActionBar.isZoomingAllowed()) { - getCanvasControl().setFitScale(true); + getCanvasControl().setFitScale(true, true /*allowZoomIn*/); } } reloadPalette(); + getCanvasControl().getPreviewManager().configurationChanged(flags); + return true; } @@ -872,6 +885,12 @@ public class GraphicalEditorPart extends EditorPart return mIncludedWithin; } + @Override + @Nullable + public LayoutCanvas getCanvas() { + return getCanvasControl(); + } + /** * Listens to target changed in the current project, to trigger a new layout rendering. */ @@ -906,7 +925,7 @@ public class GraphicalEditorPart extends EditorPart IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject()); if (target != null) { mConfigChooser.onSdkLoaded(target); - changed(CHANGED_FOLDER | CHANGED_RENDER_TARGET); + changed(CFG_FOLDER | CFG_TARGET); } } } @@ -1039,6 +1058,8 @@ public class GraphicalEditorPart extends EditorPart if (mNeedsRecompute) { recomputeLayout(); } + + mCanvasViewer.getCanvas().syncPreviewMode(); } } @@ -1165,7 +1186,7 @@ public class GraphicalEditorPart extends EditorPart AndroidTargetData targetData = mConfigChooser.onXmlModelLoaded(); updateCapabilities(targetData); - changed(CHANGED_FOLDER | CHANGED_RENDER_TARGET); + changed(CFG_FOLDER | CFG_TARGET); } /** Updates the capabilities for the given target data (which may be null) */ @@ -1253,6 +1274,7 @@ public class GraphicalEditorPart extends EditorPart } UiDocumentNode model = getModel(); + LayoutCanvas canvas = mCanvasViewer.getCanvas(); if (!ensureModelValid(model)) { // Although we display an error, we still treat an empty document as a // successful layout result so that we can drop new elements in it. @@ -1260,7 +1282,7 @@ public class GraphicalEditorPart extends EditorPart // For that purpose, create a special LayoutScene that has no image, // no root view yet indicates success and then update the canvas with it. - mCanvasViewer.getCanvas().setSession( + canvas.setSession( new StaticRenderSession( Result.Status.SUCCESS.createResult(), null /*rootViewInfo*/, null /*image*/), @@ -1278,6 +1300,8 @@ public class GraphicalEditorPart extends EditorPart IProject project = mEditedFile.getProject(); renderWithBridge(project, model, layoutLib); + + canvas.getPreviewManager().renderPreviews(); } } finally { // no matter the result, we are done doing the recompute based on the latest @@ -1310,15 +1334,6 @@ public class GraphicalEditorPart extends EditorPart } /** - * Returns the current bounds of the Android device screen, in canvas control pixels. - * - * @return the bounds of the screen, never null - */ - public Rect getScreenBounds() { - return mConfigChooser.getConfiguration().getScreenBounds(); - } - - /** * Returns the scale to multiply pixels in the layout coordinate space with to obtain * the corresponding dip (device independent pixel) * @@ -1435,7 +1450,7 @@ public class GraphicalEditorPart extends EditorPart } } else if (displayError) { // target == null - displayError("The project target is not set."); + displayError("The project target is not set. Right click project, choose Properties | Android."); } } else if (displayError) { // currentSdk == null displayError("Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.", @@ -1462,11 +1477,6 @@ public class GraphicalEditorPart extends EditorPart return null; } - if (mConfigChooser.isDisposed()) { - return null; - } - assert isUiThread(); - // attempt to get a target from the configuration selector. IAndroidTarget renderingTarget = mConfigChooser.getConfiguration().getTarget(); if (renderingTarget != null) { @@ -1501,6 +1511,10 @@ public class GraphicalEditorPart extends EditorPart private boolean ensureModelValid(UiDocumentNode model) { // check there is actually a model (maybe the file is empty). if (model.getUiChildren().size() == 0) { + if (mEditorDelegate.getEditor().isCreatingPages()) { + displayError("Loading editor"); + return false; + } displayError( "No XML content. Please add a root view or layout to your document."); return false; @@ -1513,7 +1527,6 @@ public class GraphicalEditorPart extends EditorPart LayoutLibrary layoutLib) { LayoutCanvas canvas = getCanvasControl(); Set<UiElementNode> explodeNodes = canvas.getNodesToExplode(); - Rect rect = getScreenBounds(); RenderLogger logger = new RenderLogger(mEditedFile.getName()); RenderingMode renderingMode = RenderingMode.NORMAL; // FIXME set the rendering mode using ViewRule or something. @@ -1525,7 +1538,6 @@ public class GraphicalEditorPart extends EditorPart RenderSession session = RenderService.create(this) .setModel(model) - .setSize(rect.w, rect.h) .setLog(logger) .setRenderingMode(renderingMode) .setIncludedWithin(mIncludedWithin) @@ -1565,7 +1577,7 @@ public class GraphicalEditorPart extends EditorPart } else if (missingClasses.size() > 0 || brokenClasses.size() > 0) { displayFailingClasses(missingClasses, brokenClasses, false); displayUserStackTrace(logger, true); - } else { + } else if (session != null) { // Nope, no missing or broken classes. Clear success, congrats! hideError(); @@ -1583,6 +1595,8 @@ public class GraphicalEditorPart extends EditorPart job.setSystem(true); job.schedule(3000); // 3 seconds } + + mConfigChooser.ensureInitialized(); } model.refreshUi(); @@ -2043,6 +2057,21 @@ public class GraphicalEditorPart extends EditorPart "or fix the theme style references.\n\n"); } + List<Throwable> trace = logger.getFirstTrace(); + if (trace != null + && trace.toString().contains( + "java.lang.IndexOutOfBoundsException: Index: 2, Size: 2") //$NON-NLS-1$ + && mConfigChooser.getConfiguration().getDensity() == Density.TV) { + addBoldText(mErrorLabel, + "It looks like you are using a render target where the layout library " + + "does not support the tvdpi density.\n\n"); + addText(mErrorLabel, "Please try either updating to " + + "the latest available version (using the SDK manager), or if no updated " + + "version is available for this specific version of Android, try using " + + "a more recent render target version.\n\n"); + + } + if (hasAaptErrors && logger.seenTagPrefix(LayoutLog.TAG_RESOURCES_PREFIX)) { // Text will automatically be wrapped by the error widget so no reason // to insert linebreaks in this error message: @@ -2337,7 +2366,7 @@ public class GraphicalEditorPart extends EditorPart @SuppressWarnings("restriction") String id = BuildPathsPropertyPage.PROP_ID; PreferencesUtil.createPropertyDialogOn( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), getProject(), id, null, null).open(); break; case LINK_OPEN_CLASS: @@ -2798,7 +2827,7 @@ public class GraphicalEditorPart extends EditorPart // Auto zoom the surface if you open or close flyout windows such as the palette // or the property/outline views if (newState == STATE_OPEN || newState == STATE_COLLAPSED && oldState == STATE_OPEN) { - getCanvasControl().setFitScale(true /*onlyZoomOut*/); + getCanvasControl().setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); } sDockingStateVersion++; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java index c55d0d8..b5bc9aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ImageUtils.java @@ -21,6 +21,12 @@ import static com.android.SdkConstants.DOT_GIF; import static com.android.SdkConstants.DOT_JPG; import static com.android.SdkConstants.DOT_PNG; import static com.android.utils.SdkUtils.endsWithIgnoreCase; +import static java.awt.RenderingHints.KEY_ANTIALIASING; +import static java.awt.RenderingHints.KEY_INTERPOLATION; +import static java.awt.RenderingHints.KEY_RENDERING; +import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; +import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR; +import static java.awt.RenderingHints.VALUE_RENDER_QUALITY; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -34,7 +40,6 @@ import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.IOException; @@ -471,6 +476,7 @@ public class ImageUtils { * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. + * The size of the shadow is {@link #SHADOW_SIZE}. * * @param image the image to draw the shadow into * @param x the left coordinate of the left hand side of the rectangle @@ -489,12 +495,40 @@ public class ImageUtils { } /** + * Draws a small drop shadow for the given rectangle into the given context. It + * will not draw anything if the rectangle is smaller than a minimum + * determined by the assets used to draw the shadow graphics. + * The size of the shadow is {@link #SMALL_SHADOW_SIZE}. + * + * @param image the image to draw the shadow into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static final void drawSmallRectangleShadow(BufferedImage image, + int x, int y, int width, int height) { + Graphics gc = image.getGraphics(); + try { + drawSmallRectangleShadow(gc, x, y, width, height); + } finally { + gc.dispose(); + } + } + + /** * The width and height of the drop shadow painted by * {@link #drawRectangleShadow(Graphics, int, int, int, int)} */ public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics /** + * The width and height of the drop shadow painted by + * {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)} + */ + public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics + + /** * Draws a drop shadow for the given rectangle into the given context. It * will not draw anything if the rectangle is smaller than a minimum * determined by the assets used to draw the shadow graphics. @@ -569,8 +603,82 @@ public class ImageUtils { null); } + /** + * Draws a small drop shadow for the given rectangle into the given context. It + * will not draw anything if the rectangle is smaller than a minimum + * determined by the assets used to draw the shadow graphics. + * <p> + * + * @param gc the graphics context to draw into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static final void drawSmallRectangleShadow(Graphics gc, + int x, int y, int width, int height) { + if (sShadow2BottomLeft == null) { + // Shadow graphics. This was generated by creating a drop shadow in + // Gimp, using the parameters x offset=5, y offset=%, blur radius=5, + // color=black, and opacity=51. These values attempt to make a shadow + // that is legible both for dark and light themes, on top of the + // canvas background (rgb(150,150,150). Darker shadows would tend to + // blend into the foreground for a dark holo screen, and lighter shadows + // would be hard to spot on the canvas background. If you make adjustments, + // make sure to check the shadow with both dark and light themes. + // + // After making the graphics, I cut out the top right, bottom left + // and bottom right corners as 20x20 images, and these are reproduced by + // painting them in the corresponding places in the target graphics context. + // I then grabbed a single horizontal gradient line from the middle of the + // right edge,and a single vertical gradient line from the bottom. These + // are then painted scaled/stretched in the target to fill the gaps between + // the three corner images. + // + // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right + sShadow2BottomLeft = readImage("shadow2-bl.png"); //$NON-NLS-1$ + sShadow2Bottom = readImage("shadow2-b.png"); //$NON-NLS-1$ + sShadow2BottomRight = readImage("shadow2-br.png"); //$NON-NLS-1$ + sShadow2Right = readImage("shadow2-r.png"); //$NON-NLS-1$ + sShadow2TopRight = readImage("shadow2-tr.png"); //$NON-NLS-1$ + assert sShadow2BottomLeft != null; + assert sShadow2TopRight != null; + assert sShadow2BottomRight.getWidth() == SMALL_SHADOW_SIZE; + assert sShadow2BottomRight.getHeight() == SMALL_SHADOW_SIZE; + } + + int blWidth = sShadow2BottomLeft.getWidth(); + int trHeight = sShadow2TopRight.getHeight(); + if (width < blWidth) { + return; + } + if (height < trHeight) { + return; + } + + gc.drawImage(sShadow2BottomLeft, x, y + height, null); + gc.drawImage(sShadow2BottomRight, x + width, y + height, null); + gc.drawImage(sShadow2TopRight, x + width, y, null); + gc.drawImage(sShadow2Bottom, + x + sShadow2BottomLeft.getWidth(), y + height, + x + width, y + height + sShadow2Bottom.getHeight(), + 0, 0, sShadow2Bottom.getWidth(), sShadow2Bottom.getHeight(), + null); + gc.drawImage(sShadow2Right, + x + width, y + sShadow2TopRight.getHeight(), + x + width + sShadow2Right.getWidth(), y + height, + 0, 0, sShadow2Right.getWidth(), sShadow2Right.getHeight(), + null); + } + + /** + * Reads the given image from the plugin folder + * + * @param name the name of the image (including file extension) + * @return the corresponding image, or null if something goes wrong + */ @Nullable - private static BufferedImage readImage(@NonNull String name) { + public static BufferedImage readImage(@NonNull String name) { InputStream stream = ImageUtils.class.getResourceAsStream("/icons/" + name); //$NON-NLS-1$ if (stream != null) { try { @@ -589,12 +697,19 @@ public class ImageUtils { return null; } + // Normal drop shadow private static BufferedImage sShadowBottomLeft; private static BufferedImage sShadowBottom; private static BufferedImage sShadowBottomRight; private static BufferedImage sShadowRight; private static BufferedImage sShadowTopRight; + // Small drop shadow + private static BufferedImage sShadow2BottomLeft; + private static BufferedImage sShadow2Bottom; + private static BufferedImage sShadow2BottomRight; + private static BufferedImage sShadow2Right; + private static BufferedImage sShadow2TopRight; /** * Returns a bounding rectangle for the given list of rectangles. If the list is @@ -731,20 +846,102 @@ public class ImageUtils { if (imageType == BufferedImage.TYPE_CUSTOM) { imageType = BufferedImage.TYPE_INT_ARGB; } - BufferedImage scaled = new BufferedImage( - destWidth + rightMargin, destHeight + bottomMargin, imageType); - Graphics2D g2 = scaled.createGraphics(); - g2.setComposite(AlphaComposite.Src); - g2.setColor(new Color(0, true)); - g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, null); - g2.dispose(); + if (xScale > 0.5 && yScale > 0.5) { + BufferedImage scaled = + new BufferedImage(destWidth + rightMargin, destHeight + bottomMargin, imageType); + Graphics2D g2 = scaled.createGraphics(); + g2.setComposite(AlphaComposite.Src); + g2.setColor(new Color(0, true)); + g2.fillRect(0, 0, destWidth + rightMargin, destHeight + bottomMargin); + g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, + null); + g2.dispose(); + return scaled; + } else { + // When creating a thumbnail, using the above code doesn't work very well; + // you get some visible artifacts, especially for text. Instead use the + // technique of repeatedly scaling the image into half; this will cause + // proper averaging of neighboring pixels, and will typically (for the kinds + // of screen sizes used by this utility method in the layout editor) take + // about 3-4 iterations to get the result since we are logarithmically reducing + // the size. Besides, each successive pass in operating on much fewer pixels + // (a reduction of 4 in each pass). + // + // However, we may not be resizing to a size that can be reached exactly by + // successively diving in half. Therefore, once we're within a factor of 2 of + // the final size, we can do a resize to the exact target size. + // However, we can get even better results if we perform this final resize + // up front. Let's say we're going from width 1000 to a destination width of 85. + // The first approach would cause a resize from 1000 to 500 to 250 to 125, and + // then a resize from 125 to 85. That last resize can distort/blur a lot. + // Instead, we can start with the destination width, 85, and double it + // successfully until we're close to the initial size: 85, then 170, + // then 340, and finally 680. (The next one, 1360, is larger than 1000). + // So, now we *start* the thumbnail operation by resizing from width 1000 to + // width 680, which will preserve a lot of visual details such as text. + // Then we can successively resize the image in half, 680 to 340 to 170 to 85. + // We end up with the expected final size, but we've been doing an exact + // divide-in-half resizing operation at the end so there is less distortion. + + + int iterations = 0; // Number of halving operations to perform after the initial resize + int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer + int nearestHeight = destHeight; + while (nearestWidth < sourceWidth / 2) { + nearestWidth *= 2; + nearestHeight *= 2; + iterations++; + } - return scaled; + // If we're supposed to add in margins, we need to do it in the initial resizing + // operation if we don't have any subsequent resizing operations. + if (iterations == 0) { + nearestWidth += rightMargin; + nearestHeight += bottomMargin; + } + + BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType); + Graphics2D g2 = scaled.createGraphics(); + g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, nearestWidth, nearestHeight, + 0, 0, sourceWidth, sourceHeight, null); + g2.dispose(); + + sourceWidth = nearestWidth; + sourceHeight = nearestHeight; + source = scaled; + + for (int iteration = iterations - 1; iteration >= 0; iteration--) { + int halfWidth = sourceWidth / 2; + int halfHeight = sourceHeight / 2; + if (iteration == 0) { // Last iteration: Add margins in final image + scaled = new BufferedImage(halfWidth + rightMargin, halfHeight + bottomMargin, + imageType); + } else { + scaled = new BufferedImage(halfWidth, halfHeight, imageType); + } + g2 = scaled.createGraphics(); + g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + g2.drawImage(source, 0, 0, + halfWidth, halfHeight, 0, 0, + sourceWidth, sourceHeight, + null); + g2.dispose(); + + sourceWidth = halfWidth; + sourceHeight = halfHeight; + source = scaled; + iterations--; + } + return scaled; + } } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java index 0b8f784..7bab914 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/IncludeFinder.java @@ -20,6 +20,8 @@ import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.EXT_XML; import static com.android.SdkConstants.FD_RESOURCES; import static com.android.SdkConstants.FD_RES_LAYOUT; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_FRAGMENT; import static com.android.SdkConstants.VIEW_INCLUDE; import static com.android.ide.eclipse.adt.AdtConstants.WS_LAYOUTS; import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; @@ -29,6 +31,8 @@ import static org.eclipse.core.resources.IResourceDelta.CHANGED; import static org.eclipse.core.resources.IResourceDelta.CONTENT; import static org.eclipse.core.resources.IResourceDelta.REMOVED; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.ide.common.resources.ResourceFile; import com.android.ide.common.resources.ResourceFolder; @@ -56,6 +60,7 @@ import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; @@ -117,8 +122,9 @@ public class IncludeFinder { * Returns the {@link IncludeFinder} for the given project * * @param project the project the finder is associated with - * @return an {@IncludeFinder} for the given project, never null + * @return an {@link IncludeFinder} for the given project, never null */ + @NonNull public static IncludeFinder get(IProject project) { IncludeFinder finder = null; try { @@ -157,6 +163,7 @@ public class IncludeFinder { * @param included the file that is included * @return the files that are including the given file, or null or empty */ + @Nullable public List<Reference> getIncludedBy(IResource included) { ensureInitialized(); String mapKey = getMapKey(included); @@ -503,8 +510,10 @@ public class IncludeFinder { * empty if the file does not include any include tags; it does this by only parsing * if it detects the string <include in the file. */ - private List<String> findIncludes(String xml) { - int index = xml.indexOf("<include"); //$NON-NLS-1$ + @VisibleForTesting + @NonNull + static List<String> findIncludes(@NonNull String xml) { + int index = xml.indexOf(ATTR_LAYOUT); if (index != -1) { return findIncludesInXml(xml); } @@ -518,7 +527,9 @@ public class IncludeFinder { * @param xml layout XML content to be parsed for includes * @return a list of included urls, or null */ - private List<String> findIncludesInXml(String xml) { + @VisibleForTesting + @NonNull + static List<String> findIncludesInXml(@NonNull String xml) { Document document = DomUtilities.parseDocument(xml, false /*logParserErrors*/); if (document != null) { return findIncludesInDocument(document); @@ -528,27 +539,52 @@ public class IncludeFinder { } /** Searches the given DOM document and returns the list of includes, if any */ - private List<String> findIncludesInDocument(Document document) { - NodeList includes = document.getElementsByTagName(VIEW_INCLUDE); - if (includes.getLength() > 0) { - List<String> urls = new ArrayList<String>(); - for (int i = 0; i < includes.getLength(); i++) { - Element element = (Element) includes.item(i); - String url = element.getAttribute(ATTR_LAYOUT); + @NonNull + private static List<String> findIncludesInDocument(@NonNull Document document) { + List<String> includes = findIncludesInDocument(document, null); + if (includes == null) { + includes = Collections.emptyList(); + } + return includes; + } + + @Nullable + private static List<String> findIncludesInDocument(@NonNull Node node, + @Nullable List<String> urls) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + String tag = node.getNodeName(); + boolean isInclude = tag.equals(VIEW_INCLUDE); + boolean isFragment = tag.equals(VIEW_FRAGMENT); + if (isInclude || isFragment) { + Element element = (Element) node; + String url; + if (isInclude) { + url = element.getAttribute(ATTR_LAYOUT); + } else { + url = element.getAttributeNS(TOOLS_URI, ATTR_LAYOUT); + } if (url.length() > 0) { String resourceName = urlToLocalResource(url); if (resourceName != null) { + if (urls == null) { + urls = new ArrayList<String>(); + } urls.add(resourceName); } } + } + } - return urls; + NodeList children = node.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + urls = findIncludesInDocument(children.item(i), urls); } - return Collections.emptyList(); + return urls; } + /** * Returns the layout URL to a local resource name (provided the URL is a local * resource, not something in @android etc.) Returns null otherwise. @@ -628,6 +664,7 @@ public class IncludeFinder { ResourceManager.getInstance().addListener(sListener); } + /** Stop listening on project resources */ public static void stop() { assert sListener != null; ResourceManager.getInstance().addListener(sListener); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java index 4368db4..1b1bd23 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java @@ -33,6 +33,8 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Screen; import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; import com.google.common.base.Strings; @@ -691,7 +693,7 @@ public class LayoutActionBar extends Composite { * Reset the canvas scale to best fit (so content is as large as possible without scrollbars) */ void rescaleToFit(boolean onlyZoomOut) { - mEditor.getCanvasControl().setFitScale(onlyZoomOut); + mEditor.getCanvasControl().setFitScale(onlyZoomOut, true /*allowZoomIn*/); } boolean rescaleToReal(boolean real) { @@ -708,7 +710,9 @@ public class LayoutActionBar extends Composite { // compute average dpi of X and Y ConfigurationChooser chooser = mEditor.getConfigurationChooser(); Configuration config = chooser.getConfiguration(); - float dpi = (config.getXDpi() + config.getYDpi()) / 2.f; + Device device = config.getDevice(); + Screen screen = device.getDefaultHardware().getScreen(); + double dpi = (screen.getXdpi() + screen.getYdpi()) / 2.; // get the monitor dpi float monitor = AdtPrefs.getPrefs().getMonitorDensity(); 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 86878ac..814b82c 100644 --- 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 @@ -18,6 +18,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.SdkConstants; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.IDragElement.IDragAttribute; import com.android.ide.common.api.INode; import com.android.ide.common.api.Margins; import com.android.ide.common.api.Point; @@ -27,6 +29,7 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; @@ -81,6 +84,7 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorSite; @@ -119,7 +123,7 @@ public class LayoutCanvas extends Canvas { private static final boolean DEBUG = false; - /* package */ static final String PREFIX_CANVAS_ACTION = "canvas_action_"; + static final String PREFIX_CANVAS_ACTION = "canvas_action_"; //$NON-NLS-1$ /** The layout editor that uses this layout canvas. */ private final LayoutEditorDelegate mEditorDelegate; @@ -147,13 +151,13 @@ public class LayoutCanvas extends Canvas { private DropTarget mDropTarget; /** Factory that can create {@link INode} proxies. */ - private final NodeFactory mNodeFactory = new NodeFactory(this); + private final @NonNull NodeFactory mNodeFactory = new NodeFactory(this); /** Vertical scaling & scrollbar information. */ - private CanvasTransform mVScale; + private final CanvasTransform mVScale; /** Horizontal scaling & scrollbar information. */ - private CanvasTransform mHScale; + private final CanvasTransform mHScale; /** Drag source associated with this canvas. */ private DragSource mDragSource; @@ -218,6 +222,9 @@ public class LayoutCanvas extends Canvas { /** The overlay which paints masks hiding everything but included content. */ private IncludeOverlay mIncludeOverlay; + /** Configuration previews shown next to the layout */ + private final RenderPreviewManager mPreviewManager; + /** * Gesture Manager responsible for identifying mouse, keyboard and drag and * drop events. @@ -239,6 +246,14 @@ public class LayoutCanvas extends Canvas { private Color mBackgroundColor; + /** + * Creates a new {@link LayoutCanvas} widget + * + * @param editorDelegate the associated editor delegate + * @param rulesEngine the rules engine + * @param parent parent SWT widget + * @param style the SWT style + */ public LayoutCanvas(LayoutEditorDelegate editorDelegate, RulesEngine rulesEngine, Composite parent, @@ -253,6 +268,7 @@ public class LayoutCanvas extends Canvas { mClipboardSupport = new ClipboardSupport(this, parent); mHScale = new CanvasTransform(this, getHorizontalBar()); mVScale = new CanvasTransform(this, getVerticalBar()); + mPreviewManager = new RenderPreviewManager(this); // Unit test suite passes a null here; TODO: Replace with mocking IFile file = editorDelegate != null ? editorDelegate.getEditor().getInputFile() : null; @@ -314,9 +330,7 @@ public class LayoutCanvas extends Canvas { } } - Rectangle clientArea = getClientArea(); - mHScale.setClientSize(clientArea.width); - mVScale.setClientSize(clientArea.height); + updateScrollBars(); // Update the zoom level in the canvas when you toggle the zoom if (coordinator != null) { @@ -355,6 +369,51 @@ public class LayoutCanvas extends Canvas { mLintTooltipManager.register(); } + void updateScrollBars() { + Rectangle clientArea = getClientArea(); + Image image = mImageOverlay.getImage(); + if (image != null) { + ImageData imageData = image.getImageData(); + int clientWidth = clientArea.width; + int clientHeight = clientArea.height; + + int imageWidth = imageData.width; + int imageHeight = imageData.height; + + int fullWidth = imageWidth; + int fullHeight = imageHeight; + + if (mPreviewManager.hasPreviews()) { + fullHeight = Math.max(fullHeight, + (int) (mPreviewManager.getHeight() / mHScale.getScale())); + } + + if (clientWidth == 0) { + clientWidth = imageWidth; + Shell shell = getShell(); + if (shell != null) { + org.eclipse.swt.graphics.Point size = shell.getSize(); + if (size.x > 0) { + clientWidth = size.x * 70 / 100; + } + } + } + if (clientHeight == 0) { + clientHeight = imageHeight; + Shell shell = getShell(); + if (shell != null) { + org.eclipse.swt.graphics.Point size = shell.getSize(); + if (size.y > 0) { + clientWidth = size.y * 80 / 100; + } + } + } + + mHScale.setSize(imageWidth, fullWidth, clientWidth); + mVScale.setSize(imageHeight, fullHeight, clientHeight); + } + } + private Runnable mZoomCheck = new Runnable() { private Boolean mWasZoomed; @@ -372,10 +431,9 @@ public class LayoutCanvas extends Canvas { Boolean zoomed = coordinator.isEditorMaximized(); if (mWasZoomed != zoomed) { if (mWasZoomed != null) { - LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor() - .getLayoutActionBar(); + LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); if (actionBar.isZoomingAllowed()) { - setFitScale(true /*onlyZoomOut*/); + setFitScale(true /*onlyZoomOut*/, true /*allowZoomIn*/); } } mWasZoomed = zoomed; @@ -415,18 +473,26 @@ public class LayoutCanvas extends Canvas { } else { // Zooming actions char c = e.character; - LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor().getLayoutActionBar(); + LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); if (c == '1' && actionBar.isZoomingAllowed()) { setScale(1, true); } else if (c == '0' && actionBar.isZoomingAllowed()) { - setFitScale(true); + setFitScale(true, true /*allowZoomIn*/); } else if (e.keyCode == '0' && (e.stateMask & SWT.MOD2) != 0 && actionBar.isZoomingAllowed()) { - setFitScale(false); - } else if (c == '+' && actionBar.isZoomingAllowed()) { - actionBar.rescale(1); + setFitScale(false, true /*allowZoomIn*/); + } else if ((c == '+' || c == '=') && actionBar.isZoomingAllowed()) { + if ((e.stateMask & SWT.MOD1) != 0) { + mPreviewManager.zoomIn(); + } else { + actionBar.rescale(1); + } } else if (c == '-' && actionBar.isZoomingAllowed()) { - actionBar.rescale(-1); + if ((e.stateMask & SWT.MOD1) != 0) { + mPreviewManager.zoomOut(); + } else { + actionBar.rescale(-1); + } } } } @@ -507,24 +573,38 @@ public class LayoutCanvas extends Canvas { mBackgroundColor = null; } + mPreviewManager.disposePreviews(); mViewHierarchy.dispose(); } + /** + * Returns the configuration preview manager for this canvas + * + * @return the configuration preview manager for this canvas + */ + @NonNull + public RenderPreviewManager getPreviewManager() { + return mPreviewManager; + } + /** Returns the Rules Engine, associated with the current project. */ - /* package */ RulesEngine getRulesEngine() { + RulesEngine getRulesEngine() { return mRulesEngine; } /** Sets the Rules Engine, associated with the current project. */ - /* package */ void setRulesEngine(RulesEngine rulesEngine) { + void setRulesEngine(RulesEngine rulesEngine) { mRulesEngine = rulesEngine; } /** * Returns the factory to use to convert from {@link CanvasViewInfo} or from * {@link UiViewElementNode} to {@link INode} proxies. + * + * @return the node factory */ - /* package */ NodeFactory getNodeFactory() { + @NonNull + public NodeFactory getNodeFactory() { return mNodeFactory; } @@ -533,12 +613,14 @@ public class LayoutCanvas extends Canvas { * * @return The GCWrapper used to paint view rules */ - /* package */ GCWrapper getGcWrapper() { + GCWrapper getGcWrapper() { return mGCWrapper; } /** * Returns the {@link LayoutEditorDelegate} associated with this canvas. + * + * @return the delegate */ public LayoutEditorDelegate getEditorDelegate() { return mEditorDelegate; @@ -589,7 +671,7 @@ public class LayoutCanvas extends Canvas { * @return A {@link CanvasTransform} for mapping between layout and control * coordinates in the horizontal dimension. */ - /* package */ CanvasTransform getHorizontalTransform() { + CanvasTransform getHorizontalTransform() { return mHScale; } @@ -600,7 +682,7 @@ public class LayoutCanvas extends Canvas { * @return A {@link CanvasTransform} for mapping between layout and control * coordinates in the vertical dimension. */ - /* package */ CanvasTransform getVerticalTransform() { + CanvasTransform getVerticalTransform() { return mVScale; } @@ -648,6 +730,11 @@ public class LayoutCanvas extends Canvas { return mSelectAllAction; } + /** Returns the associated {@link GraphicalEditorPart} */ + GraphicalEditorPart getGraphicalEditor() { + return mEditorDelegate.getGraphicalEditor(); + } + /** * 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. @@ -663,22 +750,21 @@ public class LayoutCanvas extends Canvas { * {@link #showInvisibleViews(boolean)}) where individual invisible nodes * are padded during certain interactions. */ - /* package */ void setSession(RenderSession session, Set<UiElementNode> explodedNodes, + void setSession(RenderSession session, Set<UiElementNode> explodedNodes, boolean layoutlib5) { // disable any hover clearHover(); mViewHierarchy.setSession(session, explodedNodes, layoutlib5); if (mViewHierarchy.isValid() && session != null) { - Image image = mImageOverlay.setImage(session.getImage(), session.isAlphaChannelImage()); + Image image = mImageOverlay.setImage(session.getImage(), + session.isAlphaChannelImage()); mOutlinePage.setModel(mViewHierarchy.getRoot()); - mEditorDelegate.getGraphicalEditor().setModel(mViewHierarchy.getRoot()); + getGraphicalEditor().setModel(mViewHierarchy.getRoot()); if (image != null) { - Rectangle clientArea = getClientArea(); - mHScale.setSize(image.getImageData().width, clientArea.width); - mVScale.setSize(image.getImageData().height, clientArea.height); + updateScrollBars(); if (mZoomFitNextImage) { // Must be run asynchronously because getClientArea() returns 0 bounds // when the editor is being initialized @@ -691,6 +777,9 @@ public class LayoutCanvas extends Canvas { } }); } + + // Ensure that if we have a a preview mode enabled, it's shown + syncPreviewMode(); } } @@ -700,10 +789,9 @@ public class LayoutCanvas extends Canvas { void ensureZoomed() { if (mZoomFitNextImage && getClientArea().height > 0) { mZoomFitNextImage = false; - LayoutActionBar actionBar = mEditorDelegate.getGraphicalEditor() - .getLayoutActionBar(); + LayoutActionBar actionBar = getGraphicalEditor().getLayoutActionBar(); if (actionBar.isZoomingAllowed()) { - setFitScale(true); + setFitScale(true, true /*allowZoomIn*/); } } } @@ -713,11 +801,18 @@ public class LayoutCanvas extends Canvas { redraw(); } + /** + * Returns the zoom scale factor of the canvas (the amount the full + * resolution render of the device is zoomed before being shown on the + * canvas) + * + * @return the image scale + */ public double getScale() { return mHScale.getScale(); } - /* package */ void setScale(double scale, boolean redraw) { + void setScale(double scale, boolean redraw) { if (scale <= 0.0) { scale = 1.0; } @@ -746,8 +841,13 @@ public class LayoutCanvas extends Canvas { * @param onlyZoomOut if true, then the zooming factor will never be larger than 1, * which means that this function will zoom out if necessary to show the * rendered image, but it will never zoom in. + * TODO: Rename this, it sounds like it conflicts with allowZoomIn, + * even though one is referring to the zoom level and one is referring + * to the overall act of scaling above/below 1. + * @param allowZoomIn if false, then if the computed zoom factor is smaller than + * the current zoom factor, it will be ignored. */ - void setFitScale(boolean onlyZoomOut) { + public void setFitScale(boolean onlyZoomOut, boolean allowZoomIn) { ImageOverlay imageOverlay = getImageOverlay(); if (imageOverlay == null) { return; @@ -758,6 +858,14 @@ public class LayoutCanvas extends Canvas { int canvasWidth = canvasSize.width; int canvasHeight = canvasSize.height; + boolean hasPreviews = mPreviewManager.hasPreviews(); + if (hasPreviews) { + canvasWidth = 2 * canvasWidth / 3; + } else { + canvasWidth -= 4; + canvasHeight -= 4; + } + ImageData imageData = image.getImageData(); int sceneWidth = imageData.width; int sceneHeight = imageData.height; @@ -796,6 +904,10 @@ public class LayoutCanvas extends Canvas { scale = Math.min(1.0, scale); } + if (!allowZoomIn && scale > getScale()) { + return; + } + setScale(scale, true); } } @@ -808,7 +920,7 @@ public class LayoutCanvas extends Canvas { * @param canvasY Y in the canvas coordinates * @return A new {@link Point} in control client coordinates (not display coordinates) */ - /* package */ Point layoutToControlPoint(int canvasX, int canvasY) { + Point layoutToControlPoint(int canvasX, int canvasY) { int x = mHScale.translate(canvasX); int y = mVScale.translate(canvasY); return new Point(x, y); @@ -823,7 +935,7 @@ public class LayoutCanvas extends Canvas { * <p/> * Returns null if there's no action for the given id. */ - /* package */ IAction getAction(String actionId) { + IAction getAction(String actionId) { String prefix = PREFIX_CANVAS_ACTION; if (mMenuManager == null || actionId == null || @@ -857,6 +969,8 @@ public class LayoutCanvas extends Canvas { mImageOverlay.paint(gc); } + mPreviewManager.paint(gc); + if (mShowOutline) { if (mOutlineOverlay == null) { mOutlineOverlay = new OutlineOverlay(mViewHierarchy, mHScale, mVScale); @@ -907,7 +1021,7 @@ public class LayoutCanvas extends Canvas { * @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) { + void showInvisibleViews(boolean show) { if (mShowInvisible == show) { return; } @@ -968,14 +1082,14 @@ public class LayoutCanvas extends Canvas { /** * Clears the hover. */ - /* package */ void clearHover() { + void clearHover() { mHoverOverlay.clearHover(); } /** * Hover on top of a known child. */ - /* package */ void hover(MouseEvent e) { + void hover(MouseEvent e) { // Check if a button is pressed; no hovers during drags if ((e.stateMask & SWT.BUTTON_MASK) != 0) { clearHover(); @@ -1027,7 +1141,7 @@ public class LayoutCanvas extends Canvas { * @param url The layout attribute url of the form @layout/foo */ private void showInclude(String url) { - GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor(); + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); IPath filePath = graphicalEditor.findResourceFile(url); if (filePath == null) { // Should not be possible - if the URL had been bad, then we wouldn't @@ -1074,8 +1188,7 @@ public class LayoutCanvas extends Canvas { try { // Set initial state of a new file // TODO: Only set rendering target portion of the state - QualifiedName qname = ConfigurationChooser.NAME_CONFIG_STATE; - String state = AdtPlugin.getFileProperty(leavingFile, qname); + String state = ConfigurationDescription.getDescription(leavingFile); xmlFile.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state); } catch (CoreException e) { @@ -1127,7 +1240,7 @@ public class LayoutCanvas extends Canvas { * @return the layout resource name of this layout */ public String getLayoutResourceName() { - GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor(); + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); return graphicalEditor.getLayoutResourceName(); } @@ -1138,7 +1251,7 @@ public class LayoutCanvas extends Canvas { */ /* public String getMe() { - GraphicalEditorPart graphicalEditor = mEditorDelegate.getGraphicalEditor(); + GraphicalEditorPart graphicalEditor = getGraphicalEditor(); IFile editedFile = graphicalEditor.getEditedFile(); return editedFile.getProjectRelativePath().toOSString(); } @@ -1265,11 +1378,11 @@ public class LayoutCanvas extends Canvas { copyActionAttributes(mSelectAllAction, ActionFactory.SELECT_ALL); } - /* package */ String getCutLabel() { + String getCutLabel() { return mCutAction.getText(); } - /* package */ String getDeleteLabel() { + String getDeleteLabel() { // verb "Delete" from the DELETE action's title return mDeleteAction.getText(); } @@ -1284,7 +1397,7 @@ public class LayoutCanvas extends Canvas { hasSelection = false; } - StyledText errorLabel = mEditorDelegate.getGraphicalEditor().getErrorLabel(); + StyledText errorLabel = getGraphicalEditor().getErrorLabel(); mCutAction.setEnabled(hasSelection); mCopyAction.setEnabled(hasSelection || errorLabel.getSelectionCount() > 0); mDeleteAction.setEnabled(hasSelection); @@ -1427,7 +1540,7 @@ public class LayoutCanvas extends Canvas { private void setupStaticMenuActions(IMenuManager manager) { manager.removeAll(); - manager.add(new SelectionManager.SelectionMenu(mEditorDelegate.getGraphicalEditor())); + manager.add(new SelectionManager.SelectionMenu(getGraphicalEditor())); manager.add(new Separator()); manager.add(mCutAction); manager.add(mCopyAction); @@ -1455,7 +1568,7 @@ public class LayoutCanvas extends Canvas { /** * Deletes the selection. Equivalent to pressing the Delete key. */ - /* package */ void delete() { + void delete() { mDeleteAction.run(); } @@ -1470,16 +1583,16 @@ public class LayoutCanvas extends Canvas { * This is invoked by * {@link MoveGesture#drop(org.eclipse.swt.dnd.DropTargetEvent)}. * - * @param rootFqcn A non-null non-empty FQCN that must match an existing - * {@link ViewElementDescriptor} to add as root to the current - * empty XML document. + * @param root A non-null descriptor of the root element to create. */ - /* package */ void createDocumentRoot(String rootFqcn) { + void createDocumentRoot(final @NonNull SimpleElement root) { + String rootFqcn = root.getFqcn(); // Need a valid empty document to create the new root final UiDocumentNode uiDoc = mEditorDelegate.getUiRootNode(); if (uiDoc == null || uiDoc.getUiChildren().size() > 0) { - debugPrintf("Failed to create document root for %1$s: document is not empty", rootFqcn); + debugPrintf("Failed to create document root for %1$s: document is not empty", + rootFqcn); return; } @@ -1511,6 +1624,16 @@ public class LayoutCanvas extends Canvas { SdkConstants.NS_RESOURCES, true /*override*/); + IDragAttribute[] attributes = root.getAttributes(); + if (attributes != null) { + for (IDragAttribute attribute : attributes) { + String uri = attribute.getUri(); + String name = attribute.getName(); + String value = attribute.getValue(); + uiNew.setAttributeValue(name, uri, value, false /*override*/); + } + } + // Adjust the attributes DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); @@ -1528,8 +1651,7 @@ public class LayoutCanvas extends Canvas { */ public Margins getInsets(String fqcn) { if (ViewMetadataRepository.INSETS_SUPPORTED) { - ConfigurationChooser configComposite = - mEditorDelegate.getGraphicalEditor().getConfigurationChooser(); + ConfigurationChooser configComposite = getGraphicalEditor().getConfigurationChooser(); String theme = configComposite.getThemeName(); Density density = configComposite.getConfiguration().getDensity(); return ViewMetadataRepository.getInsets(fqcn, density, theme); @@ -1552,4 +1674,47 @@ public class LayoutCanvas extends Canvas { mLintTooltipManager.hide(); } } + + /** @see #setPreview(RenderPreview) */ + private RenderPreview mPreview; + + /** + * Sets the {@link RenderPreview} associated with the currently rendering + * configuration. + * <p> + * A {@link RenderPreview} has various additional state beyond its rendering, + * such as its display name (which can be edited by the user). When you click on + * previews, the layout editor switches to show the given configuration preview. + * The preview is then no longer shown in the list of previews and is instead rendered + * in the main editor. However, when you then switch away to some other preview, we + * want to be able to restore the preview with all its state. + * + * @param preview the preview associated with the current canvas + */ + public void setPreview(@Nullable RenderPreview preview) { + mPreview = preview; + } + + /** + * Returns the {@link RenderPreview} associated with this layout canvas. + * + * @see #setPreview(RenderPreview) + * @return the {@link RenderPreview} + */ + @Nullable + public RenderPreview getPreview() { + return mPreview; + } + + /** Ensures that the configuration previews are up to date for this canvas */ + public void syncPreviewMode() { + if (mImageOverlay != null && mImageOverlay.getImage() != null && + getGraphicalEditor().getConfigurationChooser().getResources() != null) { + if (mPreviewManager.recomputePreviews(false)) { + // Zoom when syncing modes + mZoomFitNextImage = true; + ensureZoomed(); + } + } + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java index a164e3d..b79e3b0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutMetadata.java @@ -16,44 +16,46 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.SdkConstants.ANDROID_LAYOUT_RESOURCE_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_NUM_COLUMNS; import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; import static com.android.SdkConstants.GRID_VIEW; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VALUE_AUTO_FIT; - -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.AdapterBinding; import com.android.ide.common.rendering.api.DataBindingItem; import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; - -import org.eclipse.jface.text.IDocument; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.progress.WorkbenchJob; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xmlpull.v1.XmlPullParser; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.List; +import java.util.Map; /** * Design-time metadata lookup for layouts, such as fragment and AdapterView bindings. */ -@SuppressWarnings("restriction") // XML DOM model public class LayoutMetadata { /** The default layout to use for list items in expandable list views */ public static final String DEFAULT_EXPANDABLE_LIST_ITEM = "simple_expandable_list_item_2"; //$NON-NLS-1$ @@ -64,8 +66,6 @@ public class LayoutMetadata { /** The string to start metadata comments with */ private static final String COMMENT_PROLOGUE = " Preview: "; - /** The string to end metadata comments with */ - private static final String COMMENT_EPILOGUE = " "; /** The property key, included in comments, which references a list item layout */ public static final String KEY_LV_ITEM = "listitem"; //$NON-NLS-1$ /** The property key, included in comments, which references a list header layout */ @@ -74,49 +74,14 @@ public class LayoutMetadata { public static final String KEY_LV_FOOTER = "listfooter"; //$NON-NLS-1$ /** The property key, included in comments, which references a fragment layout to show */ public static final String KEY_FRAGMENT_LAYOUT = "layout"; //$NON-NLS-1$ + // NOTE: If you add additional keys related to resources, make sure you update the + // ResourceRenameParticipant /** Utility class, do not create instances */ private LayoutMetadata() { } /** - * Returns the given property of the given DOM node, or null - * - * @param document the document to look up and read lock the model for - * @param node the XML node to associate metadata with - * @param name the name of the property to look up - * @return the value stored with the given node and name, or null - * @deprecated this method gets metadata using the old comment-based style; should - * only be used for migration at this point - */ - @Deprecated - @Nullable - public static String getProperty( - @Nullable IDocument document, - @NonNull Node node, - @NonNull String name) { - IStructuredModel model = null; - try { - if (document != null) { - IModelManager modelManager = StructuredModelManager.getModelManager(); - model = modelManager.getExistingModelForRead(document); - } - - Node comment = findComment(node); - if (comment != null) { - String text = comment.getNodeValue(); - return getProperty(name, text); - } - - return null; - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - } - - /** * Returns the given property specified in the <b>current</b> element being * processed by the given pull parser. * @@ -136,124 +101,25 @@ public class LayoutMetadata { } /** - * Returns the given property specified in the given XML comment + * Clears the old metadata from the given node * - * @param name the name of the property to look up - * @param text the comment text for an XML node - * @return the value stored with the given node and name, or null - */ - public static String getProperty(String name, String text) { - assert text.startsWith(COMMENT_PROLOGUE); - String valuesString = text.substring(COMMENT_PROLOGUE.length()); - String[] values = valuesString.split(","); //$NON-NLS-1$ - if (values.length == 1) { - valuesString = values[0].trim(); - if (valuesString.indexOf('\n') != -1) { - values = valuesString.split("\n"); //$NON-NLS-1$ - } - } - String target = name + '='; - for (int j = 0; j < values.length; j++) { - String value = values[j].trim(); - if (value.startsWith(target)) { - return value.substring(target.length()).trim(); - } - } - return null; - } - - /** - * Sets the given property of the given DOM node to a given value, or if null clears - * the property. - * - * @param document the document to look up and write lock the model for * @param node the XML node to associate metadata with - * @param name the name of the property to set - * @param value the value to store for the given node and name, or null to remove it - * @deprecated this method sets metadata using the old comment-based style; should - * only be used for migration at this point + * @deprecated this method clears metadata using the old comment-based style; + * should only be used for migration at this point */ @Deprecated - public static void setProperty(IDocument document, Node node, String name, String value) { - // Reserved characters: [,-=] - assert name.indexOf('-') == -1; - assert value == null || value.indexOf('-') == -1; - assert name.indexOf(',') == -1; - assert value == null || value.indexOf(',') == -1; - assert name.indexOf('=') == -1; - assert value == null || value.indexOf('=') == -1; - - IStructuredModel model = null; - try { - IModelManager modelManager = StructuredModelManager.getModelManager(); - model = modelManager.getExistingModelForEdit(document); - if (model instanceof IDOMModel) { - IDOMModel domModel = (IDOMModel) model; - Document domDocument = domModel.getDocument(); - assert node.getOwnerDocument() == domDocument; - } - - Document doc = node.getOwnerDocument(); - Node commentNode = findComment(node); - - String commentText = null; - if (commentNode != null) { - String text = commentNode.getNodeValue(); - assert text.startsWith(COMMENT_PROLOGUE); - String valuesString = text.substring(COMMENT_PROLOGUE.length()); - String[] values = valuesString.split(","); //$NON-NLS-1$ - if (values.length == 1) { - valuesString = values[0].trim(); - if (valuesString.indexOf('\n') != -1) { - values = valuesString.split("\n"); //$NON-NLS-1$ - } - } - String target = name + '='; - List<String> preserve = new ArrayList<String>(); - for (int j = 0; j < values.length; j++) { - String v = values[j].trim(); - if (v.length() == 0) { - continue; - } - if (!v.startsWith(target)) { - preserve.add(v.trim()); - } - } - if (value != null) { - preserve.add(name + '=' + value.trim()); - } - if (preserve.size() > 0) { - if (preserve.size() > 1) { - Collections.sort(preserve); - String firstLineIndent = AndroidXmlEditor.getIndent(document, commentNode); - String oneIndentLevel = " "; //$NON-NLS-1$ - StringBuilder sb = new StringBuilder(); - sb.append(COMMENT_PROLOGUE); - sb.append('\n'); - for (String s : preserve) { - sb.append(firstLineIndent); - sb.append(oneIndentLevel); - sb.append(s); - sb.append('\n'); - } - sb.append(firstLineIndent); - sb.append(COMMENT_EPILOGUE); - commentText = sb.toString(); - } else { - commentText = COMMENT_PROLOGUE + preserve.get(0) + COMMENT_EPILOGUE; - } - } - } else if (value != null) { - commentText = COMMENT_PROLOGUE + name + '=' + value + COMMENT_EPILOGUE; - } - - if (commentText == null) { - if (commentNode != null) { + public static void clearLegacyComment(Node node) { + NodeList children = node.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.COMMENT_NODE) { + String text = child.getNodeValue(); + if (text.startsWith(COMMENT_PROLOGUE)) { + Node commentNode = child; // Remove the comment, along with surrounding whitespace if applicable Node previous = commentNode.getPreviousSibling(); if (previous != null && previous.getNodeType() == Node.TEXT_NODE) { - String text = previous.getNodeValue(); - if (text.trim().length() == 0) { + if (previous.getNodeValue().trim().length() == 0) { node.removeChild(previous); } } @@ -261,53 +127,13 @@ public class LayoutMetadata { Node first = node.getFirstChild(); if (first != null && first.getNextSibling() == null && first.getNodeType() == Node.TEXT_NODE) { - String text = first.getNodeValue(); - if (text.trim().length() == 0) { + if (first.getNodeValue().trim().length() == 0) { node.removeChild(first); } } } - return; - } - - if (commentNode != null) { - commentNode.setNodeValue(commentText); - } else { - commentNode = doc.createComment(commentText); - String firstLineIndent = AndroidXmlEditor.getIndent(document, node); - Node firstChild = node.getFirstChild(); - boolean indentAfter = firstChild == null - || firstChild.getNodeType() != Node.TEXT_NODE - || firstChild.getNodeValue().indexOf('\n') == -1; - String oneIndentLevel = " "; //$NON-NLS-1$ - node.insertBefore(doc.createTextNode('\n' + firstLineIndent + oneIndentLevel), - firstChild); - node.insertBefore(commentNode, firstChild); - if (indentAfter) { - node.insertBefore(doc.createTextNode('\n' + firstLineIndent), firstChild); - } - } - } finally { - if (model != null) { - model.releaseFromEdit(); - } - } - } - - /** Finds the comment node associated with the given node, or null if not found */ - private static Node findComment(Node node) { - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - if (child.getNodeType() == Node.COMMENT_NODE) { - String text = child.getNodeValue(); - if (text.startsWith(COMMENT_PROLOGUE)) { - return child; - } } } - - return null; } /** @@ -344,19 +170,115 @@ public class LayoutMetadata { * @param value the value to store for the given node and name, or null to remove it */ public static void setProperty( - @NonNull AndroidXmlEditor editor, + @NonNull final AndroidXmlEditor editor, @NonNull final Node node, @NonNull final String name, @Nullable final String value) { // Clear out the old metadata - IDocument document = editor.getStructuredSourceViewer().getDocument(); - setProperty(document, node, name, null); + clearLegacyComment(node); if (node.getNodeType() == Node.ELEMENT_NODE) { - Element element = (Element) node; - AdtUtils.setToolsAttribute(editor, element, "Bind View", name, value, + final Element element = (Element) node; + final String undoLabel = "Bind View"; + AdtUtils.setToolsAttribute(editor, element, undoLabel, name, value, false /*reveal*/, false /*append*/); + + // Also apply the same layout to any corresponding elements in other configurations + // of this layout. + final IFile file = editor.getInputFile(); + if (file != null) { + final List<IFile> variations = AdtUtils.getResourceVariations(file, false); + if (variations.isEmpty()) { + return; + } + Display display = AdtPlugin.getDisplay(); + WorkbenchJob job = new WorkbenchJob(display, "Update alternate views") { + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + for (IFile variation : variations) { + if (variation.equals(file)) { + continue; + } + try { + // If the corresponding file is open in the IDE, use the + // editor version instead + if (!AdtPrefs.getPrefs().isSharedLayoutEditor()) { + if (setPropertyInEditor(undoLabel, variation, element, name, + value)) { + return Status.OK_STATUS; + } + } + + boolean old = editor.getIgnoreXmlUpdate(); + try { + editor.setIgnoreXmlUpdate(true); + setPropertyInFile(undoLabel, variation, element, name, value); + } finally { + editor.setIgnoreXmlUpdate(old); + } + } catch (Exception e) { + AdtPlugin.log(e, variation.getFullPath().toOSString()); + } + } + return Status.OK_STATUS; + } + + }; + job.setSystem(true); + job.schedule(); + } + } + } + + private static boolean setPropertyInEditor( + @NonNull String undoLabel, + @NonNull IFile variation, + @NonNull final Element equivalentElement, + @NonNull final String name, + @Nullable final String value) { + Collection<IEditorPart> editors = + AdtUtils.findEditorsFor(variation, false /*restore*/); + for (IEditorPart part : editors) { + AndroidXmlEditor editor = AdtUtils.getXmlEditor(part); + if (editor != null) { + Document doc = DomUtilities.getDocument(editor); + if (doc != null) { + Element element = DomUtilities.findCorresponding(equivalentElement, doc); + if (element != null) { + AdtUtils.setToolsAttribute(editor, element, undoLabel, name, + value, false /*reveal*/, false /*append*/); + if (part instanceof GraphicalEditorPart) { + GraphicalEditorPart g = (GraphicalEditorPart) part; + g.recomputeLayout(); + g.getCanvasControl().redraw(); + } + return true; + } + } + } } + + return false; + } + + private static boolean setPropertyInFile( + @NonNull String undoLabel, + @NonNull IFile variation, + @NonNull final Element element, + @NonNull final String name, + @Nullable final String value) { + Document doc = DomUtilities.getDocument(variation); + if (doc != null && element.getOwnerDocument() != doc) { + Element other = DomUtilities.findCorresponding(element, doc); + if (other != null) { + AdtUtils.setToolsAttribute(variation, other, undoLabel, + name, value, false); + + return true; + } + } + + return false; } /** Strips out @layout/ or @android:layout/ from the given layout reference */ @@ -375,10 +297,36 @@ public class LayoutMetadata { * has not yet chosen a target layout to use for the given AdapterView. * * @param viewObject the view object to create an adapter binding for + * @param map a map containing tools attribute metadata + * @return a binding, or null + */ + @Nullable + public static AdapterBinding getNodeBinding( + @Nullable Object viewObject, + @NonNull Map<String, String> map) { + String header = map.get(KEY_LV_HEADER); + String footer = map.get(KEY_LV_FOOTER); + String layout = map.get(KEY_LV_ITEM); + if (layout != null || header != null || footer != null) { + int count = 12; + return getNodeBinding(viewObject, header, footer, layout, count); + } + + return null; + } + + /** + * Creates an {@link AdapterBinding} for the given view object, or null if the user + * has not yet chosen a target layout to use for the given AdapterView. + * + * @param viewObject the view object to create an adapter binding for * @param uiNode the ui node corresponding to the view object * @return a binding, or null */ - public static AdapterBinding getNodeBinding(Object viewObject, UiViewElementNode uiNode) { + @Nullable + public static AdapterBinding getNodeBinding( + @Nullable Object viewObject, + @NonNull UiViewElementNode uiNode) { Node xmlNode = uiNode.getXmlNode(); String header = getProperty(xmlNode, KEY_LV_HEADER); @@ -392,14 +340,30 @@ public class LayoutMetadata { Element element = (Element) xmlNode; String columns = element.getAttributeNS(ANDROID_URI, ATTR_NUM_COLUMNS); int multiplier = 2; - if (columns != null && columns.length() > 0) { - int c = Integer.parseInt(columns); - if (c >= 1 && c <= 10) { - multiplier = c; + if (columns != null && columns.length() > 0 && + !columns.equals(VALUE_AUTO_FIT)) { + try { + int c = Integer.parseInt(columns); + if (c >= 1 && c <= 10) { + multiplier = c; + } + } catch (NumberFormatException nufe) { + // some unexpected numColumns value: just stick with 2 columns for + // preview purposes } } count *= multiplier; } + + return getNodeBinding(viewObject, header, footer, layout, count); + } + + return null; + } + + private static AdapterBinding getNodeBinding(Object viewObject, + String header, String footer, String layout, int count) { + if (layout != null || header != null || footer != null) { AdapterBinding binding = new AdapterBinding(count); if (header != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java index d40af29..56b86aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutWindowCoordinator.java @@ -232,7 +232,10 @@ public class LayoutWindowCoordinator implements IPartListener2 { * * @param editor the editor to sync */ - private void sync(GraphicalEditorPart editor) { + private void sync(@Nullable GraphicalEditorPart editor) { + if (editor == null) { + return; + } if (mEditorMaximized) { editor.showStructureViews(true /*outline*/, true /*properties*/, true /*layout*/); } else if (mOutlineOpen) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java index 7ddb372..bce1512 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LintOverlay.java @@ -75,6 +75,12 @@ public class LintOverlay extends Overlay { CanvasTransform mHScale = mCanvas.getHorizontalTransform(); CanvasTransform mVScale = mCanvas.getVerticalTransform(); + // Right/bottom edges of the canvas image; don't paint overlays outside of + // that. (With for example RelativeLayouts with margins rendered on smaller + // screens than they are intended for this can happen.) + int maxX = mHScale.translate(0) + mHScale.getScaledImgSize(); + int maxY = mVScale.translate(0) + mVScale.getScaledImgSize(); + int oldAlpha = gc.getAlpha(); try { gc.setAlpha(ALPHA); @@ -95,6 +101,10 @@ public class LintOverlay extends Overlay { x += w - iconWidth; y += h - iconHeight; + if (x > maxX || y > maxY) { + continue; + } + boolean isError = false; IMarker marker = editor.getIssueForNode(vi.getUiViewNode()); if (marker != null) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java index 076b11a..4577f8d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ListViewTypeMenu.java @@ -23,24 +23,19 @@ import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutMet import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.Capability; -import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator; -import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; -import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; import com.android.resources.ResourceType; -import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IFile; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.Separator; -import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.window.Window; import org.eclipse.swt.widgets.Menu; -import org.eclipse.swt.widgets.Shell; import org.w3c.dom.Node; /** @@ -171,33 +166,12 @@ public class ListViewTypeMenu extends SubmenuAction { @Override public void run() { LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); - IProject project = delegate.getEditor().getProject(); - // get the resource repository for this project and the system resources. - ResourceRepository projectRepository = ResourceManager.getInstance() - .getProjectResources(project); - Shell shell = mCanvas.getShell(); - - AndroidTargetData data = delegate.getEditor().getTargetData(); - ResourceRepository systemRepository = data.getFrameworkResources(); - - ResourceChooser dlg = new ResourceChooser(project, - ResourceType.LAYOUT, projectRepository, - systemRepository, shell); - - IInputValidator validator = - CyclicDependencyValidator.create(delegate.getEditor().getInputFile()); - - if (validator != null) { - // Ensure wide enough to accommodate validator error message - dlg.setSize(85, 10); - dlg.setInputValidator(validator); - } - - String layout = getSelectedLayout(); - if (layout != null) { - dlg.setCurrentResource(layout); - } - + IFile file = delegate.getEditor().getInputFile(); + GraphicalEditorPart editor = delegate.getGraphicalEditor(); + ResourceChooser dlg = ResourceChooser.create(editor, ResourceType.LAYOUT) + .setInputValidator(CyclicDependencyValidator.create(file)) + .setInitialSize(85, 10) + .setCurrentResource(getSelectedLayout()); int result = dlg.open(); if (result == ResourceChooser.CLEAR_RETURN_CODE) { setNewType(mType, null); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java index 1d87eb7..7cf3a64 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/MoveGesture.java @@ -383,7 +383,10 @@ public class MoveGesture extends DropGesture { } // Make sure we aren't removing the same nodes that are being added - assert !added.contains(child); + // No, that can happen when canceling out of a drop handler such as + // when dropping an included layout, then canceling out of the + // resource chooser. + //assert !added.contains(child); } } }; @@ -829,9 +832,7 @@ public class MoveGesture extends DropGesture { return; } - String rootFqcn = elements[0].getFqcn(); - - mCanvas.createDocumentRoot(rootFqcn); + mCanvas.createDocumentRoot(elements[0]); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java index 119506b..46168b7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java @@ -378,6 +378,9 @@ public class PaletteControl extends Composite { ConfigurationChooser configChooser = mEditor.getConfigurationChooser(); String theme = configChooser.getThemeName(); String device = configChooser.getDeviceName(); + if (device == null) { + return; + } AndroidTargetData targetData = target != null ? Sdk.getCurrent().getTargetData(target) : null; if (target == mCurrentTarget && targetData == mCurrentTargetData @@ -997,14 +1000,11 @@ public class PaletteControl extends Composite { // This is important since when we fill the size of certain views (like // a SeekBar), we want it to at most be the width of the screen, and for small // screens the RENDER_WIDTH was wider. - Rect screenBounds = editor.getScreenBounds(); - int renderWidth = Math.min(screenBounds.w, MAX_RENDER_WIDTH); - int renderHeight = Math.min(screenBounds.h, MAX_RENDER_HEIGHT); LayoutLog silentLogger = new LayoutLog(); session = RenderService.create(editor) .setModel(model) - .setSize(renderWidth, renderHeight) + .setMaxRenderSize(MAX_RENDER_WIDTH, MAX_RENDER_HEIGHT) .setLog(silentLogger) .setOverrideBgColor(overrideBgColor) .setDecorations(false) diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java index 60e9920..c92ce81 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java @@ -265,7 +265,7 @@ public class PreviewIconFactory { session = RenderService.create(editor) .setModel(model) - .setSize(width, height) + .setOverrideRenderSize(width, height) .setRenderingMode(RenderingMode.FULL_EXPAND) .setLog(new RenderLogger("palette")) .setOverrideBgColor(overrideBgColor) @@ -377,11 +377,15 @@ public class PreviewIconFactory { * * @return a pair of possibly null color descriptions */ + @NonNull private Pair<RGB, RGB> getColorsFromTheme() { RGB background = null; RGB foreground = null; ResourceResolver resources = mPalette.getEditor().getResourceResolver(); + if (resources == null) { + return Pair.of(background, foreground); + } StyleResourceValue theme = resources.getCurrentTheme(); if (theme != null) { background = resolveThemeColor(resources, "windowBackground"); //$NON-NLS-1$ @@ -436,7 +440,7 @@ public class PreviewIconFactory { ResourceResolver resources = editor.getResourceResolver(); ResourceValue resourceValue = resources.findItemInTheme(themeItemName); BufferedImage image = RenderService.create(editor) - .setSize(100, 100) + .setOverrideRenderSize(100, 100) .renderDrawable(resourceValue); if (image != null) { // Use the middle pixel as the color since that works better for gradients; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java new file mode 100644 index 0000000..07baaeb --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreview.java @@ -0,0 +1,1333 @@ +/* + * Copyright (C) 2012 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 static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_RENDERING; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.DEFAULT; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.INCLUDES; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.ide.common.resources.ResourceFile; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; +import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.io.IAbstractFile; +import com.android.resources.Density; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.State; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.IJobChangeListener; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.progress.UIJob; +import org.w3c.dom.Document; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.lang.ref.SoftReference; +import java.util.Comparator; +import java.util.Map; + +/** + * Represents a preview rendering of a given configuration + */ +public class RenderPreview implements IJobChangeListener { + /** Whether previews should use large shadows */ + static final boolean LARGE_SHADOWS = false; + + /** + * Still doesn't work; get exceptions from layoutlib: + * java.lang.IllegalStateException: After scene creation, #init() must be called + * at com.android.layoutlib.bridge.impl.RenderAction.acquire(RenderAction.java:151) + * <p> + * TODO: Investigate. + */ + private static final boolean RENDER_ASYNC = false; + + /** + * Height of the toolbar shown over a preview during hover. Needs to be + * large enough to accommodate icons below. + */ + private static final int HEADER_HEIGHT = 20; + + /** Whether to dump out rendering failures of the previews to the log */ + private static final boolean DUMP_RENDER_DIAGNOSTICS = false; + + /** Extra error checking in debug mode */ + private static final boolean DEBUG = false; + + private static final Image EDIT_ICON; + private static final Image ZOOM_IN_ICON; + private static final Image ZOOM_OUT_ICON; + private static final Image CLOSE_ICON; + private static final int EDIT_ICON_WIDTH; + private static final int ZOOM_IN_ICON_WIDTH; + private static final int ZOOM_OUT_ICON_WIDTH; + private static final int CLOSE_ICON_WIDTH; + static { + ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); + IconFactory icons = IconFactory.getInstance(); + CLOSE_ICON = sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE); + EDIT_ICON = icons.getIcon("editPreview"); //$NON-NLS-1$ + ZOOM_IN_ICON = icons.getIcon("zoomplus"); //$NON-NLS-1$ + ZOOM_OUT_ICON = icons.getIcon("zoomminus"); //$NON-NLS-1$ + CLOSE_ICON_WIDTH = CLOSE_ICON.getImageData().width; + EDIT_ICON_WIDTH = EDIT_ICON.getImageData().width; + ZOOM_IN_ICON_WIDTH = ZOOM_IN_ICON.getImageData().width; + ZOOM_OUT_ICON_WIDTH = ZOOM_OUT_ICON.getImageData().width; + } + + /** The configuration being previewed */ + private @NonNull Configuration mConfiguration; + + /** Configuration to use if we have an alternate input to be rendered */ + private @NonNull Configuration mAlternateConfiguration; + + /** The associated manager */ + private final @NonNull RenderPreviewManager mManager; + private final @NonNull LayoutCanvas mCanvas; + + private @NonNull SoftReference<ResourceResolver> mResourceResolver = + new SoftReference<ResourceResolver>(null); + private @Nullable Job mJob; + private @Nullable Image mThumbnail; + private @Nullable String mDisplayName; + private int mWidth; + private int mHeight; + private int mX; + private int mY; + private int mTitleHeight; + private double mScale = 1.0; + private double mAspectRatio; + + /** If non null, points to a separate file containing the source */ + private @Nullable IFile mAlternateInput; + + /** If included within another layout, the name of that outer layout */ + private @Nullable Reference mIncludedWithin; + + /** Whether the mouse is actively hovering over this preview */ + private boolean mActive; + + /** + * Whether this preview cannot be rendered because of a model error - such + * as an invalid configuration, a missing resource, an error in the XML + * markup, etc. If non null, contains the error message (or a blank string + * if not known), and null if the render was successful. + */ + private String mError; + + /** Whether in the current layout, this preview is visible */ + private boolean mVisible; + + /** Whether the configuration has changed and needs to be refreshed the next time + * this preview made visible. This corresponds to the change flags in + * {@link ConfigurationClient}. */ + private int mDirty; + + /** + * Creates a new {@linkplain RenderPreview} + * + * @param manager the manager + * @param canvas canvas where preview is painted + * @param configuration the associated configuration + * @param width the initial width to use for the preview + * @param height the initial height to use for the preview + */ + private RenderPreview( + @NonNull RenderPreviewManager manager, + @NonNull LayoutCanvas canvas, + @NonNull Configuration configuration) { + mManager = manager; + mCanvas = canvas; + mConfiguration = configuration; + updateSize(); + + // Should only attempt to create configurations for fully configured devices + assert mConfiguration.getDevice() != null + && mConfiguration.getDeviceState() != null + && mConfiguration.getLocale() != null + && mConfiguration.getTarget() != null + && mConfiguration.getTheme() != null + && mConfiguration.getFullConfig() != null + && mConfiguration.getFullConfig().getScreenSizeQualifier() != null : + mConfiguration; + } + + /** + * Sets the configuration to use for this preview + * + * @param configuration the new configuration + */ + public void setConfiguration(@NonNull Configuration configuration) { + mConfiguration = configuration; + } + + /** + * Gets the scale being applied to the thumbnail + * + * @return the scale being applied to the thumbnail + */ + public double getScale() { + return mScale; + } + + /** + * Sets the scale to apply to the thumbnail + * + * @param scale the factor to scale the thumbnail picture by + */ + public void setScale(double scale) { + disposeThumbnail(); + mScale = scale; + } + + /** + * Returns the aspect ratio of this render preview + * + * @return the aspect ratio + */ + public double getAspectRatio() { + return mAspectRatio; + } + + /** + * Returns whether the preview is actively hovered + * + * @return whether the mouse is hovering over the preview + */ + public boolean isActive() { + return mActive; + } + + /** + * Sets whether the preview is actively hovered + * + * @param active if the mouse is hovering over the preview + */ + public void setActive(boolean active) { + mActive = active; + } + + /** + * Returns whether the preview is visible. Previews that are off + * screen are typically marked invisible during layout, which means we don't + * have to expend effort computing preview thumbnails etc + * + * @return true if the preview is visible + */ + public boolean isVisible() { + return mVisible; + } + + /** + * Returns whether this preview represents a forked layout + * + * @return true if this preview represents a separate file + */ + public boolean isForked() { + return mAlternateInput != null || mIncludedWithin != null; + } + + /** + * Returns the file to be used for this preview, or null if this is not a + * forked layout meaning that the file is the one used in the chooser + * + * @return the file or null for non-forked layouts + */ + @Nullable + public IFile getAlternateInput() { + if (mAlternateInput != null) { + return mAlternateInput; + } else if (mIncludedWithin != null) { + return mIncludedWithin.getFile(); + } + + return null; + } + + /** + * Returns the area of this render preview, PRIOR to scaling + * + * @return the area (width times height without scaling) + */ + int getArea() { + return mWidth * mHeight; + } + + /** + * Sets whether the preview is visible. Previews that are off + * screen are typically marked invisible during layout, which means we don't + * have to expend effort computing preview thumbnails etc + * + * @param visible whether this preview is visible + */ + public void setVisible(boolean visible) { + if (visible != mVisible) { + mVisible = visible; + if (mVisible) { + if (mDirty != 0) { + // Just made the render preview visible: + configurationChanged(mDirty); // schedules render + } else { + updateForkStatus(); + mManager.scheduleRender(this); + } + } else { + dispose(); + } + } + } + + /** + * Sets the layout position relative to the top left corner of the preview + * area, in control coordinates + */ + void setPosition(int x, int y) { + mX = x; + mY = y; + } + + /** + * Gets the layout X position relative to the top left corner of the preview + * area, in control coordinates + */ + int getX() { + return mX; + } + + /** + * Gets the layout Y position relative to the top left corner of the preview + * area, in control coordinates + */ + int getY() { + return mY; + } + + /** Determine whether this configuration has a better match in a different layout file */ + private void updateForkStatus() { + ConfigurationChooser chooser = mManager.getChooser(); + FolderConfiguration config = mConfiguration.getFullConfig(); + if (mAlternateInput != null && chooser.isBestMatchFor(mAlternateInput, config)) { + return; + } + + mAlternateInput = null; + IFile editedFile = chooser.getEditedFile(); + if (editedFile != null) { + if (!chooser.isBestMatchFor(editedFile, config)) { + ProjectResources resources = chooser.getResources(); + if (resources != null) { + ResourceFile best = resources.getMatchingFile(editedFile.getName(), + ResourceType.LAYOUT, config); + if (best != null) { + IAbstractFile file = best.getFile(); + if (file instanceof IFileWrapper) { + mAlternateInput = ((IFileWrapper) file).getIFile(); + } else if (file instanceof File) { + mAlternateInput = AdtUtils.fileToIFile(((File) file)); + } + } + } + if (mAlternateInput != null) { + mAlternateConfiguration = Configuration.create(mConfiguration, + mAlternateInput); + } + } + } + } + + /** + * Creates a new {@linkplain RenderPreview} + * + * @param manager the manager + * @param configuration the associated configuration + * @return a new configuration + */ + @NonNull + public static RenderPreview create( + @NonNull RenderPreviewManager manager, + @NonNull Configuration configuration) { + LayoutCanvas canvas = manager.getCanvas(); + return new RenderPreview(manager, canvas, configuration); + } + + /** + * Throws away this preview: cancels any pending rendering jobs and disposes + * of image resources etc + */ + public void dispose() { + disposeThumbnail(); + + if (mJob != null) { + mJob.cancel(); + mJob = null; + } + } + + /** Disposes the thumbnail rendering. */ + void disposeThumbnail() { + if (mThumbnail != null) { + mThumbnail.dispose(); + mThumbnail = null; + } + } + + /** + * Returns the display name of this preview + * + * @return the name of the preview + */ + @NonNull + public String getDisplayName() { + if (mDisplayName == null) { + String displayName = getConfiguration().getDisplayName(); + if (displayName == null) { + // No display name: this must be the configuration used by default + // for the view which is originally displayed (before adding thumbnails), + // and you've switched away to something else; now we need to display a name + // for this original configuration. For now, just call it "Original" + return "Original"; + } + + return displayName; + } + + return mDisplayName; + } + + /** + * Sets the display name of this preview. By default, the display name is + * the display name of the configuration, but it can be overridden by calling + * this setter (which only sets the preview name, without editing the configuration.) + * + * @param displayName the new display name + */ + public void setDisplayName(@NonNull String displayName) { + mDisplayName = displayName; + } + + /** + * Sets an inclusion context to use for this layout, if any. This will render + * the configuration preview as the outer layout with the current layout + * embedded within. + * + * @param includedWithin a reference to a layout which includes this one + */ + public void setIncludedWithin(Reference includedWithin) { + mIncludedWithin = includedWithin; + } + + /** + * Request a new render after the given delay + * + * @param delay the delay to wait before starting the render job + */ + public void render(long delay) { + Job job = mJob; + if (job != null) { + job.cancel(); + } + if (RENDER_ASYNC) { + job = new AsyncRenderJob(); + } else { + job = new RenderJob(); + } + job.schedule(delay); + job.addJobChangeListener(this); + mJob = job; + } + + /** Render immediately */ + private void renderSync() { + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + if (editor.getReadyLayoutLib(false /*displayError*/) == null) { + // Don't attempt to render when there is no ready layout library: most likely + // the targets are loading/reloading. + return; + } + + disposeThumbnail(); + + Configuration configuration = + mAlternateInput != null && mAlternateConfiguration != null + ? mAlternateConfiguration : mConfiguration; + ResourceResolver resolver = getResourceResolver(configuration); + RenderService renderService = RenderService.create(editor, configuration, resolver); + + if (mIncludedWithin != null) { + renderService.setIncludedWithin(mIncludedWithin); + } + + if (mAlternateInput != null) { + IAndroidTarget target = editor.getRenderingTarget(); + AndroidTargetData data = null; + if (target != null) { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + data = sdk.getTargetData(target); + } + } + + // Construct UI model from XML + DocumentDescriptor documentDescriptor; + if (data == null) { + documentDescriptor = new DocumentDescriptor("temp", null);//$NON-NLS-1$ + } else { + documentDescriptor = data.getLayoutDescriptors().getDescriptor(); + } + UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); + model.setEditor(mCanvas.getEditorDelegate().getEditor()); + model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); + + Document document = DomUtilities.getDocument(mAlternateInput); + if (document == null) { + mError = "No document"; + createErrorThumbnail(); + return; + } + model.loadFromXmlNode(document); + renderService.setModel(model); + } else { + renderService.setModel(editor.getModel()); + } + RenderLogger log = new RenderLogger(getDisplayName()); + renderService.setLog(log); + RenderSession session = renderService.createRenderSession(); + Result render = session.render(1000); + + if (DUMP_RENDER_DIAGNOSTICS) { + if (log.hasProblems() || !render.isSuccess()) { + AdtPlugin.log(IStatus.ERROR, "Found problems rendering preview " + + getDisplayName() + ": " + + render.getErrorMessage() + " : " + + log.getProblems(false)); + Throwable exception = render.getException(); + if (exception != null) { + AdtPlugin.log(exception, "Failure rendering preview " + getDisplayName()); + } + } + } + + if (render.isSuccess()) { + mError = null; + } else { + mError = render.getErrorMessage(); + if (mError == null) { + mError = ""; + } + } + + if (render.getStatus() == Status.ERROR_TIMEOUT) { + // TODO: Special handling? schedule update again later + return; + } + if (render.isSuccess()) { + BufferedImage image = session.getImage(); + if (image != null) { + createThumbnail(image); + } + } + + if (mError != null) { + createErrorThumbnail(); + } + } + + private ResourceResolver getResourceResolver(Configuration configuration) { + ResourceResolver resourceResolver = mResourceResolver.get(); + if (resourceResolver != null) { + return resourceResolver; + } + + GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor(); + String theme = configuration.getTheme(); + if (theme == null) { + return null; + } + + Map<ResourceType, Map<String, ResourceValue>> configuredFrameworkRes = null; + Map<ResourceType, Map<String, ResourceValue>> configuredProjectRes = null; + + FolderConfiguration config = configuration.getFullConfig(); + IAndroidTarget target = graphicalEditor.getRenderingTarget(); + ResourceRepository frameworkRes = null; + if (target != null) { + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return null; + } + AndroidTargetData data = sdk.getTargetData(target); + + if (data != null) { + // TODO: SHARE if possible + frameworkRes = data.getFrameworkResources(); + configuredFrameworkRes = frameworkRes.getConfiguredResources(config); + } else { + return null; + } + } else { + return null; + } + assert configuredFrameworkRes != null; + + + // get the resources of the file's project. + ProjectResources projectRes = ResourceManager.getInstance().getProjectResources( + graphicalEditor.getProject()); + configuredProjectRes = projectRes.getConfiguredResources(config); + + if (!theme.startsWith(PREFIX_RESOURCE_REF)) { + if (frameworkRes.hasResourceItem(ANDROID_STYLE_RESOURCE_PREFIX + theme)) { + theme = ANDROID_STYLE_RESOURCE_PREFIX + theme; + } else { + theme = STYLE_RESOURCE_PREFIX + theme; + } + } + + resourceResolver = ResourceResolver.create( + configuredProjectRes, configuredFrameworkRes, + ResourceHelper.styleToTheme(theme), + ResourceHelper.isProjectStyle(theme)); + mResourceResolver = new SoftReference<ResourceResolver>(resourceResolver); + return resourceResolver; + } + + /** + * Sets the new image of the preview and generates a thumbnail + * + * @param image the full size image + */ + void createThumbnail(BufferedImage image) { + if (image == null) { + mThumbnail = null; + return; + } + + ImageOverlay imageOverlay = mCanvas.getImageOverlay(); + boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow(); + double scale = getWidth() / (double) image.getWidth(); + int shadowSize; + if (LARGE_SHADOWS) { + shadowSize = drawShadows ? SHADOW_SIZE : 0; + } else { + shadowSize = drawShadows ? SMALL_SHADOW_SIZE : 0; + } + if (scale < 1.0) { + if (LARGE_SHADOWS) { + image = ImageUtils.scale(image, scale, scale, + shadowSize, shadowSize); + if (drawShadows) { + ImageUtils.drawRectangleShadow(image, 0, 0, + image.getWidth() - shadowSize, + image.getHeight() - shadowSize); + } + } else { + image = ImageUtils.scale(image, scale, scale, + shadowSize, shadowSize); + if (drawShadows) { + ImageUtils.drawSmallRectangleShadow(image, 0, 0, + image.getWidth() - shadowSize, + image.getHeight() - shadowSize); + } + } + } + + mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, + true /* transferAlpha */, -1); + } + + void createErrorThumbnail() { + int shadowSize = LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; + int width = getWidth(); + int height = getHeight(); + BufferedImage image = new BufferedImage(width + shadowSize, height + shadowSize, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g = image.createGraphics(); + g.setColor(new java.awt.Color(0xfffbfcc6)); + g.fillRect(0, 0, width, height); + + g.dispose(); + + ImageOverlay imageOverlay = mCanvas.getImageOverlay(); + boolean drawShadows = imageOverlay == null || imageOverlay.getShowDropShadow(); + if (drawShadows) { + if (LARGE_SHADOWS) { + ImageUtils.drawRectangleShadow(image, 0, 0, + image.getWidth() - SHADOW_SIZE, + image.getHeight() - SHADOW_SIZE); + } else { + ImageUtils.drawSmallRectangleShadow(image, 0, 0, + image.getWidth() - SMALL_SHADOW_SIZE, + image.getHeight() - SMALL_SHADOW_SIZE); + } + } + + mThumbnail = SwtUtils.convertToSwt(mCanvas.getDisplay(), image, + true /* transferAlpha */, -1); + } + + private static double getScale(int width, int height) { + int maxWidth = RenderPreviewManager.getMaxWidth(); + int maxHeight = RenderPreviewManager.getMaxHeight(); + if (width > 0 && height > 0 + && (width > maxWidth || height > maxHeight)) { + if (width >= height) { // landscape + return maxWidth / (double) width; + } else { // portrait + return maxHeight / (double) height; + } + } + + return 1.0; + } + + /** + * Returns the width of the preview, in pixels + * + * @return the width in pixels + */ + public int getWidth() { + return (int) (mWidth * mScale * RenderPreviewManager.getScale()); + } + + /** + * Returns the height of the preview, in pixels + * + * @return the height in pixels + */ + public int getHeight() { + return (int) (mHeight * mScale * RenderPreviewManager.getScale()); + } + + /** + * Handles clicks within the preview (x and y are positions relative within the + * preview + * + * @param x the x coordinate within the preview where the click occurred + * @param y the y coordinate within the preview where the click occurred + * @return true if this preview handled (and therefore consumed) the click + */ + public boolean click(int x, int y) { + if (y >= mTitleHeight && y < mTitleHeight + HEADER_HEIGHT) { + int left = 0; + left += CLOSE_ICON_WIDTH; + if (x <= left) { + // Delete + mManager.deletePreview(this); + return true; + } + left += ZOOM_IN_ICON_WIDTH; + if (x <= left) { + // Zoom in + mScale = mScale * (1 / 0.5); + if (Math.abs(mScale-1.0) < 0.0001) { + mScale = 1.0; + } + + render(0); + mManager.layout(true); + mCanvas.redraw(); + return true; + } + left += ZOOM_OUT_ICON_WIDTH; + if (x <= left) { + // Zoom out + mScale = mScale * (0.5 / 1); + if (Math.abs(mScale-1.0) < 0.0001) { + mScale = 1.0; + } + render(0); + + mManager.layout(true); + mCanvas.redraw(); + return true; + } + left += EDIT_ICON_WIDTH; + if (x <= left) { + // Edit. For now, just rename + InputDialog d = new InputDialog( + AdtPlugin.getShell(), + "Rename Preview", // title + "Name:", + getDisplayName(), + null); + if (d.open() == Window.OK) { + String newName = d.getValue(); + mConfiguration.setDisplayName(newName); + if (mDescription != null) { + mManager.rename(mDescription, newName); + } + mCanvas.redraw(); + } + + return true; + } + + // Clicked anywhere else on header + // Perhaps open Edit dialog here? + } + + mManager.switchTo(this); + return true; + } + + /** + * Paints the preview at the given x/y position + * + * @param gc the graphics context to paint it into + * @param x the x coordinate to paint the preview at + * @param y the y coordinate to paint the preview at + */ + void paint(GC gc, int x, int y) { + mTitleHeight = paintTitle(gc, x, y, true /*showFile*/); + y += mTitleHeight; + y += 2; + + int width = getWidth(); + int height = getHeight(); + if (mThumbnail != null && mError == null) { + gc.drawImage(mThumbnail, x, y); + + if (mActive) { + int oldWidth = gc.getLineWidth(); + gc.setLineWidth(3); + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION)); + gc.drawRectangle(x - 1, y - 1, width + 2, height + 2); + gc.setLineWidth(oldWidth); + } + } else if (mError != null) { + if (mThumbnail != null) { + gc.drawImage(mThumbnail, x, y); + } else { + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); + gc.drawRectangle(x, y, width, height); + } + + gc.setClipping(x, y, width, height); + Image icon = IconFactory.getInstance().getIcon("renderError"); //$NON-NLS-1$ + ImageData data = icon.getImageData(); + int prevAlpha = gc.getAlpha(); + int alpha = 96; + if (mThumbnail != null) { + alpha -= 32; + } + gc.setAlpha(alpha); + gc.drawImage(icon, x + (width - data.width) / 2, y + (height - data.height) / 2); + + String msg = mError; + Density density = mConfiguration.getDensity(); + if (density == Density.TV || density == Density.LOW) { + msg = "Broken rendering library; unsupported DPI. Try using the SDK manager " + + "to get updated layout libraries."; + } + int charWidth = gc.getFontMetrics().getAverageCharWidth(); + int charsPerLine = (width - 10) / charWidth; + msg = SdkUtils.wrap(msg, charsPerLine, null); + gc.setAlpha(255); + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_BLACK)); + gc.drawText(msg, x + 5, y + HEADER_HEIGHT, true); + gc.setAlpha(prevAlpha); + gc.setClipping((Region) null); + } else { + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_BORDER)); + gc.drawRectangle(x, y, width, height); + + Image icon = IconFactory.getInstance().getIcon("refreshPreview"); //$NON-NLS-1$ + ImageData data = icon.getImageData(); + int prevAlpha = gc.getAlpha(); + gc.setAlpha(96); + gc.drawImage(icon, x + (width - data.width) / 2, + y + (height - data.height) / 2); + gc.setAlpha(prevAlpha); + } + + if (mActive) { + int left = x ; + int prevAlpha = gc.getAlpha(); + gc.setAlpha(208); + Color bg = mCanvas.getDisplay().getSystemColor(SWT.COLOR_WHITE); + gc.setBackground(bg); + gc.fillRectangle(left, y, x + width - left, HEADER_HEIGHT); + gc.setAlpha(prevAlpha); + + y += 2; + + // Paint icons + gc.drawImage(CLOSE_ICON, left, y); + left += CLOSE_ICON_WIDTH; + + gc.drawImage(ZOOM_IN_ICON, left, y); + left += ZOOM_IN_ICON_WIDTH; + + gc.drawImage(ZOOM_OUT_ICON, left, y); + left += ZOOM_OUT_ICON_WIDTH; + + gc.drawImage(EDIT_ICON, left, y); + left += EDIT_ICON_WIDTH; + } + } + + /** + * Paints the preview title at the given position (and returns the required + * height) + * + * @param gc the graphics context to paint into + * @param x the left edge of the preview rectangle + * @param y the top edge of the preview rectangle + */ + private int paintTitle(GC gc, int x, int y, boolean showFile) { + String displayName = getDisplayName(); + return paintTitle(gc, x, y, showFile, displayName); + } + + /** + * Paints the preview title at the given position (and returns the required + * height) + * + * @param gc the graphics context to paint into + * @param x the left edge of the preview rectangle + * @param y the top edge of the preview rectangle + * @param displayName the title string to be used + */ + int paintTitle(GC gc, int x, int y, boolean showFile, String displayName) { + int titleHeight = 0; + + if (showFile && mIncludedWithin != null) { + if (mManager.getMode() != INCLUDES) { + displayName = "<include>"; + } else { + // Skip: just paint footer instead + displayName = null; + } + } + + int width = getWidth(); + int labelTop = y + 1; + gc.setClipping(x, labelTop, width, 100); + + // Use font height rather than extent height since we want two adjacent + // previews (which may have different display names and therefore end + // up with slightly different extent heights) to have identical title + // heights such that they are aligned identically + int fontHeight = gc.getFontMetrics().getHeight(); + + if (displayName != null && displayName.length() > 0) { + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WHITE)); + Point extent = gc.textExtent(displayName); + int labelLeft = Math.max(x, x + (width - extent.x) / 2); + Image icon = null; + Locale locale = mConfiguration.getLocale(); + if (locale != null && (locale.hasLanguage() || locale.hasRegion()) + && (!(mConfiguration instanceof NestedConfiguration) + || ((NestedConfiguration) mConfiguration).isOverridingLocale())) { + icon = locale.getFlagImage(); + } + + if (icon != null) { + int flagWidth = icon.getImageData().width; + int flagHeight = icon.getImageData().height; + labelLeft = Math.max(x + flagWidth / 2, labelLeft); + gc.drawImage(icon, labelLeft - flagWidth / 2 - 1, labelTop); + labelLeft += flagWidth / 2 + 1; + gc.drawText(displayName, labelLeft, + labelTop - (extent.y - flagHeight) / 2, true); + } else { + gc.drawText(displayName, labelLeft, labelTop, true); + } + + labelTop += extent.y; + titleHeight += fontHeight; + } + + if (showFile && (mAlternateInput != null || mIncludedWithin != null)) { + // Draw file flag, and parent folder name + IFile file = mAlternateInput != null + ? mAlternateInput : mIncludedWithin.getFile(); + String fileName = file.getParent().getName() + File.separator + + file.getName(); + Point extent = gc.textExtent(fileName); + Image icon = IconFactory.getInstance().getIcon("android_file"); //$NON-NLS-1$ + int flagWidth = icon.getImageData().width; + int flagHeight = icon.getImageData().height; + + int labelLeft = Math.max(x, x + (width - extent.x - flagWidth - 1) / 2); + + gc.drawImage(icon, labelLeft, labelTop); + + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); + labelLeft += flagWidth + 1; + labelTop -= (extent.y - flagHeight) / 2; + gc.drawText(fileName, labelLeft, labelTop, true); + + titleHeight += Math.max(titleHeight, icon.getImageData().height); + } + + gc.setClipping((Region) null); + + return titleHeight; + } + + /** + * Notifies that the preview's configuration has changed. + * + * @param flags the change flags, a bitmask corresponding to the + * {@code CHANGE_} constants in {@link ConfigurationClient} + */ + public void configurationChanged(int flags) { + if (!mVisible) { + mDirty |= flags; + return; + } + + if ((flags & MASK_RENDERING) != 0) { + mResourceResolver.clear(); + // Handle inheritance + mConfiguration.syncFolderConfig(); + updateForkStatus(); + updateSize(); + } + + // Sanity check to make sure things are working correctly + if (DEBUG) { + RenderPreviewMode mode = mManager.getMode(); + if (mode == DEFAULT) { + assert mConfiguration instanceof VaryingConfiguration; + VaryingConfiguration config = (VaryingConfiguration) mConfiguration; + int alternateFlags = config.getAlternateFlags(); + switch (alternateFlags) { + case Configuration.CFG_DEVICE_STATE: { + State configState = config.getDeviceState(); + State chooserState = mManager.getChooser().getConfiguration() + .getDeviceState(); + assert configState != null && chooserState != null; + assert !configState.getName().equals(chooserState.getName()) + : configState.toString() + ':' + chooserState; + + Device configDevice = config.getDevice(); + Device chooserDevice = mManager.getChooser().getConfiguration() + .getDevice(); + assert configDevice != null && chooserDevice != null; + assert configDevice == chooserDevice + : configDevice.toString() + ':' + chooserDevice; + + break; + } + case Configuration.CFG_DEVICE: { + Device configDevice = config.getDevice(); + Device chooserDevice = mManager.getChooser().getConfiguration() + .getDevice(); + assert configDevice != null && chooserDevice != null; + assert configDevice != chooserDevice + : configDevice.toString() + ':' + chooserDevice; + + State configState = config.getDeviceState(); + State chooserState = mManager.getChooser().getConfiguration() + .getDeviceState(); + assert configState != null && chooserState != null; + assert configState.getName().equals(chooserState.getName()) + : configState.toString() + ':' + chooserState; + + break; + } + case Configuration.CFG_LOCALE: { + Locale configLocale = config.getLocale(); + Locale chooserLocale = mManager.getChooser().getConfiguration() + .getLocale(); + assert configLocale != null && chooserLocale != null; + assert configLocale != chooserLocale + : configLocale.toString() + ':' + chooserLocale; + break; + } + default: { + // Some other type of override I didn't anticipate + assert false : alternateFlags; + } + } + } + } + + mDirty = 0; + mManager.scheduleRender(this); + } + + private void updateSize() { + Device device = mConfiguration.getDevice(); + if (device == null) { + return; + } + Screen screen = device.getDefaultHardware().getScreen(); + if (screen == null) { + return; + } + + FolderConfiguration folderConfig = mConfiguration.getFullConfig(); + ScreenOrientationQualifier qualifier = folderConfig.getScreenOrientationQualifier(); + ScreenOrientation orientation = qualifier == null + ? ScreenOrientation.PORTRAIT : qualifier.getValue(); + + // compute width and height to take orientation into account. + int x = screen.getXDimension(); + int y = screen.getYDimension(); + int screenWidth, screenHeight; + + if (x > y) { + if (orientation == ScreenOrientation.LANDSCAPE) { + screenWidth = x; + screenHeight = y; + } else { + screenWidth = y; + screenHeight = x; + } + } else { + if (orientation == ScreenOrientation.LANDSCAPE) { + screenWidth = y; + screenHeight = x; + } else { + screenWidth = x; + screenHeight = y; + } + } + + int width = RenderPreviewManager.getMaxWidth(); + int height = RenderPreviewManager.getMaxHeight(); + if (screenWidth > 0) { + double scale = getScale(screenWidth, screenHeight); + width = (int) (screenWidth * scale); + height = (int) (screenHeight * scale); + } + + if (width != mWidth || height != mHeight) { + mWidth = width; + mHeight = height; + + Image thumbnail = mThumbnail; + mThumbnail = null; + if (thumbnail != null) { + thumbnail.dispose(); + } + if (mHeight != 0) { + mAspectRatio = mWidth / (double) mHeight; + } + } + } + + /** + * Returns the configuration associated with this preview + * + * @return the configuration + */ + @NonNull + public Configuration getConfiguration() { + return mConfiguration; + } + + // ---- Implements IJobChangeListener ---- + + @Override + public void aboutToRun(IJobChangeEvent event) { + } + + @Override + public void awake(IJobChangeEvent event) { + } + + @Override + public void done(IJobChangeEvent event) { + mJob = null; + } + + @Override + public void running(IJobChangeEvent event) { + } + + @Override + public void scheduled(IJobChangeEvent event) { + } + + @Override + public void sleeping(IJobChangeEvent event) { + } + + // ---- Delayed Rendering ---- + + private final class RenderJob extends UIJob { + public RenderJob() { + super("RenderPreview"); + setSystem(true); + setUser(false); + } + + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + mJob = null; + if (!mCanvas.isDisposed()) { + renderSync(); + mCanvas.redraw(); + return org.eclipse.core.runtime.Status.OK_STATUS; + } + + return org.eclipse.core.runtime.Status.CANCEL_STATUS; + } + + @Override + public Display getDisplay() { + if (mCanvas.isDisposed()) { + return null; + } + return mCanvas.getDisplay(); + } + } + + private final class AsyncRenderJob extends Job { + public AsyncRenderJob() { + super("RenderPreview"); + setSystem(true); + setUser(false); + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + mJob = null; + + if (mCanvas.isDisposed()) { + return org.eclipse.core.runtime.Status.CANCEL_STATUS; + } + + renderSync(); + + // Update display + mCanvas.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + mCanvas.redraw(); + } + }); + + return org.eclipse.core.runtime.Status.OK_STATUS; + } + } + + /** + * Sets the input file to use for rendering. If not set, this will just be + * the same file as the configuration chooser. This is used to render other + * layouts, such as variations of the currently edited layout, which are + * not kept in sync with the main layout. + * + * @param file the file to set as input + */ + public void setAlternateInput(@Nullable IFile file) { + mAlternateInput = file; + } + + /** Corresponding description for this preview if it is a manually added preview */ + private @Nullable ConfigurationDescription mDescription; + + /** + * Sets the description of this preview, if this preview is a manually added preview + * + * @param description the description of this preview + */ + public void setDescription(@Nullable ConfigurationDescription description) { + mDescription = description; + } + + /** + * Returns the description of this preview, if this preview is a manually added preview + * + * @return the description + */ + @Nullable + public ConfigurationDescription getDescription() { + return mDescription; + } + + @Override + public String toString() { + return getDisplayName() + ':' + mConfiguration; + } + + /** Sorts render previews into increasing aspect ratio order */ + static Comparator<RenderPreview> INCREASING_ASPECT_RATIO = new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + return (int) Math.signum(preview1.mAspectRatio - preview2.mAspectRatio); + } + }; + /** Sorts render previews into visual order: row by row, column by column */ + static Comparator<RenderPreview> VISUAL_ORDER = new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + int delta = preview1.mY - preview2.mY; + if (delta == 0) { + delta = preview1.mX - preview2.mX; + } + return delta; + } + }; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java new file mode 100644 index 0000000..f5d3290 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewList.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2012 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 com.android.annotations.NonNull; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.sdklib.devices.Device; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.io.Files; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.QualifiedName; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** A list of render previews */ +class RenderPreviewList { + /** Name of file saved in project directory storing previews */ + private static final String PREVIEW_FILE_NAME = "previews.xml"; //$NON-NLS-1$ + + /** Qualified name for the per-project persistent property include-map */ + private final static QualifiedName PREVIEW_LIST = new QualifiedName(AdtPlugin.PLUGIN_ID, + "previewlist");//$NON-NLS-1$ + + private final IProject mProject; + private final List<ConfigurationDescription> mList = Lists.newArrayList(); + + private RenderPreviewList(@NonNull IProject project) { + mProject = project; + } + + /** + * Returns the {@link RenderPreviewList} for the given project + * + * @param project the project the list is associated with + * @return a {@link RenderPreviewList} for the given project, never null + */ + @NonNull + public static RenderPreviewList get(@NonNull IProject project) { + RenderPreviewList list = null; + try { + list = (RenderPreviewList) project.getSessionProperty(PREVIEW_LIST); + } catch (CoreException e) { + // Not a problem; we will just create a new one + } + + if (list == null) { + list = new RenderPreviewList(project); + try { + project.setSessionProperty(PREVIEW_LIST, list); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + return list; + } + + private File getManualFile() { + return new File(AdtUtils.getAbsolutePath(mProject).toFile(), PREVIEW_FILE_NAME); + } + + void load(List<Device> deviceList) throws IOException { + File file = getManualFile(); + if (file.exists()) { + load(file, deviceList); + } + } + + void save() throws IOException { + deleteFile(); + if (!mList.isEmpty()) { + File file = getManualFile(); + save(file); + } + } + + private void save(File file) throws IOException { + //Document document = DomUtilities.createEmptyPlainDocument(); + Document document = DomUtilities.createEmptyDocument(); + if (document != null) { + for (ConfigurationDescription description : mList) { + description.toXml(document); + } + String xml = EclipseXmlPrettyPrinter.prettyPrint(document, true); + Files.write(xml, file, Charsets.UTF_8); + } + } + + void load(File file, List<Device> deviceList) throws IOException { + mList.clear(); + + String xml = Files.toString(file, Charsets.UTF_8); + Document document = DomUtilities.parseDocument(xml, true); + if (document == null || document.getDocumentElement() == null) { + return; + } + List<Element> elements = DomUtilities.getChildren(document.getDocumentElement()); + for (Element element : elements) { + ConfigurationDescription description = ConfigurationDescription.fromXml( + mProject, element, deviceList); + if (description != null) { + mList.add(description); + } + } + } + + /** + * Create a list of previews for the given canvas that matches the internal + * configuration preview list + * + * @param canvas the associated canvas + * @return a new list of previews linked to the given canvas + */ + @NonNull + List<RenderPreview> createPreviews(LayoutCanvas canvas) { + if (mList.isEmpty()) { + return new ArrayList<RenderPreview>(); + } + List<RenderPreview> previews = Lists.newArrayList(); + RenderPreviewManager manager = canvas.getPreviewManager(); + ConfigurationChooser chooser = canvas.getEditorDelegate().getGraphicalEditor() + .getConfigurationChooser(); + + Configuration chooserConfig = chooser.getConfiguration(); + for (ConfigurationDescription description : mList) { + Configuration configuration = Configuration.create(chooser); + configuration.setDisplayName(description.displayName); + configuration.setActivity(description.activity); + configuration.setLocale( + description.locale != null ? description.locale : chooserConfig.getLocale(), + true); + // TODO: Make sure this layout isn't in some v-folder which is incompatible + // with this target! + configuration.setTarget( + description.target != null ? description.target : chooserConfig.getTarget(), + true); + configuration.setTheme( + description.theme != null ? description.theme : chooserConfig.getTheme()); + configuration.setDevice( + description.device != null ? description.device : chooserConfig.getDevice(), + true); + configuration.setDeviceState( + description.state != null ? description.state : chooserConfig.getDeviceState(), + true); + configuration.setNightMode( + description.nightMode != null ? description.nightMode + : chooserConfig.getNightMode(), true); + configuration.setUiMode( + description.uiMode != null ? description.uiMode : chooserConfig.getUiMode(), true); + + //configuration.syncFolderConfig(); + configuration.getFullConfig().set(description.folder); + + RenderPreview preview = RenderPreview.create(manager, configuration); + + preview.setDescription(description); + previews.add(preview); + } + + return previews; + } + + void remove(@NonNull RenderPreview preview) { + ConfigurationDescription description = preview.getDescription(); + if (description != null) { + mList.remove(description); + } + } + + boolean isEmpty() { + return mList.isEmpty(); + } + + void add(@NonNull RenderPreview preview) { + Configuration configuration = preview.getConfiguration(); + ConfigurationDescription description = + ConfigurationDescription.fromConfiguration(mProject, configuration); + // RenderPreviews can have display names that aren't reflected in the configuration + description.displayName = preview.getDisplayName(); + mList.add(description); + preview.setDescription(description); + } + + void delete() { + mList.clear(); + deleteFile(); + } + + private void deleteFile() { + File file = getManualFile(); + if (file.exists()) { + file.delete(); + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java new file mode 100644 index 0000000..4b0f484 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewManager.java @@ -0,0 +1,1695 @@ +/* + * Copyright (C) 2012 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 static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.CFG_DEVICE_STATE; +import static com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration.MASK_ALL; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils.SMALL_SHADOW_SIZE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreview.LARGE_SHADOWS; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.CUSTOM; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.NONE; +import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode.SCREENS; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.api.Rect; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.DeviceConfigHelper; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationClient; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Locale; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.NestedConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.VaryingConfiguration; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.IncludeFinder.Reference; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.resources.Density; +import com.android.resources.ScreenSize; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.State; +import com.google.common.collect.Lists; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.ui.IWorkbenchPartSite; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.ide.IDE; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * Manager for the configuration previews, which handles layout computations, + * managing the image buffer cache, etc + */ +public class RenderPreviewManager { + private static double sScale = 1.0; + private static final int RENDER_DELAY = 150; + private static final int PREVIEW_VGAP = 18; + private static final int PREVIEW_HGAP = 12; + private static final int MAX_WIDTH = 200; + private static final int MAX_HEIGHT = MAX_WIDTH; + private static final int ZOOM_ICON_WIDTH = 16; + private static final int ZOOM_ICON_HEIGHT = 16; + private @Nullable List<RenderPreview> mPreviews; + private @Nullable RenderPreviewList mManualList; + private final @NonNull LayoutCanvas mCanvas; + private final @NonNull CanvasTransform mVScale; + private final @NonNull CanvasTransform mHScale; + private int mPrevCanvasWidth; + private int mPrevCanvasHeight; + private int mPrevImageWidth; + private int mPrevImageHeight; + private @NonNull RenderPreviewMode mMode = NONE; + private @Nullable RenderPreview mActivePreview; + private @Nullable ScrollBarListener mListener; + private int mLayoutHeight; + /** Last seen state revision in this {@link RenderPreviewManager}. If less + * than {@link #sRevision}, the previews need to be updated on next exposure */ + private static int mRevision; + /** Current global revision count */ + private static int sRevision; + private boolean mNeedLayout; + private boolean mNeedRender; + private boolean mNeedZoom; + private SwapAnimation mAnimation; + + /** + * Creates a {@link RenderPreviewManager} associated with the given canvas + * + * @param canvas the canvas to manage previews for + */ + public RenderPreviewManager(@NonNull LayoutCanvas canvas) { + mCanvas = canvas; + mHScale = canvas.getHorizontalTransform(); + mVScale = canvas.getVerticalTransform(); + } + + /** + * Revise the global state revision counter. This will cause all layout + * preview managers to refresh themselves to the latest revision when they + * are next exposed. + */ + public static void bumpRevision() { + sRevision++; + } + + /** + * Returns the associated chooser + * + * @return the associated chooser + */ + @NonNull + ConfigurationChooser getChooser() { + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + return editor.getConfigurationChooser(); + } + + /** + * Returns the associated canvas + * + * @return the canvas + */ + @NonNull + public LayoutCanvas getCanvas() { + return mCanvas; + } + + /** Zooms in (grows all previews) */ + public void zoomIn() { + sScale = sScale * (1 / 0.9); + if (Math.abs(sScale-1.0) < 0.0001) { + sScale = 1.0; + } + + updatedZoom(); + } + + /** Zooms out (shrinks all previews) */ + public void zoomOut() { + sScale = sScale * (0.9 / 1); + if (Math.abs(sScale-1.0) < 0.0001) { + sScale = 1.0; + } + updatedZoom(); + } + + /** Zooms to 100 (resets zoom) */ + public void zoomReset() { + sScale = 1.0; + updatedZoom(); + mNeedZoom = mNeedLayout = true; + mCanvas.redraw(); + } + + private void updatedZoom() { + if (hasPreviews()) { + for (RenderPreview preview : mPreviews) { + preview.disposeThumbnail(); + } + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + preview.disposeThumbnail(); + } + } + + mNeedLayout = mNeedRender = true; + mCanvas.redraw(); + } + + static int getMaxWidth() { + return (int) (sScale * MAX_WIDTH); + } + + static int getMaxHeight() { + return (int) (sScale * MAX_HEIGHT); + } + + static double getScale() { + return sScale; + } + + /** + * Returns whether there are any manual preview items (provided the current + * mode is manual previews + * + * @return true if there are items in the manual preview list + */ + public boolean hasManualPreviews() { + assert mMode == CUSTOM; + return mManualList != null && !mManualList.isEmpty(); + } + + /** Delete all the previews */ + public void deleteManualPreviews() { + disposePreviews(); + selectMode(NONE); + mCanvas.setFitScale(true /* onlyZoomOut */, true /*allowZoomIn*/); + + if (mManualList != null) { + mManualList.delete(); + } + } + + /** Dispose all the previews */ + public void disposePreviews() { + if (mPreviews != null) { + List<RenderPreview> old = mPreviews; + mPreviews = null; + for (RenderPreview preview : old) { + preview.dispose(); + } + } + } + + /** + * Deletes the given preview + * + * @param preview the preview to be deleted + */ + public void deletePreview(RenderPreview preview) { + mPreviews.remove(preview); + preview.dispose(); + layout(true); + mCanvas.redraw(); + + if (mManualList != null) { + mManualList.remove(preview); + saveList(); + } + } + + /** + * Compute the total width required for the previews, including internal padding + * + * @return total width in pixels + */ + public int computePreviewWidth() { + int maxPreviewWidth = 0; + if (hasPreviews()) { + for (RenderPreview preview : mPreviews) { + maxPreviewWidth = Math.max(maxPreviewWidth, preview.getWidth()); + } + + if (maxPreviewWidth > 0) { + maxPreviewWidth += 2 * PREVIEW_HGAP; // 2x for left and right side + maxPreviewWidth += LARGE_SHADOWS ? SHADOW_SIZE : SMALL_SHADOW_SIZE; + } + + return maxPreviewWidth; + } + + return 0; + } + + /** + * Layout Algorithm. This sets the {@link RenderPreview#getX()} and + * {@link RenderPreview#getY()} coordinates of all the previews. It also + * marks previews as visible or invisible via + * {@link RenderPreview#setVisible(boolean)} according to their position and + * the current visible view port in the layout canvas. Finally, it also sets + * the {@code mLayoutHeight} field, such that the scrollbars can compute the + * right scrolled area, and that scrolling can cause render refreshes on + * views that are made visible. + * <p> + * This is not a traditional bin packing problem, because the objects to be + * packaged do not have a fixed size; we can scale them up and down in order + * to provide an "optimal" size. + * <p> + * See http://en.wikipedia.org/wiki/Packing_problem See + * http://en.wikipedia.org/wiki/Bin_packing_problem + */ + void layout(boolean refresh) { + mNeedLayout = false; + + if (mPreviews == null || mPreviews.isEmpty()) { + return; + } + + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + + if (!refresh && + (scaledImageWidth == mPrevImageWidth + && scaledImageHeight == mPrevImageHeight + && clientArea.width == mPrevCanvasWidth + && clientArea.height == mPrevCanvasHeight)) { + // No change + return; + } + + mPrevImageWidth = scaledImageWidth; + mPrevImageHeight = scaledImageHeight; + mPrevCanvasWidth = clientArea.width; + mPrevCanvasHeight = clientArea.height; + + if (mListener == null) { + mListener = new ScrollBarListener(); + mCanvas.getVerticalBar().addSelectionListener(mListener); + } + + beginRenderScheduling(); + + mLayoutHeight = 0; + + if (previewsHaveIdenticalSize() || fixedOrder()) { + // If all the preview boxes are of identical sizes, or if the order is predetermined, + // just lay them out in rows. + rowLayout(); + } else if (previewsFit()) { + layoutFullFit(); + } else { + rowLayout(); + } + + mCanvas.updateScrollBars(); + } + + /** + * Performs a simple layout where the views are laid out in a row, wrapping + * around the top left canvas image. + */ + private void rowLayout() { + // TODO: Separate layout heuristics for portrait and landscape orientations (though + // it also depends on the dimensions of the canvas window, which determines the + // shape of the leftover space) + + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int maxVisibleY = clientArea.y + clientArea.height; + + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + int nextY = 0; + + // First lay out images across the top right hand side + int x = rightHandSide; + int y = 0; + boolean wrapped = false; + + int vgap = PREVIEW_VGAP; + for (RenderPreview preview : mPreviews) { + // If we have forked previews, double the vgap to allow space for two labels + if (preview.isForked()) { + vgap *= 2; + break; + } + } + + List<RenderPreview> aspectOrder; + if (!fixedOrder()) { + aspectOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); + } else { + aspectOrder = mPreviews; + } + + for (RenderPreview preview : aspectOrder) { + if (x > 0 && x + preview.getWidth() > availableWidth) { + x = rightHandSide; + int prevY = y; + y = nextY; + if ((prevY <= bottomBorder || + y <= bottomBorder) + && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { + // If there's really no visible room below, don't bother + // Similarly, don't wrap individually scaled views + if (bottomBorder < availableHeight - 40 && preview.getScale() < 1.2) { + // If it's closer to the top row than the bottom, just + // mark the next row for left justify instead + if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { + rightHandSide = 0; + wrapped = true; + } else if (!wrapped) { + y = nextY = Math.max(nextY, bottomBorder + vgap); + x = rightHandSide = 0; + wrapped = true; + } + } + } + } + if (x > 0 && y <= bottomBorder + && Math.max(nextY, y + preview.getHeight()) > bottomBorder) { + if (clientArea.height - bottomBorder < preview.getHeight()) { + // No room below the device on the left; just continue on the + // bottom row + } else if (preview.getScale() < 1.2) { + if (bottomBorder - y > y + preview.getHeight() - bottomBorder) { + rightHandSide = 0; + wrapped = true; + } else { + y = nextY = Math.max(nextY, bottomBorder + vgap); + x = rightHandSide = 0; + wrapped = true; + } + } + } + + preview.setPosition(x, y); + + if (y > maxVisibleY && maxVisibleY > 0) { + preview.setVisible(false); + } else if (!preview.isVisible()) { + preview.setVisible(true); + } + + x += preview.getWidth(); + x += PREVIEW_HGAP; + nextY = Math.max(nextY, y + preview.getHeight() + vgap); + } + + mLayoutHeight = nextY; + } + + private boolean fixedOrder() { + return mMode == SCREENS; + } + + /** Returns true if all the previews have the same identical size */ + private boolean previewsHaveIdenticalSize() { + if (!hasPreviews()) { + return true; + } + + Iterator<RenderPreview> iterator = mPreviews.iterator(); + RenderPreview first = iterator.next(); + int width = first.getWidth(); + int height = first.getHeight(); + + while (iterator.hasNext()) { + RenderPreview preview = iterator.next(); + if (width != preview.getWidth() || height != preview.getHeight()) { + return false; + } + } + + return true; + } + + /** Returns true if all the previews can fully fit in the available space */ + private boolean previewsFit() { + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + + // First see if we can fit everything; if so, we can try to make the layouts + // larger such that they fill up all the available space + long availableArea = rightHandSide * bottomBorder + + availableWidth * (Math.max(0, availableHeight - bottomBorder)); + + long requiredArea = 0; + for (RenderPreview preview : mPreviews) { + // Note: This does not include individual preview scale; the layout + // algorithm itself may be tweaking the scales to fit elements within + // the layout + requiredArea += preview.getArea(); + } + + return requiredArea * sScale < availableArea; + } + + private void layoutFullFit() { + int scaledImageWidth = mHScale.getScaledImgSize(); + int scaledImageHeight = mVScale.getScaledImgSize(); + Rectangle clientArea = mCanvas.getClientArea(); + int availableWidth = clientArea.x + clientArea.width - getX(); + int availableHeight = clientArea.y + clientArea.height - getY(); + int maxVisibleY = clientArea.y + clientArea.height; + int bottomBorder = scaledImageHeight; + int rightHandSide = scaledImageWidth + PREVIEW_HGAP; + + int minWidth = Integer.MAX_VALUE; + int minHeight = Integer.MAX_VALUE; + for (RenderPreview preview : mPreviews) { + minWidth = Math.min(minWidth, preview.getWidth()); + minHeight = Math.min(minHeight, preview.getHeight()); + } + + BinPacker packer = new BinPacker(minWidth, minHeight); + + // TODO: Instead of this, just start with client area and occupy scaled image size! + + // Add in gap on right and bottom since we'll add that requirement on the width and + // height rectangles too (for spacing) + packer.addSpace(new Rect(rightHandSide, 0, + availableWidth - rightHandSide + PREVIEW_HGAP, + availableHeight + PREVIEW_VGAP)); + if (maxVisibleY > bottomBorder) { + packer.addSpace(new Rect(0, bottomBorder + PREVIEW_VGAP, + availableWidth + PREVIEW_HGAP, maxVisibleY - bottomBorder + PREVIEW_VGAP)); + } + + // TODO: Sort previews first before attempting to position them? + + ArrayList<RenderPreview> aspectOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(aspectOrder, RenderPreview.INCREASING_ASPECT_RATIO); + + for (RenderPreview preview : aspectOrder) { + int previewWidth = preview.getWidth(); + int previewHeight = preview.getHeight(); + previewHeight += PREVIEW_VGAP; + if (preview.isForked()) { + previewHeight += PREVIEW_VGAP; + } + previewWidth += PREVIEW_HGAP; + // title height? how do I account for that? + Rect position = packer.occupy(previewWidth, previewHeight); + if (position != null) { + preview.setPosition(position.x, position.y); + preview.setVisible(true); + } else { + // Can't fit: give up and do plain row layout + rowLayout(); + return; + } + } + + mLayoutHeight = availableHeight; + } + /** + * Paints the configuration previews + * + * @param gc the graphics context to paint into + */ + void paint(GC gc) { + if (hasPreviews()) { + // Ensure up to date at all times; consider moving if it's too expensive + layout(mNeedLayout); + if (mNeedRender) { + renderPreviews(); + } + if (mNeedZoom) { + boolean allowZoomIn = true /*mMode == NONE*/; + mCanvas.setFitScale(false /*onlyZoomOut*/, allowZoomIn); + mNeedZoom = false; + } + int rootX = getX(); + int rootY = getY(); + + for (RenderPreview preview : mPreviews) { + if (preview.isVisible()) { + int x = rootX + preview.getX(); + int y = rootY + preview.getY(); + preview.paint(gc, x, y); + } + } + + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + String displayName = null; + Configuration configuration = preview.getConfiguration(); + if (configuration instanceof VaryingConfiguration) { + // Use override flags from stashed preview, but configuration + // data from live (not varying) configured configuration + VaryingConfiguration cfg = (VaryingConfiguration) configuration; + int flags = cfg.getAlternateFlags() | cfg.getOverrideFlags(); + displayName = NestedConfiguration.computeDisplayName(flags, + getChooser().getConfiguration()); + } else if (configuration instanceof NestedConfiguration) { + int flags = ((NestedConfiguration) configuration).getOverrideFlags(); + displayName = NestedConfiguration.computeDisplayName(flags, + getChooser().getConfiguration()); + } else { + displayName = configuration.getDisplayName(); + } + if (displayName != null) { + CanvasTransform hi = mHScale; + CanvasTransform vi = mVScale; + + int destX = hi.translate(0); + int destY = vi.translate(0); + int destWidth = hi.getScaledImgSize(); + int destHeight = vi.getScaledImgSize(); + + int x = destX + destWidth / 2 - preview.getWidth() / 2; + int y = destY + destHeight; + + preview.paintTitle(gc, x, y, false /*showFile*/, displayName); + } + } + + // Zoom overlay + int x = getZoomX(); + if (x > 0) { + int y = getZoomY(); + int oldAlpha = gc.getAlpha(); + + // Paint background oval rectangle behind the zoom and close icons + gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); + gc.setAlpha(128); + int padding = 3; + int arc = 5; + gc.fillRoundRectangle(x - padding, y - padding, + ZOOM_ICON_WIDTH + 2 * padding, + 4 * ZOOM_ICON_HEIGHT + 2 * padding, arc, arc); + + gc.setAlpha(255); + IconFactory iconFactory = IconFactory.getInstance(); + Image zoomOut = iconFactory.getIcon("zoomminus"); //$NON-NLS-1$); + Image zoomIn = iconFactory.getIcon("zoomplus"); //$NON-NLS-1$); + Image zoom100 = iconFactory.getIcon("zoom100"); //$NON-NLS-1$); + Image close = iconFactory.getIcon("close"); //$NON-NLS-1$); + + gc.drawImage(zoomIn, x, y); + y += ZOOM_ICON_HEIGHT; + gc.drawImage(zoomOut, x, y); + y += ZOOM_ICON_HEIGHT; + gc.drawImage(zoom100, x, y); + y += ZOOM_ICON_HEIGHT; + gc.drawImage(close, x, y); + y += ZOOM_ICON_HEIGHT; + gc.setAlpha(oldAlpha); + } + } else if (mMode == CUSTOM) { + int rootX = getX(); + rootX += mHScale.getScaledImgSize(); + rootX += 2 * PREVIEW_HGAP; + int rootY = getY(); + rootY += 20; + gc.setFont(mCanvas.getFont()); + gc.setForeground(mCanvas.getDisplay().getSystemColor(SWT.COLOR_BLACK)); + gc.drawText("Add previews with \"Add as Thumbnail\"\nin the configuration menu", + rootX, rootY, true); + } + + if (mAnimation != null) { + mAnimation.tick(gc); + } + } + + private void addPreview(@NonNull RenderPreview preview) { + if (mPreviews == null) { + mPreviews = Lists.newArrayList(); + } + mPreviews.add(preview); + } + + /** Adds the current configuration as a new configuration preview */ + public void addAsThumbnail() { + ConfigurationChooser chooser = getChooser(); + String name = chooser.getConfiguration().getDisplayName(); + if (name == null || name.isEmpty()) { + name = getUniqueName(); + } + InputDialog d = new InputDialog( + AdtPlugin.getShell(), + "Add as Thumbnail Preview", // title + "Name of thumbnail:", + name, + null); + if (d.open() == Window.OK) { + selectMode(CUSTOM); + + String newName = d.getValue(); + // Create a new configuration from the current settings in the composite + Configuration configuration = Configuration.copy(chooser.getConfiguration()); + configuration.setDisplayName(newName); + + RenderPreview preview = RenderPreview.create(this, configuration); + addPreview(preview); + + layout(true); + beginRenderScheduling(); + scheduleRender(preview); + mCanvas.setFitScale(true /* onlyZoomOut */, false /*allowZoomIn*/); + + if (mManualList == null) { + loadList(); + } + if (mManualList != null) { + mManualList.add(preview); + saveList(); + } + } + } + + /** + * Computes a unique new name for a configuration preview that represents + * the current, default configuration + * + * @return a unique name + */ + private String getUniqueName() { + if (mPreviews == null || mPreviews.isEmpty()) { + // NO, not for the first preview! + return "Config1"; + } + + Set<String> names = new HashSet<String>(mPreviews.size()); + for (RenderPreview preview : mPreviews) { + names.add(preview.getDisplayName()); + } + + int index = 2; + while (true) { + String name = String.format("Config%1$d", index); + if (!names.contains(name)) { + return name; + } + index++; + } + } + + /** Generates a bunch of default configuration preview thumbnails */ + public void addDefaultPreviews() { + ConfigurationChooser chooser = getChooser(); + Configuration parent = chooser.getConfiguration(); + if (parent instanceof NestedConfiguration) { + parent = ((NestedConfiguration) parent).getParent(); + } + if (mCanvas.getImageOverlay().getImage() != null) { + // Create Language variation + createLocaleVariation(chooser, parent); + + // Vary screen size + // TODO: Be smarter here: Pick a screen that is both as differently as possible + // from the current screen as well as also supported. So consider + // things like supported screens, targetSdk etc. + createScreenVariations(parent); + + // Vary orientation + createStateVariation(chooser, parent); + + // Vary render target + createRenderTargetVariation(chooser, parent); + } + + // Also add in include-context previews, if any + addIncludedInPreviews(); + + // Make a placeholder preview for the current screen, in case we switch from it + RenderPreview preview = RenderPreview.create(this, parent); + mCanvas.setPreview(preview); + + sortPreviewsByOrientation(); + } + + private void createRenderTargetVariation(ConfigurationChooser chooser, Configuration parent) { + /* This is disabled for now: need to load multiple versions of layoutlib. + When I did this, there seemed to be some drug interactions between + them, and I would end up with NPEs in layoutlib code which normally works. + VaryingConfiguration configuration = + VaryingConfiguration.create(chooser, parent); + configuration.setAlternatingTarget(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + */ + } + + private void createStateVariation(ConfigurationChooser chooser, Configuration parent) { + State currentState = parent.getDeviceState(); + State nextState = parent.getNextDeviceState(currentState); + if (nextState != currentState) { + VaryingConfiguration configuration = + VaryingConfiguration.create(chooser, parent); + configuration.setAlternateDeviceState(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + } + } + + private void createLocaleVariation(ConfigurationChooser chooser, Configuration parent) { + LanguageQualifier currentLanguage = parent.getLocale().language; + for (Locale locale : chooser.getLocaleList()) { + LanguageQualifier language = locale.language; + if (!language.equals(currentLanguage)) { + VaryingConfiguration configuration = + VaryingConfiguration.create(chooser, parent); + configuration.setAlternateLocale(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + break; + } + } + } + + private void createScreenVariations(Configuration parent) { + ConfigurationChooser chooser = getChooser(); + VaryingConfiguration configuration; + + configuration = VaryingConfiguration.create(chooser, parent); + configuration.setVariation(0); + configuration.setAlternateDevice(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + + configuration = VaryingConfiguration.create(chooser, parent); + configuration.setVariation(1); + configuration.setAlternateDevice(true); + configuration.syncFolderConfig(); + addPreview(RenderPreview.create(this, configuration)); + } + + /** + * Returns the current mode as seen by this {@link RenderPreviewManager}. + * Note that it may not yet have been synced with the global mode kept in + * {@link AdtPrefs#getRenderPreviewMode()}. + * + * @return the current preview mode + */ + @NonNull + public RenderPreviewMode getMode() { + return mMode; + } + + /** + * Update the set of previews for the current mode + * + * @param force force a refresh even if the preview type has not changed + * @return true if the views were recomputed, false if the previews were + * already showing and the mode not changed + */ + public boolean recomputePreviews(boolean force) { + RenderPreviewMode newMode = AdtPrefs.getPrefs().getRenderPreviewMode(); + if (newMode == mMode && !force + && (mRevision == sRevision + || mMode == NONE + || mMode == CUSTOM)) { + return false; + } + + RenderPreviewMode oldMode = mMode; + mMode = newMode; + mRevision = sRevision; + + sScale = 1.0; + disposePreviews(); + + switch (mMode) { + case DEFAULT: + addDefaultPreviews(); + break; + case INCLUDES: + addIncludedInPreviews(); + break; + case LOCALES: + addLocalePreviews(); + break; + case SCREENS: + addScreenSizePreviews(); + break; + case VARIATIONS: + addVariationPreviews(); + break; + case CUSTOM: + addManualPreviews(); + break; + case NONE: + // Can't just set mNeedZoom because with no previews, the paint + // method does nothing + mCanvas.setFitScale(false /*onlyZoomOut*/, true /*allowZoomIn*/); + break; + default: + assert false : mMode; + } + + // We schedule layout for the next redraw rather than process it here immediately; + // not only does this let us avoid doing work for windows where the tab is in the + // background, but when a file is opened we may not know the size of the canvas + // yet, and the layout methods need it in order to do a good job. By the time + // the canvas is painted, we have accurate bounds. + mNeedLayout = mNeedRender = true; + mCanvas.redraw(); + + if (oldMode != mMode && (oldMode == NONE || mMode == NONE)) { + // If entering or exiting preview mode: updating padding which is compressed + // only in preview mode. + mCanvas.getHorizontalTransform().refresh(); + mCanvas.getVerticalTransform().refresh(); + } + + return true; + } + + /** + * Sets the new render preview mode to use + * + * @param mode the new mode + */ + public void selectMode(@NonNull RenderPreviewMode mode) { + if (mode != mMode) { + AdtPrefs.getPrefs().setPreviewMode(mode); + recomputePreviews(false); + } + } + + /** Similar to {@link #addDefaultPreviews()} but for locales */ + public void addLocalePreviews() { + + ConfigurationChooser chooser = getChooser(); + List<Locale> locales = chooser.getLocaleList(); + Configuration parent = chooser.getConfiguration(); + + for (Locale locale : locales) { + if (!locale.hasLanguage() && !locale.hasRegion()) { + continue; + } + NestedConfiguration configuration = NestedConfiguration.create(chooser, parent); + configuration.setOverrideLocale(true); + configuration.setLocale(locale, false); + + String displayName = ConfigurationChooser.getLocaleLabel(chooser, locale, false); + assert displayName != null; // it's never non null when locale is non null + configuration.setDisplayName(displayName); + + addPreview(RenderPreview.create(this, configuration)); + } + + // Make a placeholder preview for the current screen, in case we switch from it + Configuration configuration = parent; + Locale locale = configuration.getLocale(); + String label = ConfigurationChooser.getLocaleLabel(chooser, locale, false); + if (label == null) { + label = "default"; + } + configuration.setDisplayName(label); + RenderPreview preview = RenderPreview.create(this, parent); + if (preview != null) { + mCanvas.setPreview(preview); + } + + // No need to sort: they should all be identical + } + + /** Similar to {@link #addDefaultPreviews()} but for screen sizes */ + public void addScreenSizePreviews() { + ConfigurationChooser chooser = getChooser(); + List<Device> devices = chooser.getDeviceList(); + Configuration configuration = chooser.getConfiguration(); + boolean canScaleNinePatch = configuration.supports(Capability.FIXED_SCALABLE_NINE_PATCH); + + // Rearrange the devices a bit such that the most interesting devices bubble + // to the front + // 10" tablet, 7" tablet, reference phones, tiny phone, and in general the first + // version of each seen screen size + List<Device> sorted = new ArrayList<Device>(devices); + Set<ScreenSize> seenSizes = new HashSet<ScreenSize>(); + State currentState = configuration.getDeviceState(); + String currentStateName = currentState != null ? currentState.getName() : ""; + + for (int i = 0, n = sorted.size(); i < n; i++) { + Device device = sorted.get(i); + boolean interesting = false; + + State state = device.getState(currentStateName); + if (state == null) { + state = device.getAllStates().get(0); + } + + if (device.getName().startsWith("Nexus ") //$NON-NLS-1$ + || device.getName().endsWith(" Nexus")) { //$NON-NLS-1$ + // Not String#contains("Nexus") because that would also pick up all the generic + // entries ("3.7in WVGA (Nexus One)") so we'd have them duplicated + interesting = true; + } + + FolderConfiguration c = DeviceConfigHelper.getFolderConfig(state); + if (c != null) { + ScreenSizeQualifier sizeQualifier = c.getScreenSizeQualifier(); + if (sizeQualifier != null) { + ScreenSize size = sizeQualifier.getValue(); + if (!seenSizes.contains(size)) { + seenSizes.add(size); + interesting = true; + } + } + + // Omit LDPI, not really used anymore + DensityQualifier density = c.getDensityQualifier(); + if (density != null) { + Density d = density.getValue(); + if (d == Density.LOW) { + interesting = false; + } + + if (!canScaleNinePatch && d == Density.TV) { + interesting = false; + } + } + } + + if (interesting) { + NestedConfiguration screenConfig = NestedConfiguration.create(chooser, + configuration); + screenConfig.setOverrideDevice(true); + screenConfig.setDevice(device, true); + screenConfig.syncFolderConfig(); + screenConfig.setDisplayName(ConfigurationChooser.getDeviceLabel(device, true)); + addPreview(RenderPreview.create(this, screenConfig)); + } + } + + // Sorted by screen size, in decreasing order + sortPreviewsByScreenSize(); + } + + /** + * Previews this layout as included in other layouts + */ + public void addIncludedInPreviews() { + ConfigurationChooser chooser = getChooser(); + IProject project = chooser.getProject(); + if (project == null) { + return; + } + IncludeFinder finder = IncludeFinder.get(project); + + final List<Reference> includedBy = finder.getIncludedBy(chooser.getEditedFile()); + + if (includedBy == null || includedBy.isEmpty()) { + // TODO: Generate some useful defaults, such as including it in a ListView + // as the list item layout? + return; + } + + for (final Reference reference : includedBy) { + String title = reference.getDisplayName(); + Configuration config = Configuration.create(chooser.getConfiguration(), + reference.getFile()); + RenderPreview preview = RenderPreview.create(this, config); + preview.setDisplayName(title); + preview.setIncludedWithin(reference); + + addPreview(preview); + } + + sortPreviewsByOrientation(); + } + + /** + * Previews this layout as included in other layouts + */ + public void addVariationPreviews() { + ConfigurationChooser chooser = getChooser(); + + IFile file = chooser.getEditedFile(); + List<IFile> variations = AdtUtils.getResourceVariations(file, false /*includeSelf*/); + + // Sort by parent folder + Collections.sort(variations, new Comparator<IFile>() { + @Override + public int compare(IFile file1, IFile file2) { + return file1.getParent().getName().compareTo(file2.getParent().getName()); + } + }); + + Configuration currentConfig = chooser.getConfiguration(); + + for (IFile variation : variations) { + String title = variation.getParent().getName(); + Configuration config = Configuration.create(chooser.getConfiguration(), variation); + config.setTheme(currentConfig.getTheme()); + config.setActivity(currentConfig.getActivity()); + RenderPreview preview = RenderPreview.create(this, config); + preview.setDisplayName(title); + preview.setAlternateInput(variation); + + addPreview(preview); + } + + sortPreviewsByOrientation(); + } + + /** + * Previews this layout using a custom configured set of layouts + */ + public void addManualPreviews() { + if (mManualList == null) { + loadList(); + } else { + mPreviews = mManualList.createPreviews(mCanvas); + } + } + + private void loadList() { + IProject project = getChooser().getProject(); + if (project == null) { + return; + } + + if (mManualList == null) { + mManualList = RenderPreviewList.get(project); + } + + try { + mManualList.load(getChooser().getDeviceList()); + mPreviews = mManualList.createPreviews(mCanvas); + } catch (IOException e) { + AdtPlugin.log(e, null); + } + } + + private void saveList() { + if (mManualList != null) { + try { + mManualList.save(); + } catch (IOException e) { + AdtPlugin.log(e, null); + } + } + } + + void rename(ConfigurationDescription description, String newName) { + IProject project = getChooser().getProject(); + if (project == null) { + return; + } + + if (mManualList == null) { + mManualList = RenderPreviewList.get(project); + } + description.displayName = newName; + saveList(); + } + + + /** + * Notifies that the main configuration has changed. + * + * @param flags the change flags, a bitmask corresponding to the + * {@code CHANGE_} constants in {@link ConfigurationClient} + */ + public void configurationChanged(int flags) { + // Similar to renderPreviews, but only acts on incomplete previews + if (hasPreviews()) { + // Do zoomed images first + beginRenderScheduling(); + for (RenderPreview preview : mPreviews) { + if (preview.getScale() > 1.2) { + preview.configurationChanged(flags); + } + } + for (RenderPreview preview : mPreviews) { + if (preview.getScale() <= 1.2) { + preview.configurationChanged(flags); + } + } + RenderPreview preview = mCanvas.getPreview(); + if (preview != null) { + preview.configurationChanged(flags); + preview.dispose(); + } + mNeedLayout = true; + mCanvas.redraw(); + } + } + + /** Updates the configuration preview thumbnails */ + public void renderPreviews() { + if (hasPreviews()) { + beginRenderScheduling(); + + // Process in visual order + ArrayList<RenderPreview> visualOrder = new ArrayList<RenderPreview>(mPreviews); + Collections.sort(visualOrder, RenderPreview.VISUAL_ORDER); + + // Do zoomed images first + for (RenderPreview preview : visualOrder) { + if (preview.getScale() > 1.2 && preview.isVisible()) { + scheduleRender(preview); + } + } + // Non-zoomed images + for (RenderPreview preview : visualOrder) { + if (preview.getScale() <= 1.2 && preview.isVisible()) { + scheduleRender(preview); + } + } + } + + mNeedRender = false; + } + + private int mPendingRenderCount; + + /** + * Reset rendering scheduling. The next render request will be scheduled + * after a single delay unit. + */ + public void beginRenderScheduling() { + mPendingRenderCount = 0; + } + + /** + * Schedule rendering the given preview. Each successive call will add an additional + * delay unit to the schedule from the previous {@link #scheduleRender(RenderPreview)} + * call, until {@link #beginRenderScheduling()} is called again. + * + * @param preview the preview to render + */ + public void scheduleRender(@NonNull RenderPreview preview) { + mPendingRenderCount++; + preview.render(mPendingRenderCount * RENDER_DELAY); + } + + /** + * Switch to the given configuration preview + * + * @param preview the preview to switch to + */ + public void switchTo(@NonNull RenderPreview preview) { + IFile input = preview.getAlternateInput(); + if (input != null) { + IWorkbenchPartSite site = mCanvas.getEditorDelegate().getEditor().getSite(); + try { + // This switches to the given file, but the file might not have + // an identical configuration to what was shown in the preview. + // For example, while viewing a 10" layout-xlarge file, it might + // show a preview for a 5" version tied to the default layout. If + // you click on it, it will open the default layout file, but it might + // be using a different screen size; any of those that match the + // default layout, say a 3.8". + // + // Thus, we need to also perform a screen size sync first + Configuration configuration = preview.getConfiguration(); + boolean setSize = false; + if (configuration instanceof NestedConfiguration) { + NestedConfiguration nestedConfig = (NestedConfiguration) configuration; + setSize = nestedConfig.isOverridingDevice(); + if (configuration instanceof VaryingConfiguration) { + VaryingConfiguration c = (VaryingConfiguration) configuration; + setSize |= c.isAlternatingDevice(); + } + + if (setSize) { + ConfigurationChooser chooser = getChooser(); + IFile editedFile = chooser.getEditedFile(); + if (editedFile != null) { + chooser.syncToVariations(CFG_DEVICE|CFG_DEVICE_STATE, + editedFile, configuration, false, false); + } + } + } + + IDE.openEditor(site.getWorkbenchWindow().getActivePage(), input, + CommonXmlEditor.ID); + } catch (PartInitException e) { + AdtPlugin.log(e, null); + } + return; + } + + GraphicalEditorPart editor = mCanvas.getEditorDelegate().getGraphicalEditor(); + ConfigurationChooser chooser = editor.getConfigurationChooser(); + + Configuration originalConfiguration = chooser.getConfiguration(); + + // The new configuration is the configuration which will become the configuration + // in the layout editor's chooser + Configuration previewConfiguration = preview.getConfiguration(); + Configuration newConfiguration = previewConfiguration; + if (newConfiguration instanceof NestedConfiguration) { + // Should never use a complementing configuration for the main + // rendering's configuration; instead, create a new configuration + // with a snapshot of the configuration's current values + newConfiguration = Configuration.copy(previewConfiguration); + + // Remap all the previews to be parented to this new copy instead + // of the old one (which is no longer controlled by the chooser) + for (RenderPreview p : mPreviews) { + Configuration configuration = p.getConfiguration(); + if (configuration instanceof NestedConfiguration) { + NestedConfiguration nested = (NestedConfiguration) configuration; + nested.setParent(newConfiguration); + } + } + } + + // Make a preview for the configuration which *was* showing in the + // chooser up until this point: + RenderPreview newPreview = mCanvas.getPreview(); + if (newPreview == null) { + newPreview = RenderPreview.create(this, originalConfiguration); + } + + // Update its configuration such that it is complementing or inheriting + // from the new chosen configuration + if (previewConfiguration instanceof VaryingConfiguration) { + VaryingConfiguration varying = VaryingConfiguration.create( + (VaryingConfiguration) previewConfiguration, + newConfiguration); + varying.updateDisplayName(); + originalConfiguration = varying; + newPreview.setConfiguration(originalConfiguration); + } else if (previewConfiguration instanceof NestedConfiguration) { + NestedConfiguration nested = NestedConfiguration.create( + (NestedConfiguration) previewConfiguration, + originalConfiguration, + newConfiguration); + nested.setDisplayName(nested.computeDisplayName()); + originalConfiguration = nested; + newPreview.setConfiguration(originalConfiguration); + } + + // Replace clicked preview with preview of the formerly edited main configuration + // This doesn't work yet because the image overlay has had its image + // replaced by the configuration previews! I should make a list of them + //newPreview.setFullImage(mImageOverlay.getAwtImage()); + for (int i = 0, n = mPreviews.size(); i < n; i++) { + if (preview == mPreviews.get(i)) { + mPreviews.set(i, newPreview); + break; + } + } + + // Stash the corresponding preview (not active) on the canvas so we can + // retrieve it if clicking to some other preview later + mCanvas.setPreview(preview); + preview.setVisible(false); + + // Switch to the configuration from the clicked preview (though it's + // most likely a copy, see above) + chooser.setConfiguration(newConfiguration); + editor.changed(MASK_ALL); + + // Scroll to the top again, if necessary + mCanvas.getVerticalBar().setSelection(mCanvas.getVerticalBar().getMinimum()); + + mNeedLayout = mNeedZoom = true; + mCanvas.redraw(); + mAnimation = new SwapAnimation(preview, newPreview); + } + + /** + * Gets the preview at the given location, or null if none. This is + * currently deeply tied to where things are painted in onPaint(). + */ + RenderPreview getPreview(ControlPoint mousePos) { + if (hasPreviews()) { + int rootX = getX(); + if (mousePos.x < rootX) { + return null; + } + int rootY = getY(); + + for (RenderPreview preview : mPreviews) { + int x = rootX + preview.getX(); + int y = rootY + preview.getY(); + if (mousePos.x >= x && mousePos.x <= x + preview.getWidth()) { + if (mousePos.y >= y && mousePos.y <= y + preview.getHeight()) { + return preview; + } + } + } + } + + return null; + } + + private int getX() { + return mHScale.translate(0); + } + + private int getY() { + return mVScale.translate(0); + } + + private int getZoomX() { + Rectangle clientArea = mCanvas.getClientArea(); + int x = clientArea.x + clientArea.width - ZOOM_ICON_WIDTH; + if (x < mHScale.getScaledImgSize() + PREVIEW_HGAP) { + // No visible previews because the main image is zoomed too far + return -1; + } + + return x - 6; + } + + private int getZoomY() { + Rectangle clientArea = mCanvas.getClientArea(); + return clientArea.y + 5; + } + + /** + * Returns the height of the layout + * + * @return the height + */ + public int getHeight() { + return mLayoutHeight; + } + + /** + * Notifies that preview manager that the mouse cursor has moved to the + * given control position within the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void moved(ControlPoint mousePos) { + RenderPreview hovered = getPreview(mousePos); + if (hovered != mActivePreview) { + if (mActivePreview != null) { + mActivePreview.setActive(false); + } + mActivePreview = hovered; + if (mActivePreview != null) { + mActivePreview.setActive(true); + } + mCanvas.redraw(); + } + } + + /** + * Notifies that preview manager that the mouse cursor has entered the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void enter(ControlPoint mousePos) { + moved(mousePos); + } + + /** + * Notifies that preview manager that the mouse cursor has exited the layout canvas + * + * @param mousePos the mouse position, relative to the layout canvas + */ + public void exit(ControlPoint mousePos) { + if (mActivePreview != null) { + mActivePreview.setActive(false); + } + mActivePreview = null; + mCanvas.redraw(); + } + + /** + * Process a mouse click, and return true if it was handled by this manager + * (e.g. the click was on a preview) + * + * @param mousePos the mouse position where the click occurred + * @return true if the click occurred over a preview and was handled, false otherwise + */ + public boolean click(ControlPoint mousePos) { + // Clicked zoom? + int x = getZoomX(); + if (x > 0) { + if (mousePos.x >= x && mousePos.x <= x + ZOOM_ICON_WIDTH) { + int y = getZoomY(); + if (mousePos.y >= y && mousePos.y <= y + 4 * ZOOM_ICON_HEIGHT) { + if (mousePos.y < y + ZOOM_ICON_HEIGHT) { + zoomIn(); + } else if (mousePos.y < y + 2 * ZOOM_ICON_HEIGHT) { + zoomOut(); + } else if (mousePos.y < y + 3 * ZOOM_ICON_HEIGHT) { + zoomReset(); + } else { + selectMode(NONE); + } + return true; + } + } + } + + RenderPreview preview = getPreview(mousePos); + if (preview != null) { + boolean handled = preview.click(mousePos.x - getX() - preview.getX(), + mousePos.y - getY() - preview.getY()); + if (handled) { + // In case layout was performed, there could be a new preview + // under this coordinate now, so make sure it's hover etc + // shows up + moved(mousePos); + return true; + } + } + + return false; + } + + /** + * Returns true if there are thumbnail previews + * + * @return true if thumbnails are being shown + */ + public boolean hasPreviews() { + return mPreviews != null && !mPreviews.isEmpty(); + } + + + private void sortPreviewsByScreenSize() { + if (mPreviews != null) { + Collections.sort(mPreviews, new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + Configuration config1 = preview1.getConfiguration(); + Configuration config2 = preview2.getConfiguration(); + Device device1 = config1.getDevice(); + Device device2 = config1.getDevice(); + if (device1 != null && device2 != null) { + Screen screen1 = device1.getDefaultHardware().getScreen(); + Screen screen2 = device2.getDefaultHardware().getScreen(); + if (screen1 != null && screen2 != null) { + double delta = screen1.getDiagonalLength() + - screen2.getDiagonalLength(); + if (delta != 0.0) { + return (int) Math.signum(delta); + } else { + if (screen1.getPixelDensity() != screen2.getPixelDensity()) { + return screen1.getPixelDensity().compareTo( + screen2.getPixelDensity()); + } + } + } + + } + State state1 = config1.getDeviceState(); + State state2 = config2.getDeviceState(); + if (state1 != state2 && state1 != null && state2 != null) { + return state1.getName().compareTo(state2.getName()); + } + + return preview1.getDisplayName().compareTo(preview2.getDisplayName()); + } + }); + } + } + + private void sortPreviewsByOrientation() { + if (mPreviews != null) { + Collections.sort(mPreviews, new Comparator<RenderPreview>() { + @Override + public int compare(RenderPreview preview1, RenderPreview preview2) { + Configuration config1 = preview1.getConfiguration(); + Configuration config2 = preview2.getConfiguration(); + State state1 = config1.getDeviceState(); + State state2 = config2.getDeviceState(); + if (state1 != state2 && state1 != null && state2 != null) { + return state1.getName().compareTo(state2.getName()); + } + + return preview1.getDisplayName().compareTo(preview2.getDisplayName()); + } + }); + } + } + + /** + * Vertical scrollbar listener which updates render previews which are not + * visible and triggers a redraw + */ + private class ScrollBarListener implements SelectionListener { + @Override + public void widgetSelected(SelectionEvent e) { + if (mPreviews == null) { + return; + } + + ScrollBar bar = mCanvas.getVerticalBar(); + int selection = bar.getSelection(); + int thumb = bar.getThumb(); + int maxY = selection + thumb; + beginRenderScheduling(); + for (RenderPreview preview : mPreviews) { + if (!preview.isVisible() && preview.getY() <= maxY) { + preview.setVisible(true); + } + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } + } + + /** Animation overlay shown briefly after swapping two previews */ + private class SwapAnimation implements Runnable { + private long begin; + private long end; + private static final long DURATION = 400; // ms + private Rect initialRect1; + private Rect targetRect1; + private Rect initialRect2; + private Rect targetRect2; + private RenderPreview preview; + + SwapAnimation(RenderPreview preview1, RenderPreview preview2) { + begin = System.currentTimeMillis(); + end = begin + DURATION; + + initialRect1 = new Rect(preview1.getX(), preview1.getY(), + preview1.getWidth(), preview1.getHeight()); + + CanvasTransform hi = mCanvas.getHorizontalTransform(); + CanvasTransform vi = mCanvas.getVerticalTransform(); + initialRect2 = new Rect(hi.translate(0), vi.translate(0), + hi.getScaledImgSize(), vi.getScaledImgSize()); + preview = preview2; + } + + void tick(GC gc) { + long now = System.currentTimeMillis(); + if (now > end || mCanvas.isDisposed()) { + mAnimation = null; + return; + } + + CanvasTransform hi = mCanvas.getHorizontalTransform(); + CanvasTransform vi = mCanvas.getVerticalTransform(); + if (targetRect1 == null) { + targetRect1 = new Rect(hi.translate(0), vi.translate(0), + hi.getScaledImgSize(), vi.getScaledImgSize()); + } + double portion = (now - begin) / (double) DURATION; + Rect rect1 = new Rect( + (int) (portion * (targetRect1.x - initialRect1.x) + initialRect1.x), + (int) (portion * (targetRect1.y - initialRect1.y) + initialRect1.y), + (int) (portion * (targetRect1.w - initialRect1.w) + initialRect1.w), + (int) (portion * (targetRect1.h - initialRect1.h) + initialRect1.h)); + + if (targetRect2 == null) { + targetRect2 = new Rect(preview.getX(), preview.getY(), + preview.getWidth(), preview.getHeight()); + } + portion = (now - begin) / (double) DURATION; + Rect rect2 = new Rect( + (int) (portion * (targetRect2.x - initialRect2.x) + initialRect2.x), + (int) (portion * (targetRect2.y - initialRect2.y) + initialRect2.y), + (int) (portion * (targetRect2.w - initialRect2.w) + initialRect2.w), + (int) (portion * (targetRect2.h - initialRect2.h) + initialRect2.h)); + + gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_GRAY)); + gc.drawRectangle(rect1.x, rect1.y, rect1.w, rect1.h); + gc.drawRectangle(rect2.x, rect2.y, rect2.w, rect2.h); + + mCanvas.getDisplay().timerExec(5, this); + } + + @Override + public void run() { + mCanvas.redraw(); + } + } + + /** + * Notifies the {@linkplain RenderPreviewManager} that the configuration used + * in the main chooser has been changed. This may require updating parent references + * in the preview configurations inheriting from it. + * + * @param oldConfiguration the previous configuration + * @param newConfiguration the new configuration in the chooser + */ + public void updateChooserConfig( + @NonNull Configuration oldConfiguration, + @NonNull Configuration newConfiguration) { + if (hasPreviews()) { + for (RenderPreview preview : mPreviews) { + Configuration configuration = preview.getConfiguration(); + if (configuration instanceof NestedConfiguration) { + NestedConfiguration nestedConfig = (NestedConfiguration) configuration; + if (nestedConfig.getParent() == oldConfiguration) { + nestedConfig.setParent(newConfiguration); + } + } + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java new file mode 100644 index 0000000..0f06d7f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderPreviewMode.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 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; + +/** + * The {@linkplain RenderPreviewMode} records what type of configurations to + * render in the layout editor + */ +public enum RenderPreviewMode { + /** Generate a set of default previews with maximum variation */ + DEFAULT, + + /** Preview all the locales */ + LOCALES, + + /** Preview all the screen sizes */ + SCREENS, + + /** Preview layout as included in other layouts */ + INCLUDES, + + /** Preview all the variations of this layout */ + VARIATIONS, + + /** Show a manually configured set of previews */ + CUSTOM, + + /** No previews */ + NONE; +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java index e0c3add..fdc5fed 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/RenderService.java @@ -17,11 +17,15 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; +import com.android.annotations.NonNull; import com.android.ide.common.api.IClientRulesEngine; import com.android.ide.common.api.INode; import com.android.ide.common.api.Rect; +import com.android.ide.common.rendering.HardwareConfigHelper; import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.rendering.api.DrawableParams; +import com.android.ide.common.rendering.api.HardwareConfig; import com.android.ide.common.rendering.api.IImageFactory; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.LayoutLog; @@ -32,10 +36,9 @@ import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.SessionParams.RenderingMode; import com.android.ide.common.rendering.api.ViewInfo; import com.android.ide.common.resources.ResourceResolver; -import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.ContextPullParser; -import com.android.ide.eclipse.adt.internal.editors.layout.ExplodedRenderingHelper; import com.android.ide.eclipse.adt.internal.editors.layout.ProjectCallback; import com.android.ide.eclipse.adt.internal.editors.layout.UiElementPullParser; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.Configuration; @@ -47,7 +50,12 @@ import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElement import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; -import com.android.resources.Density; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import org.eclipse.core.resources.IProject; import org.xmlpull.v1.XmlPullParser; @@ -55,8 +63,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.awt.image.BufferedImage; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.StringReader; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -81,18 +89,12 @@ public class RenderService { private final int mTargetSdkVersion; private final LayoutLibrary mLayoutLib; private final IImageFactory mImageFactory; - private final Density mDensity; - private final float mXdpi; - private final float mYdpi; - private final ScreenSizeQualifier mScreenSize; + private final HardwareConfigHelper mHardwareConfigHelper; // The following fields are optional or configurable using the various chained // setters: private UiDocumentNode mModel; - private int mWidth = -1; - private int mHeight = -1; - private boolean mUseExplodeMode; private Reference mIncludedWithin; private RenderingMode mRenderingMode = RenderingMode.NORMAL; private LayoutLog mLogger; @@ -109,10 +111,14 @@ public class RenderService { mImageFactory = canvas.getImageOverlay(); ConfigurationChooser chooser = editor.getConfigurationChooser(); Configuration config = chooser.getConfiguration(); - mDensity = config.getDensity(); - mXdpi = config.getXDpi(); - mYdpi = config.getYDpi(); - mScreenSize = chooser.getConfiguration().getFullConfig().getScreenSizeQualifier(); + FolderConfiguration folderConfig = config.getFullConfig(); + + Device device = config.getDevice(); + assert device != null; // Should only attempt render with configuration that has device + mHardwareConfigHelper = new HardwareConfigHelper(device); + mHardwareConfigHelper.setOrientation( + folderConfig.getScreenOrientationQualifier().getValue()); + mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/); mResourceResolver = editor.getResourceResolver(); mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib); @@ -120,6 +126,53 @@ public class RenderService { mTargetSdkVersion = editor.getTargetSdkVersion(); } + private RenderService(GraphicalEditorPart editor, + Configuration configuration, ResourceResolver resourceResolver) { + mEditor = editor; + + mProject = editor.getProject(); + LayoutCanvas canvas = editor.getCanvasControl(); + mImageFactory = canvas.getImageOverlay(); + FolderConfiguration folderConfig = configuration.getFullConfig(); + + Device device = configuration.getDevice(); + assert device != null; + mHardwareConfigHelper = new HardwareConfigHelper(device); + mHardwareConfigHelper.setOrientation( + folderConfig.getScreenOrientationQualifier().getValue()); + + mLayoutLib = editor.getReadyLayoutLib(true /*displayError*/); + mResourceResolver = resourceResolver != null ? resourceResolver : editor.getResourceResolver(); + mProjectCallback = editor.getProjectCallback(true /*reset*/, mLayoutLib); + mMinSdkVersion = editor.getMinSdkVersion(); + mTargetSdkVersion = editor.getTargetSdkVersion(); + } + + /** + * Returns true if this configuration supports the given rendering + * capability + * + * @param target the target to look up the layout library for + * @param capability the capability to check + * @return true if the capability is supported + */ + public static boolean supports( + @NonNull IAndroidTarget target, + @NonNull Capability capability) { + Sdk sdk = Sdk.getCurrent(); + if (sdk != null) { + AndroidTargetData targetData = sdk.getTargetData(target); + if (targetData != null) { + LayoutLibrary layoutLib = targetData.getLayoutLibrary(); + if (layoutLib != null) { + return layoutLib.supports(capability); + } + } + } + + return false; + } + /** * Creates a new {@link RenderService} associated with the given editor. * @@ -133,6 +186,21 @@ public class RenderService { } /** + * Creates a new {@link RenderService} associated with the given editor. + * + * @param editor the editor to provide configuration data such as the render target + * @param configuration the configuration to use (and fallback to editor for the rest) + * @param resolver a resource resolver to use to look up resources + * @return a {@link RenderService} which can perform rendering services + */ + public static RenderService create(GraphicalEditorPart editor, + Configuration configuration, ResourceResolver resolver) { + RenderService renderService = new RenderService(editor, configuration, resolver); + + return renderService; + } + + /** * Renders the given model, using this editor's theme and screen settings, and returns * the result as a {@link RenderSession}. * @@ -176,16 +244,34 @@ public class RenderService { } /** - * Sets the width and height to be used during rendering (which might be adjusted if + * Overrides the width and height to be used during rendering (which might be adjusted if * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}. * - * @param width the width in pixels of the layout to be rendered - * @param height the height in pixels of the layout to be rendered + * A value of -1 will make the rendering use the normal width and height coming from the + * {@link Configuration#getDevice()} object. + * + * @param overrideRenderWidth the width in pixels of the layout to be rendered + * @param overrideRenderHeight the height in pixels of the layout to be rendered * @return this (such that chains of setters can be stringed together) */ - public RenderService setSize(int width, int height) { - mWidth = width; - mHeight = height; + public RenderService setOverrideRenderSize(int overrideRenderWidth, int overrideRenderHeight) { + mHardwareConfigHelper.setOverrideRenderSize(overrideRenderWidth, overrideRenderHeight); + return this; + } + + /** + * Sets the max width and height to be used during rendering (which might be adjusted if + * the {@link #setRenderingMode(RenderingMode)} is {@link RenderingMode#FULL_EXPAND}. + * + * A value of -1 will make the rendering use the normal width and height coming from the + * {@link Configuration#getDevice()} object. + * + * @param maxRenderWidth the max width in pixels of the layout to be rendered + * @param maxRenderHeight the max height in pixels of the layout to be rendered + * @return this (such that chains of setters can be stringed together) + */ + public RenderService setMaxRenderSize(int maxRenderWidth, int maxRenderHeight) { + mHardwareConfigHelper.setMaxRenderSize(maxRenderWidth, maxRenderHeight); return this; } @@ -265,7 +351,7 @@ public class RenderService { * @return the {@link RenderSession} resulting from rendering the current model */ public RenderSession createRenderSession() { - assert mModel != null && mWidth != -1 && mHeight != -1 : "Incomplete service config"; + assert mModel != null : "Incomplete service config"; finishConfiguration(); if (mResourceResolver == null) { @@ -273,26 +359,10 @@ public class RenderService { return null; } - int width = mWidth; - int height = mHeight; - if (mUseExplodeMode) { - // compute how many padding in x and y will bump the screen size - List<UiElementNode> children = mModel.getUiChildren(); - if (children.size() == 1) { - ExplodedRenderingHelper helper = new ExplodedRenderingHelper( - children.get(0).getXmlNode(), mProject); - - // there are 2 paddings for each view - // left and right, or top and bottom. - int paddingValue = ExplodedRenderingHelper.PADDING_VALUE * 2; - - width += helper.getWidthPadding() * paddingValue; - height += helper.getHeightPadding() * paddingValue; - } - } + HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig(); UiElementPullParser modelParser = new UiElementPullParser(mModel, - mUseExplodeMode, mExpandNodes, mDensity, mXdpi, mProject); + false, mExpandNodes, hardwareConfig.getDensity(), mProject); ILayoutPullParser topParser = modelParser; // Code to support editing included layout @@ -316,11 +386,12 @@ public class RenderService { mProjectCallback.setLayoutParser(queryLayoutName, modelParser); topParser = new ContextPullParser(mProjectCallback, layoutFile); topParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - topParser.setInput(new FileInputStream(layoutFile), "UTF-8"); //$NON-NLS-1$ + String xmlText = Files.toString(layoutFile, Charsets.UTF_8); + topParser.setInput(new StringReader(xmlText)); + } catch (IOException e) { + AdtPlugin.log(e, null); } catch (XmlPullParserException e) { - AdtPlugin.log(e, ""); //$NON-NLS-1$ - } catch (FileNotFoundException e) { - // this will not happen since we check above. + AdtPlugin.log(e, null); } } } @@ -330,8 +401,7 @@ public class RenderService { topParser, mRenderingMode, mProject /* projectKey */, - width, height, - mDensity, mXdpi, mYdpi, + hardwareConfig, mResourceResolver, mProjectCallback, mMinSdkVersion, @@ -357,10 +427,6 @@ public class RenderService { } } - if (mScreenSize != null) { - params.setConfigScreenSize(mScreenSize.getValue()); - } - if (mOverrideBgColor != null) { params.setOverrideBgColor(mOverrideBgColor.intValue()); } @@ -396,8 +462,10 @@ public class RenderService { finishConfiguration(); - DrawableParams params = new DrawableParams(drawableResourceValue, mProject, mWidth, mHeight, - mDensity, mXdpi, mYdpi, mResourceResolver, mProjectCallback, mMinSdkVersion, + HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig(); + + DrawableParams params = new DrawableParams(drawableResourceValue, mProject, hardwareConfig, + mResourceResolver, mProjectCallback, mMinSdkVersion, mTargetSdkVersion, mLogger); params.setForceNoDecor(); Result result = mLayoutLib.renderDrawable(params); @@ -422,14 +490,13 @@ public class RenderService { public Map<INode, Rect> measureChildren(INode parent, final IClientRulesEngine.AttributeFilter filter) { finishConfiguration(); - - int width = parent.getBounds().w; - int height = parent.getBounds().h; + HardwareConfig hardwareConfig = mHardwareConfigHelper.getConfig(); final NodeFactory mNodeFactory = mEditor.getCanvasControl().getNodeFactory(); UiElementNode parentNode = ((NodeProxy) parent).getNode(); UiElementPullParser topParser = new UiElementPullParser(parentNode, - false, Collections.<UiElementNode>emptySet(), mDensity, mXdpi, mProject) { + false, Collections.<UiElementNode>emptySet(), hardwareConfig.getDensity(), + mProject) { @Override public String getAttributeValue(String namespace, String localName) { if (filter != null) { @@ -465,8 +532,7 @@ public class RenderService { topParser, RenderingMode.FULL_EXPAND, mProject /* projectKey */, - width, height, - mDensity, mXdpi, mYdpi, + hardwareConfig, mResourceResolver, mProjectCallback, mMinSdkVersion, 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 7c5cd4b..eb3d6f2 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 @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.FQCN_SPACE; import static com.android.SdkConstants.FQCN_SPACE_V7; @@ -22,9 +23,7 @@ import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_MARGIN; import static com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionHandle.PIXEL_RADIUS; - import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.INode; @@ -39,6 +38,8 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.resources.ResourceType; import com.android.utils.Pair; @@ -1184,40 +1185,78 @@ public class SelectionManager implements ISelectionProvider { if (selections.size() > 0) { NodeProxy primary = selections.get(0).getNode(); if (primary != null) { - String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID); - currentId = BaseViewRule.stripIdPrefix(currentId); - InputDialog d = new InputDialog( - AdtPlugin.getDisplay().getActiveShell(), - "Set ID", - "New ID:", - currentId, - ResourceNameValidator.create(false, (IProject) null, ResourceType.ID)); - if (d.open() == Window.OK) { - final String s = d.getValue(); - mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID", - new Runnable() { - @Override - public void run() { - String newId = s; - newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s); - for (SelectionItem item : selections) { - item.getNode().setAttribute(ANDROID_URI, ATTR_ID, newId); - } + performRename(primary, selections); + } + } + } - LayoutCanvas canvas = mCanvas; - CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); - if (root != null) { - UiViewElementNode uiViewNode = root.getUiViewNode(); - NodeFactory nodeFactory = canvas.getNodeFactory(); - NodeProxy rootNode = nodeFactory.create(uiViewNode); - if (rootNode != null) { - rootNode.applyPendingChanges(); - } + /** + * Performs renaming the given node. + * + * @param primary the node to be renamed, or the primary node (to get the + * current value from if more than one node should be renamed) + * @param selections if not null, a list of nodes to apply the setting to + * (which should include the primary) + * @return the result of the renaming operation + */ + @NonNull + public RenameResult performRename( + final @NonNull INode primary, + final @Nullable List<SelectionItem> selections) { + String id = primary.getStringAttr(ANDROID_URI, ATTR_ID); + if (id != null && !id.isEmpty()) { + RenameResult result = RenameResourceWizard.renameResource( + mCanvas.getShell(), + mCanvas.getEditorDelegate().getGraphicalEditor().getProject(), + ResourceType.ID, BaseViewRule.stripIdPrefix(id), null, true /*canClear*/); + if (result.isCanceled()) { + return result; + } else if (!result.isUnavailable()) { + return result; + } + } + String currentId = primary.getStringAttr(ANDROID_URI, ATTR_ID); + currentId = BaseViewRule.stripIdPrefix(currentId); + InputDialog d = new InputDialog( + AdtPlugin.getDisplay().getActiveShell(), + "Set ID", + "New ID:", + currentId, + ResourceNameValidator.create(false, (IProject) null, ResourceType.ID)); + if (d.open() == Window.OK) { + final String s = d.getValue(); + mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Set ID", + new Runnable() { + @Override + public void run() { + String newId = s; + newId = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(s); + if (selections != null) { + for (SelectionItem item : selections) { + NodeProxy node = item.getNode(); + if (node != null) { + node.setAttribute(ANDROID_URI, ATTR_ID, newId); } } - }); + } else { + primary.setAttribute(ANDROID_URI, ATTR_ID, newId); + } + + LayoutCanvas canvas = mCanvas; + CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); + if (root != null) { + UiViewElementNode uiViewNode = root.getUiViewNode(); + NodeFactory nodeFactory = canvas.getNodeFactory(); + NodeProxy rootNode = nodeFactory.create(uiViewNode); + if (rootNode != null) { + rootNode.applyPendingChanges(); + } + } } - } + }); + return RenameResult.name(BaseViewRule.stripIdPrefix(s)); + } else { + return RenameResult.canceled(); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java index c5f976f..388907a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ClientRulesEngine.java @@ -21,6 +21,7 @@ import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.AUTO_URI; import static com.android.SdkConstants.CLASS_FRAGMENT; import static com.android.SdkConstants.CLASS_V4_FRAGMENT; +import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.URI_PREFIX; @@ -50,6 +51,7 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ViewHierarchy; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.CyclicDependencyValidator; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; @@ -116,6 +118,11 @@ import java.util.concurrent.atomic.AtomicReference; * with a few methods they can use to access functionality from this {@link RulesEngine}. */ class ClientRulesEngine implements IClientRulesEngine { + /** The return code from the dialog for the user choosing "Clear" */ + public static final int CLEAR_RETURN_CODE = -5; + /** The dialog button ID for the user choosing "Clear" */ + private static final int CLEAR_BUTTON_ID = CLEAR_RETURN_CODE; + private final RulesEngine mRulesEngine; private final String mFqcn; @@ -145,12 +152,21 @@ class ClientRulesEngine implements IClientRulesEngine { @Override public void displayAlert(@NonNull String message) { MessageDialog.openInformation( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), mFqcn, // title message); } @Override + public boolean rename(INode node) { + GraphicalEditorPart editor = mRulesEngine.getEditor(); + SelectionManager manager = editor.getCanvasControl().getSelectionManager(); + RenameResult result = manager.performRename(node, null); + + return !result.isCanceled() && !result.isUnavailable(); + } + + @Override public String displayInput(@NonNull String message, @Nullable String value, final @Nullable IValidator filter) { IInputValidator validator = null; @@ -170,12 +186,32 @@ class ClientRulesEngine implements IClientRulesEngine { } InputDialog d = new InputDialog( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), mFqcn, // title message, value == null ? "" : value, //$NON-NLS-1$ - validator); - if (d.open() == Window.OK) { + validator) { + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, CLEAR_BUTTON_ID, "Clear", false /*defaultButton*/); + super.createButtonsForButtonBar(parent); + } + + @Override + protected void buttonPressed(int buttonId) { + super.buttonPressed(buttonId); + + if (buttonId == CLEAR_BUTTON_ID) { + assert CLEAR_RETURN_CODE != Window.OK && CLEAR_RETURN_CODE != Window.CANCEL; + setReturnCode(CLEAR_RETURN_CODE); + close(); + } + } + }; + int result = d.open(); + if (result == ResourceChooser.CLEAR_RETURN_CODE) { + return ""; + } else if (result == Window.OK) { return d.getValue(); } return null; @@ -299,7 +335,7 @@ class ClientRulesEngine implements IClientRulesEngine { // get the resource repository for this project and the system resources. ResourceRepository projectRepository = ResourceManager.getInstance().getProjectResources(project); - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); if (shell == null) { return null; } @@ -338,7 +374,7 @@ class ClientRulesEngine implements IClientRulesEngine { GraphicalEditorPart editor = mRulesEngine.getEditor(); IProject project = editor.getProject(); if (project != null) { - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); if (shell == null) { return null; } @@ -437,7 +473,7 @@ class ClientRulesEngine implements IClientRulesEngine { scope = SearchEngine.createJavaSearchScope(subTypes, IJavaSearchScope.SOURCES); } - Shell parent = AdtPlugin.getDisplay().getActiveShell(); + Shell parent = AdtPlugin.getShell(); final AtomicReference<String> returnValue = new AtomicReference<String>(); final AtomicReference<SelectionDialog> dialogHolder = @@ -477,7 +513,8 @@ class ClientRulesEngine implements IClientRulesEngine { int modifiers = typeInfoRequestor.getModifiers(); if (!Flags.isPublic(modifiers) || Flags.isInterface(modifiers) - || Flags.isEnum(modifiers)) { + || Flags.isEnum(modifiers) + || Flags.isAbstract(modifiers)) { return false; } return true; @@ -509,6 +546,98 @@ class ClientRulesEngine implements IClientRulesEngine { } @Override + public String displayCustomViewClassInput() { + try { + IJavaSearchScope scope = SearchEngine.createWorkspaceScope(); + IProject project = mRulesEngine.getProject(); + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject != null) { + // Look up sub-types of each (new fragment class and compatibility fragment + // class, if any) and merge the two arrays - then create a scope from these + // elements. + IType[] viewTypes = new IType[0]; + IType fragmentType = javaProject.findType(CLASS_VIEW); + if (fragmentType != null) { + ITypeHierarchy hierarchy = + fragmentType.newTypeHierarchy(new NullProgressMonitor()); + viewTypes = hierarchy.getAllSubtypes(fragmentType); + } + scope = SearchEngine.createJavaSearchScope(viewTypes, IJavaSearchScope.SOURCES); + } + + Shell parent = AdtPlugin.getShell(); + final AtomicReference<String> returnValue = + new AtomicReference<String>(); + final AtomicReference<SelectionDialog> dialogHolder = + new AtomicReference<SelectionDialog>(); + final SelectionDialog dialog = JavaUI.createTypeDialog( + parent, + new ProgressMonitorDialog(parent), + scope, + IJavaElementSearchConstants.CONSIDER_CLASSES, false, + // Use ? as a default filter to fill dialog with matches + "?", //$NON-NLS-1$ + new TypeSelectionExtension() { + @Override + public Control createContentArea(Composite parentComposite) { + Composite composite = new Composite(parentComposite, SWT.NONE); + composite.setLayout(new GridLayout(1, false)); + Button button = new Button(composite, SWT.PUSH); + button.setText("Create New..."); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + String fqcn = createNewCustomViewClass(javaProject); + if (fqcn != null) { + returnValue.set(fqcn); + dialogHolder.get().close(); + } + } + }); + return composite; + } + + @Override + public ITypeInfoFilterExtension getFilterExtension() { + return new ITypeInfoFilterExtension() { + @Override + public boolean select(ITypeInfoRequestor typeInfoRequestor) { + int modifiers = typeInfoRequestor.getModifiers(); + if (!Flags.isPublic(modifiers) + || Flags.isInterface(modifiers) + || Flags.isEnum(modifiers) + || Flags.isAbstract(modifiers)) { + return false; + } + return true; + } + }; + } + }); + dialogHolder.set(dialog); + + dialog.setTitle("Choose Custom View Class"); + dialog.setMessage("Select a Custom View class (? = any character, * = any string):"); + if (dialog.open() == IDialogConstants.CANCEL_ID) { + return null; + } + if (returnValue.get() != null) { + return returnValue.get(); + } + + Object[] types = dialog.getResult(); + if (types != null && types.length > 0) { + return ((IType) types[0]).getFullyQualifiedName(); + } + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + return null; + } + + @Override public void redraw() { mRulesEngine.getEditor().getCanvasControl().redraw(); } @@ -548,13 +677,17 @@ class ClientRulesEngine implements IClientRulesEngine { return (int) (pixels / mRulesEngine.getEditor().getCanvasControl().getScale()); } - String createNewFragmentClass(IJavaProject javaProject) { + private String createNewFragmentClass(IJavaProject javaProject) { NewClassWizardPage page = new NewClassWizardPage(); IProject project = mRulesEngine.getProject(); - IAndroidTarget target = Sdk.getCurrent().getTarget(project); + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return null; + } + IAndroidTarget target = sdk.getTarget(project); String superClass; - if (target.getVersion().getApiLevel() < 11) { + if (target == null || target.getVersion().getApiLevel() < 11) { superClass = CLASS_V4_FRAGMENT; } else { superClass = CLASS_FRAGMENT; @@ -580,6 +713,32 @@ class ClientRulesEngine implements IClientRulesEngine { } } + private String createNewCustomViewClass(IJavaProject javaProject) { + NewClassWizardPage page = new NewClassWizardPage(); + + IProject project = mRulesEngine.getProject(); + String superClass = CLASS_VIEW; + page.setSuperClass(superClass, true /* canBeModified */); + IPackageFragmentRoot root = ManifestInfo.getSourcePackageRoot(javaProject); + if (root != null) { + page.setPackageFragmentRoot(root, true /* canBeModified */); + } + ManifestInfo manifestInfo = ManifestInfo.get(project); + IPackageFragment pkg = manifestInfo.getPackageFragment(); + if (pkg != null) { + page.setPackageFragment(pkg, true /* canBeModified */); + } + OpenNewClassWizardAction action = new OpenNewClassWizardAction(); + action.setConfiguredWizardPage(page); + action.run(); + IType createdType = page.getCreatedType(); + if (createdType != null) { + return createdType.getFullyQualifiedName(); + } else { + return null; + } + } + @Override public @NonNull String getUniqueId(@NonNull String fqcn) { UiDocumentNode root = mRulesEngine.getEditor().getModel(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java index f7eac4434..8f99237 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gre; import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX; import static com.android.SdkConstants.VIEW_MERGE; +import static com.android.SdkConstants.VIEW_TAG; import com.android.annotations.NonNull; import com.android.annotations.Nullable; @@ -792,6 +793,12 @@ public class RulesEngine { String baseName = realFqcn.substring(dotIndex+1); // Capitalize rule class name to match naming conventions, if necessary (<merge>) if (Character.isLowerCase(baseName.charAt(0))) { + if (baseName.equals(VIEW_TAG)) { + // Hack: ViewRule is generic for the "View" class, so we can't use it + // for the special XML "view" tag (lowercase); instead, the rule is + // named "ViewTagRule" instead. + baseName = "ViewTag"; //$NON-NLS-1$ + } baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1); } ruleClassName = packageName + "." + //$NON-NLS-1$ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java index 586da12..5f2659e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/ViewMetadataRepository.java @@ -37,6 +37,8 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewEleme import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.resources.Density; import com.android.utils.Pair; +import com.google.common.base.Splitter; +import com.google.common.io.Closeables; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -140,6 +142,8 @@ public class ViewMetadataRepository { } catch (Exception e) { AdtPlugin.log(e, "Parsing palette file failed"); return null; + } finally { + Closeables.closeQuietly(paletteStream); } } @@ -195,6 +199,7 @@ public class ViewMetadataRepository { } /** Returns an ordered list of categories and views, parsed from a metadata file */ + @SuppressWarnings("resource") // streams passed to parser InputSource closed by parser private List<CategoryData> getCategories() { if (mCategories == null) { mCategories = new ArrayList<CategoryData>(); @@ -536,13 +541,12 @@ public class ViewMetadataRepository { if (mRelatedTo == null || mRelatedTo.length() == 0) { return Collections.emptyList(); } else { - String[] basenames = mRelatedTo.split(","); //$NON-NLS-1$ List<String> result = new ArrayList<String>(); ViewMetadataRepository repository = ViewMetadataRepository.get(); Map<String, ViewData> classToView = repository.getClassToView(); List<String> fqns = new ArrayList<String>(classToView.keySet()); - for (String basename : basenames) { + for (String basename : Splitter.on(',').split(mRelatedTo)) { boolean found = false; for (String fqcn : fqns) { String suffix = '.' + basename; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml index db3bd7b..6a67b1d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/extra-view-metadata.xml @@ -293,11 +293,13 @@ class="android.widget.ImageView" topAttrs="src,scaleType" resize="scaled" + render="skip" relatedTo="ImageButton,VideoView" /> <view class="android.widget.ImageButton" topAttrs="src,background,style" resize="scaled" + render="skip" relatedTo="Button,ImageView" /> <view class="android.widget.Gallery" @@ -395,6 +397,10 @@ topAttrs="layout,inflatedId,visibility" render="skip" /> <view + class="view" + topAttrs="class" + render="skip" /> + <view class="android.gesture.GestureOverlayView" topAttrs="gestureStrokeType,uncertainGestureColor,eventsInterceptionEnabled,gestureColor,orientation" render="skip" /> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml index ec32882..96c7fe7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/rendering-configs.xml @@ -188,18 +188,6 @@ android:layout_height="wrap_content"> </TextView> - <ImageButton - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:id="@+id/android_widget_ImageButton" - android:src="@android:drawable/ic_menu_gallery"> - </ImageButton> - <ImageView - android:id="@+id/android_widget_ImageView" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:src="@android:drawable/ic_menu_gallery"> - </ImageView> <MultiAutoCompleteTextView android:layout_height="wrap_content" android:layout_width="200dip" diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java index 0276b6c..5e1e702 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/FlagXmlPropertyDialog.java @@ -82,6 +82,11 @@ implements IStructuredContentProvider, ICheckStateListener, SelectionListener, K mTable = mViewer.getTable(); mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + Composite workaround = PropertyFactory.addWorkaround(container); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } + mViewer.setContentProvider(this); mViewer.setInput(mFlags); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java index 59754af..2b8cfbf 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/PropertyFactory.java @@ -37,12 +37,26 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.browser.IWebBrowser; import org.eclipse.wb.internal.core.editor.structure.property.PropertyListIntersector; import org.eclipse.wb.internal.core.model.property.ComplexProperty; import org.eclipse.wb.internal.core.model.property.Property; import org.eclipse.wb.internal.core.model.property.category.PropertyCategory; import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor; +import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -66,6 +80,7 @@ import java.util.WeakHashMap; */ public class PropertyFactory { /** Disable cache during development only */ + @SuppressWarnings("unused") private static final boolean CACHE_ENABLED = true || !LintUtils.assertionsEnabled(); static { if (!CACHE_ENABLED) { @@ -253,6 +268,9 @@ public class PropertyFactory { Map<String, ComplexProperty> categoryToProperty = new HashMap<String, ComplexProperty>(); Multimap<String, Property> categoryToProperties = ArrayListMultimap.create(); + if (properties.isEmpty()) { + return properties; + } ViewElementDescriptor parent = (ViewElementDescriptor) properties.get(0).getDescriptor() .getParent(); @@ -685,4 +703,48 @@ public class PropertyFactory { public void setSortingMode(SortingMode sortingMode) { mSortMode = sortingMode; } + + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574 + public static Composite addWorkaround(Composite parent) { + if (ButtonPropertyEditorPresentation.isInWorkaround) { + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, false)); + Label label = new Label(top, SWT.WRAP); + label.setText( + "This dialog is shown instead of an inline text editor as a\n" + + "workaround for an Eclipse bug specific to OSX Mountain Lion.\n" + + "It should be fixed in Eclipse 4.3."); + label.setForeground(top.getDisplay().getSystemColor(SWT.COLOR_RED)); + GridData data = new GridData(); + data.grabExcessVerticalSpace = false; + data.grabExcessHorizontalSpace = false; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.BEGINNING; + label.setLayoutData(data); + + Link link = new Link(top, SWT.NO_FOCUS); + link.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + link.setText("<a>https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574</a>"); + link.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + try { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser(); + browser.openURL(new URL(event.text)); + } catch (Exception e) { + String message = String.format( + "Could not open browser. Vist\n%1$s\ninstead.", + event.text); + MessageDialog.openError(((Link)event.getSource()).getShell(), + "Browser Error", message); + } + } + }); + + return top; + } + + return null; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java index 3fb72a9..fb7e459 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/StringXmlPropertyDialog.java @@ -15,6 +15,10 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.properties; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.wb.internal.core.model.property.Property; import org.eclipse.wb.internal.core.model.property.editor.string.StringPropertyDialog; @@ -28,4 +32,16 @@ class StringXmlPropertyDialog extends StringPropertyDialog { protected boolean isMultiLine() { return false; } + + @Override + protected Control createDialogArea(Composite parent) { + Composite area = (Composite) super.createDialogArea(parent); + + Composite workaround = PropertyFactory.addWorkaround(area); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } + + return area; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java index 72577a5..87fb0e6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/properties/XmlPropertyEditor.java @@ -21,21 +21,31 @@ import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.DOT_PNG; import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; import com.android.annotations.NonNull; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; +import com.android.ide.common.layout.BaseViewRule; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; @@ -47,6 +57,9 @@ import com.google.common.collect.Maps; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.window.Window; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; @@ -137,7 +150,7 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { // TODO: do I have to strip off the @ too? isFramework = isFramework || value.startsWith(ANDROID_PREFIX) - || value.startsWith(ANDROID_THEME_PREFIX);; + || value.startsWith(ANDROID_THEME_PREFIX); ResourceValue v = resolver.findResValue(text, isFramework); if (v != null && !value.equals(v.getValue())) { resValue = v; @@ -200,7 +213,7 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { XmlProperty xmlProperty = (XmlProperty) property; GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); RenderService service = RenderService.create(graphicalEditor); - service.setSize(SAMPLE_SIZE, SAMPLE_SIZE); + service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE); BufferedImage drawable = service.renderDrawable(resValue); if (drawable != null) { swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable, @@ -301,16 +314,101 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { @Override protected boolean setEditorText(Property property, String text) throws Exception { + Object oldValue = property.getValue(); + String old = oldValue != null ? oldValue.toString() : null; + + // If users enters a new id without specifying the @id/@+id prefix, insert it + boolean isId = isIdProperty(property); + if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) { + text = NEW_ID_PREFIX + text; + } + + // Handle id refactoring: if you change an id, may want to update references too. + // Ask user. + if (isId && property instanceof XmlProperty + && old != null && !old.isEmpty() + && text != null && !text.isEmpty() + && !text.equals(old)) { + XmlProperty xmlProperty = (XmlProperty) property; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS); + boolean performRefactor = false; + Shell shell = AdtPlugin.getShell(); + if (refactorPref == null + || refactorPref.isEmpty() + || refactorPref.equals(MessageDialogWithToggle.PROMPT)) { + MessageDialogWithToggle dialog = + MessageDialogWithToggle.openYesNoCancelQuestion( + shell, + "Update References?", + "Update all references as well? " + + "This will update all XML references and Java R field references.", + "Do not show again", + false, + store, + AdtPrefs.PREFS_REFACTOR_IDS); + switch (dialog.getReturnCode()) { + case IDialogConstants.CANCEL_ID: + return false; + case IDialogConstants.YES_ID: + performRefactor = true; + break; + case IDialogConstants.NO_ID: + performRefactor = false; + break; + } + } else { + performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS); + } + if (performRefactor) { + CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor(); + if (xmlEditor != null) { + IProject project = xmlEditor.getProject(); + if (project != null && shell != null) { + RenameResourceWizard.renameResource(shell, project, + ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false); + } + } + } + } + property.setValue(text); + return true; } + private static boolean isIdProperty(Property property) { + XmlProperty xmlProperty = (XmlProperty) property; + return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); + } + private void openDialog(PropertyTable propertyTable, Property property) throws Exception { XmlProperty xmlProperty = (XmlProperty) property; IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); - boolean isId = xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); - if (isId) { + if (isIdProperty(property)) { + Object value = xmlProperty.getValue(); + if (value != null && !value.toString().isEmpty()) { + GraphicalEditorPart editor = xmlProperty.getGraphicalEditor(); + if (editor != null) { + LayoutCanvas canvas = editor.getCanvasControl(); + SelectionManager manager = canvas.getSelectionManager(); + + NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode()); + if (primary != null) { + RenameResult result = manager.performRename(primary, null); + if (result.isCanceled()) { + return; + } else if (!result.isUnavailable()) { + String name = result.getName(); + String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name); + xmlProperty.setValue(id); + return; + } + } + } + } + // When editing the id attribute, don't offer a resource chooser: usually // you want to enter a *new* id here attributeInfo = null; @@ -370,7 +468,7 @@ class XmlPropertyEditor extends AbstractTextPropertyEditor { // get the resource repository for this project and the system resources. ResourceRepository projectRepository = ResourceManager.getInstance().getProjectResources(project); - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); ReferenceChooserDialog dlg = new ReferenceChooserDialog( project, projectRepository, diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java index b01b4b1..d8c85aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java @@ -41,9 +41,9 @@ import static com.android.SdkConstants.VALUE_WRAP_CONTENT; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java index 65edd54..f58ac55 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java @@ -37,10 +37,10 @@ import static com.android.resources.ResourceType.LAYOUT; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; @@ -330,8 +330,9 @@ public class ExtractIncludeRefactoring extends VisualRefactoring { String newFile = sb.toString(); if (AdtPrefs.getPrefs().getFormatGuiXml()) { - newFile = XmlPrettyPrinter.prettyPrint(newFile, - XmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, null /*lineSeparator*/); + newFile = EclipseXmlPrettyPrinter.prettyPrint(newFile, + EclipseXmlFormatPreferences.create(), XmlFormatStyle.LAYOUT, + null /*lineSeparator*/); } addFile.setEdit(new InsertEdit(0, newFile)); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java index ffe6892..9b1770d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractStyleRefactoring.java @@ -43,10 +43,10 @@ import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java index 868d790..fe673a5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/GridLayoutConverter.java @@ -15,8 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_HORIZ_MASK; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_BACKGROUND; import static com.android.SdkConstants.ATTR_COLUMN_COUNT; import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE; @@ -34,10 +33,10 @@ import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; import static com.android.SdkConstants.ATTR_ORIENTATION; import static com.android.SdkConstants.FQCN_GRID_LAYOUT; +import static com.android.SdkConstants.FQCN_SPACE; import static com.android.SdkConstants.GRAVITY_VALUE_FILL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_HORIZONTAL; import static com.android.SdkConstants.GRAVITY_VALUE_FILL_VERTICAL; -import static com.android.SdkConstants.GRID_LAYOUT; import static com.android.SdkConstants.ID_PREFIX; import static com.android.SdkConstants.LINEAR_LAYOUT; import static com.android.SdkConstants.NEW_ID_PREFIX; @@ -51,10 +50,9 @@ import static com.android.SdkConstants.VALUE_HORIZONTAL; import static com.android.SdkConstants.VALUE_MATCH_PARENT; import static com.android.SdkConstants.VALUE_VERTICAL; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_HORIZ_MASK; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; - -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.layout.BaseLayoutRule; import com.android.ide.common.layout.GravityHelper; @@ -66,7 +64,9 @@ import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewEleme import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; +import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; +import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IStatus; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.text.edits.InsertEdit; @@ -227,9 +227,12 @@ class GridLayoutConverter { int column = columnFixed.size(); StringBuilder sb = new StringBuilder(64); String spaceTag = SPACE; - if (!gridLayout.equals(GRID_LAYOUT) && gridLayout.length() > GRID_LAYOUT.length()) { - String pkg = gridLayout.substring(0, gridLayout.length() - GRID_LAYOUT.length()); - spaceTag = pkg + spaceTag; + IFile file = mRefactoring.getFile(); + if (file != null) { + spaceTag = SupportLibraryHelper.getTagFor(file.getProject(), FQCN_SPACE); + if (spaceTag.equals(FQCN_SPACE)) { + spaceTag = SPACE; + } } sb.append('<').append(spaceTag).append(' '); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java index 51360e8..88423e4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RefactoringAssistant.java @@ -20,8 +20,13 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceProcessor; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; +import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceXmlTextAction; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; +import com.android.resources.ResourceType; +import com.android.utils.Pair; import org.eclipse.core.resources.IFile; import org.eclipse.jface.text.IDocument; @@ -36,6 +41,7 @@ import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.ltk.core.refactoring.Refactoring; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; import org.eclipse.ltk.ui.refactoring.RefactoringWizard; import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; import org.eclipse.swt.graphics.Image; @@ -100,6 +106,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { boolean isTagName = false; boolean isAttributeName = false; boolean isStylableAttribute = false; + Pair<ResourceType, String> resource = null; IStructuredModel model = null; try { model = xmlEditor.getModelForRead(); @@ -115,6 +122,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { isValue = true; if (value.startsWith("'@") || value.startsWith("\"@")) { //$NON-NLS-1$ //$NON-NLS-2$ isReferenceValue = true; + resource = RenameResourceXmlTextAction.findResource(doc, offset); } } else if (type.equals(DOMRegionContext.XML_TAG_NAME) || type.equals(DOMRegionContext.XML_TAG_OPEN) @@ -132,6 +140,8 @@ public class RefactoringAssistant implements IQuickAssistProcessor { // On the edge of an attribute name and an attribute value isAttributeName = true; isStylableAttribute = true; + } else if (type.equals(DOMRegionContext.XML_CONTENT)) { + resource = RenameResourceXmlTextAction.findResource(doc, offset); } } } finally { @@ -141,7 +151,7 @@ public class RefactoringAssistant implements IQuickAssistProcessor { } List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); - if (isTagName || isAttributeName || isValue) { + if (isTagName || isAttributeName || isValue || resource != null) { StructuredTextEditor structuredEditor = xmlEditor.getStructuredTextEditor(); ISelectionProvider provider = structuredEditor.getSelectionProvider(); ISelection selection = provider.getSelection(); @@ -173,6 +183,12 @@ public class RefactoringAssistant implements IQuickAssistProcessor { if (isValue && !isReferenceValue) { proposals.add(new RefactoringProposal(xmlEditor, new ExtractStringRefactoring(file, xmlEditor, textSelection))); + } else if (resource != null) { + RenameResourceProcessor processor = new RenameResourceProcessor( + file.getProject(), resource.getFirst(), + resource.getSecond(), null); + RenameRefactoring refactoring = new RenameRefactoring(processor); + proposals.add(new RefactoringProposal(xmlEditor, refactoring)); } LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(xmlEditor); @@ -275,6 +291,12 @@ public class RefactoringAssistant implements IQuickAssistProcessor { } else if (mRefactoring instanceof ExtractStringRefactoring) { wizard = new ExtractStringWizard((ExtractStringRefactoring) mRefactoring, mEditor.getProject()); + } else if (mRefactoring instanceof RenameRefactoring) { + RenameRefactoring refactoring = (RenameRefactoring) mRefactoring; + RenameResourceProcessor processor = + (RenameResourceProcessor) refactoring.getProcessor(); + ResourceType type = processor.getType(); + wizard = new RenameResourceWizard((RenameRefactoring) mRefactoring, type, false); } else { throw new IllegalArgumentException(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java index 7f9cc71..e0d6313 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/RelativeLayoutConversionHelper.java @@ -15,15 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP; -import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_BACKGROUND; import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE; @@ -58,10 +50,16 @@ import static com.android.SdkConstants.VALUE_N_DP; import static com.android.SdkConstants.VALUE_TRUE; import static com.android.SdkConstants.VALUE_VERTICAL; import static com.android.SdkConstants.VALUE_WRAP_CONTENT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_BOTTOM; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_HORIZ; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_CENTER_VERT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_HORIZ; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_FILL_VERT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_LEFT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_RIGHT; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_TOP; +import static com.android.ide.common.layout.GravityHelper.GRAVITY_VERT_MASK; - -import com.android.SdkConstants; -import static com.android.SdkConstants.ANDROID_URI; import com.android.ide.common.layout.GravityHelper; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java index 1dcc1b7..4eff2cd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UnwrapRefactoring.java @@ -22,7 +22,7 @@ import static com.android.SdkConstants.EXT_XML; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java index 8f678c1..0e56bdf 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/UseCompoundDrawableRefactoring.java @@ -42,10 +42,10 @@ import static com.android.SdkConstants.VALUE_VERTICAL; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; @@ -353,13 +353,10 @@ public class UseCompoundDrawableRefactoring extends VisualRefactoring { } } - XmlFormatPreferences formatPrefs = XmlFormatPreferences.create(); - XmlPrettyPrinter printer = new XmlPrettyPrinter(formatPrefs, XmlFormatStyle.LAYOUT, - null /*lineSeparator*/); - StringBuilder sb = new StringBuilder(300); - printer.prettyPrint(-1, tempDocument, null, null, sb, false /*openTagOnly*/); - String xml = sb.toString(); - + String xml = EclipseXmlPrettyPrinter.prettyPrint( + tempDocument.getDocumentElement(), + EclipseXmlFormatPreferences.create(), + XmlFormatStyle.LAYOUT, null, false); TextEdit replace = new ReplaceEdit(mSelectionStart, mSelectionEnd - mSelectionStart, xml); rootEdit.addChild(replace); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java index c2035f2..904a3a0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java @@ -29,13 +29,13 @@ import static com.android.SdkConstants.XMLNS_PREFIX; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; -import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationChooser; +import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationDescription; import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; @@ -54,7 +54,6 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Path; -import org.eclipse.core.runtime.QualifiedName; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; @@ -295,6 +294,10 @@ public abstract class VisualRefactoring extends Refactoring { return args; } + IFile getFile() { + return mFile; + } + // ---- Shared functionality ---- @@ -304,8 +307,7 @@ public abstract class VisualRefactoring extends Refactoring { try { // Duplicate the current state into the newly created file - QualifiedName qname = ConfigurationChooser.NAME_CONFIG_STATE; - String state = AdtPlugin.getFileProperty(leavingFile, qname); + String state = ConfigurationDescription.getDescription(leavingFile); // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current // theme to show. @@ -1310,8 +1312,8 @@ public abstract class VisualRefactoring extends Refactoring { //int end = actual.length() - distanceFromEnd; //int length = end - start; //TextEdit format = AndroidXmlFormattingStrategy.format(model, start, length); - XmlFormatPreferences formatPrefs = XmlFormatPreferences.create(); - String formatted = XmlPrettyPrinter.prettyPrint(actual, formatPrefs, style, + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); + String formatted = EclipseXmlPrettyPrinter.prettyPrint(actual, formatPrefs, style, null /*lineSeparator*/); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java index ff2e9bd..07b00b8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java @@ -28,8 +28,8 @@ import static com.android.SdkConstants.VALUE_WRAP_CONTENT; import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java index 7050be4..d9d2722 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/uimodel/UiViewElementNode.java @@ -153,11 +153,17 @@ public class UiViewElementNode extends UiElementNode { if (className != null && className.length() > 0) { int index = className.lastIndexOf('.'); if (index != -1) { - className = className.substring(index + 1); + className = "customView"; //$NON-NLS-1$ } img = icons.getIcon(className); } } + + if (img == null) { + // Can't have both view.png and View.png; issues on case sensitive vs + // case insensitive file systems + img = icons.getIcon("View"); //$NON-NLS-1$ + } } if (img == null) { img = desc.getGenericIcon(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java index 8f78a0f..1492adb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestContentAssist.java @@ -48,7 +48,7 @@ public final class ManifestContentAssist extends AndroidContentAssist { } @Override - protected void computeAttributeValues(List<ICompletionProposal> proposals, int offset, + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, String parentTagName, String attributeName, Node node, String wordPrefix, boolean skipEndTag, int replaceLength) { if (attributeName.endsWith(ATTRIBUTE_MIN_SDK_VERSION) @@ -60,7 +60,11 @@ public final class ManifestContentAssist extends AndroidContentAssist { List<Pair<String, String>> choices = new ArrayList<Pair<String, String>>(); int max = AdtUtils.getHighestKnownApiLevel(); // Look for any more recent installed versions the user may have - IAndroidTarget[] targets = Sdk.getCurrent().getTargets(); + Sdk sdk = Sdk.getCurrent(); + if (sdk == null) { + return false; + } + IAndroidTarget[] targets = sdk.getTargets(); for (IAndroidTarget target : targets) { AndroidVersion version = target.getVersion(); int apiLevel = version.getApiLevel(); @@ -81,9 +85,10 @@ public final class ManifestContentAssist extends AndroidContentAssist { addMatchingProposals(proposals, choices.toArray(), offset, node, wordPrefix, needTag, true /* isAttribute */, false /* isNew */, skipEndTag /* skipEndTag */, replaceLength); + return true; } else { - super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node, - wordPrefix, skipEndTag, replaceLength); + return super.computeAttributeValues(proposals, offset, parentTagName, attributeName, + node, wordPrefix, skipEndTag, replaceLength); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java index b1bfa88..55ebf59 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/ManifestEditor.java @@ -321,8 +321,8 @@ public final class ManifestEditor extends AndroidXmlEditor { mMarkerMonitor = new IFileListener() { @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { - if (file.equals(inputFile)) { + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (isAndroidProject && file.equals(inputFile)) { processMarkerChanges(markerDeltas); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java index a1a4b58..4c829d9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/manifest/model/UiClassAttributeNode.java @@ -82,6 +82,10 @@ import org.eclipse.ui.forms.widgets.TableWrapData; import org.w3c.dom.Element; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; /** * Represents an XML attribute for a class, that can be modified using a simple text field or @@ -686,7 +690,46 @@ public class UiClassAttributeNode extends UiTextAttributeNode { @Override public String[] getPossibleValues(String prefix) { - // TODO: compute a list of existing classes for content assist completion + // Compute a list of existing classes for content assist completion + IProject project = getProject(); + if (project == null || mReferenceClass == null) { + return null; + } + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + IType type = javaProject.findType(mReferenceClass); + // Use sets because query sometimes repeats the same class + Set<String> libraryTypes = new HashSet<String>(80); + Set<String> localTypes = new HashSet<String>(30); + if (type != null) { + ITypeHierarchy hierarchy = type.newTypeHierarchy(new NullProgressMonitor()); + IType[] allSubtypes = hierarchy.getAllSubtypes(type); + for (IType subType : allSubtypes) { + int flags = subType.getFlags(); + if (Flags.isPublic(flags) && !Flags.isAbstract(flags)) { + String fqcn = subType.getFullyQualifiedName(); + if (subType.getResource() != null) { + localTypes.add(fqcn); + } else { + libraryTypes.add(fqcn); + } + } + } + } + + List<String> local = new ArrayList<String>(localTypes); + List<String> library = new ArrayList<String>(libraryTypes); + Collections.sort(local); + Collections.sort(library); + List<String> combined = new ArrayList<String>(local.size() + library.size()); + combined.addAll(local); + combined.addAll(library); + return combined.toArray(new String[combined.size()]); + } catch (Exception e) { + AdtPlugin.log(e, null); + } + return null; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java index 71cb35d..ffe637c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiAttributeNode.java @@ -16,17 +16,7 @@ package com.android.ide.eclipse.adt.internal.editors.uimodel; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.ide.eclipse.adt.internal.editors.color.ColorDescriptors.ATTR_COLOR; -import static com.google.common.base.Strings.nullToEmpty; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; +import com.android.ide.common.xml.XmlAttributeSortOrder; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; @@ -34,8 +24,6 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.forms.IManagedForm; import org.w3c.dom.Node; -import java.util.Comparator; - /** * Represents an XML attribute that can be modified by the XML editor's user interface. * <p/> @@ -177,107 +165,10 @@ public abstract class UiAttributeNode implements Comparable<UiAttributeNode> { public abstract void commit(); // ---- Implements Comparable ---- + @Override public int compareTo(UiAttributeNode o) { - return compareAttributes(mDescriptor.getXmlLocalName(), o.mDescriptor.getXmlLocalName()); - } - - /** - * Returns {@link Comparator} values for ordering attributes in the following - * order: - * <ul> - * <li> id - * <li> style - * <li> layout_width - * <li> layout_height - * <li> other layout params, sorted alphabetically - * <li> other attributes, sorted alphabetically - * </ul> - * - * @param name1 the first attribute name to compare - * @param name2 the second attribute name to compare - * @return a negative number if name1 should be ordered before name2 - */ - public static int compareAttributes(String name1, String name2) { - int priority1 = getAttributePriority(name1); - int priority2 = getAttributePriority(name2); - if (priority1 != priority2) { - return priority1 - priority2; - } - - // Sort remaining attributes alphabetically - return name1.compareTo(name2); - } - - /** - * Returns {@link Comparator} values for ordering attributes in the following - * order: - * <ul> - * <li> id - * <li> style - * <li> layout_width - * <li> layout_height - * <li> other layout params, sorted alphabetically - * <li> other attributes, sorted alphabetically, first by namespace, then by name - * </ul> - * @param prefix1 the namespace prefix, if any, of {@code name1} - * @param name1 the first attribute name to compare - * @param prefix2 the namespace prefix, if any, of {@code name2} - * @param name2 the second attribute name to compare - * @return a negative number if name1 should be ordered before name2 - */ - public static int compareAttributes( - @Nullable String prefix1, @NonNull String name1, - @Nullable String prefix2, @NonNull String name2) { - int priority1 = getAttributePriority(name1); - int priority2 = getAttributePriority(name2); - if (priority1 != priority2) { - return priority1 - priority2; - } - - int namespaceDelta = nullToEmpty(prefix1).compareTo(nullToEmpty(prefix2)); - if (namespaceDelta != 0) { - return namespaceDelta; - } - - // Sort remaining attributes alphabetically - return name1.compareTo(name2); - } - - - /** Returns a sorting priority for the given attribute name */ - private static int getAttributePriority(String name) { - if (ATTR_ID.equals(name)) { - return 10; - } - - if (ATTR_NAME.equals(name)) { - return 15; - } - - if (ATTR_STYLE.equals(name)) { - return 20; - } - - if (name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { - // Width and height are special cased because we (a) want width and height - // before the other layout attributes, and (b) we want width to sort before height - // even though it comes after it alphabetically. - if (name.equals(ATTR_LAYOUT_WIDTH)) { - return 30; - } - if (name.equals(ATTR_LAYOUT_HEIGHT)) { - return 40; - } - - return 50; - } - - // "color" sorts to the end - if (ATTR_COLOR.equals(name)) { - return 100; - } - - return 60; + return XmlAttributeSortOrder.compareAttributes(mDescriptor.getXmlLocalName(), + o.mDescriptor.getXmlLocalName()); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java index f905c73..ed447c6 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiElementNode.java @@ -26,6 +26,7 @@ import com.android.SdkConstants; import com.android.annotations.VisibleForTesting; import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.resources.platform.AttributeInfo; +import com.android.ide.common.xml.XmlAttributeSortOrder; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; @@ -1663,7 +1664,7 @@ public class UiElementNode implements IPropertySource { List<Attr> move = new ArrayList<Attr>(); for (int i = 0, n = attributes.getLength(); i < n; i++) { Attr attribute = (Attr) attributes.item(i); - if (UiAttributeNode.compareAttributes( + if (XmlAttributeSortOrder.compareAttributes( attribute.getPrefix(), attribute.getLocalName(), firstNamePrefix, firstName) > 0) { move.add(attribute); @@ -1699,7 +1700,7 @@ public class UiElementNode implements IPropertySource { String domAttributeName = domAttribute.getLocalName(); String uiAttributeName = uiAttribute.getDescriptor().getXmlLocalName(); - compare = UiAttributeNode.compareAttributes(domAttributeName, + compare = XmlAttributeSortOrder.compareAttributes(domAttributeName, uiAttributeName); } else { compare = 1; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java index bce3db4..eb51d3f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java @@ -84,6 +84,13 @@ import java.util.regex.Pattern; public class UiResourceAttributeNode extends UiTextAttributeNode { private ResourceType mType; + /** + * Creates a new {@linkplain UiResourceAttributeNode} + * + * @param type the associated resource type + * @param attributeDescriptor the attribute descriptor for this attribute + * @param uiParent the parent ui node, if any + */ public UiResourceAttributeNode(ResourceType type, AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { super(attributeDescriptor, uiParent); @@ -138,10 +145,15 @@ public class UiResourceAttributeNode extends UiTextAttributeNode { } /** - * Shows a dialog letting the user choose a set of enum, and returns a string - * containing the result. + * Shows a dialog letting the user choose a set of enum, and returns a + * string containing the result. + * + * @param shell the parent shell + * @param currentValue an initial value, if any + * @return the chosen string, or null */ - public String showDialog(Shell shell, String currentValue) { + @Nullable + public String showDialog(@NonNull Shell shell, @Nullable String currentValue) { // we need to get the project of the file being edited. UiElementNode uiNode = getUiParent(); AndroidXmlEditor editor = uiNode.getEditor(); @@ -154,17 +166,8 @@ public class UiResourceAttributeNode extends UiTextAttributeNode { if (mType != null) { // get the Target Data to get the system resources AndroidTargetData data = editor.getTargetData(); - ResourceRepository frameworkRepository = data.getFrameworkResources(); - - // open a resource chooser dialog for specified resource type. - ResourceChooser dlg = new ResourceChooser(project, - mType, - projectRepository, - frameworkRepository, - shell); - - dlg.setCurrentResource(currentValue); - + ResourceChooser dlg = ResourceChooser.create(project, mType, data, shell) + .setCurrentResource(currentValue); if (dlg.open() == Window.OK) { return dlg.getCurrentResource(); } @@ -329,6 +332,13 @@ public class UiResourceAttributeNode extends UiTextAttributeNode { if (resTypes.contains(ResourceType.ATTR) || resTypes.contains(ResourceType.STYLE)) { results.add(PREFIX_THEME_REF + ResourceType.ATTR.getName() + '/'); + if (prefix != null && prefix.startsWith(ANDROID_THEME_PREFIX)) { + // including attr isn't required + for (ResourceItem item : repository.getResourceItemsOfType( + ResourceType.ATTR)) { + results.add(ANDROID_THEME_PREFIX + item.getName()); + } + } } return results.toArray(new String[results.size()]); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java index bd6c079..d0ee92c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/values/ValuesContentAssist.java @@ -70,7 +70,7 @@ public class ValuesContentAssist extends AndroidContentAssist { } @Override - protected void computeAttributeValues(List<ICompletionProposal> proposals, int offset, + protected boolean computeAttributeValues(List<ICompletionProposal> proposals, int offset, String parentTagName, String attributeName, Node node, String wordPrefix, boolean skipEndTag, int replaceLength) { super.computeAttributeValues(proposals, offset, parentTagName, attributeName, node, @@ -129,10 +129,12 @@ public class ValuesContentAssist extends AndroidContentAssist { addMatchingProposals(proposals, sorted.toArray(), offset, node, wordPrefix, needTag, true /* isAttribute */, false /* isNew */, skipEndTag /* skipEndTag */, replaceLength); - return; + return true; } } } + + return false; } @Override diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java index d9aab14..4281f19 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java @@ -377,6 +377,9 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener AvdInfo preferredAvd = null; if (config.mAvdName != null) { preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/); + } + + if (preferredAvd != null) { IAndroidTarget preferredAvdTarget = preferredAvd.getTarget(); if (preferredAvdTarget != null && !preferredAvdTarget.getVersion().canRun(minApiVersion)) { @@ -606,7 +609,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener // open the chooser dialog. It'll fill 'response' with the device to use // or the AVD to launch. DeviceChooserDialog dialog = new DeviceChooserDialog( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), response, launchInfo.getPackageName(), desiredProjectTarget, minApiVersion); if (dialog.open() == Dialog.OK) { @@ -1377,7 +1380,7 @@ public final class AndroidLaunchController implements IDebugBridgeChangeListener } } } catch (CoreException e) { - MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(), + MessageDialog.openError(AdtPlugin.getShell(), "Launch Error", e.getStatus().getMessage()); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java index 80dac65..b047b1b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java @@ -25,6 +25,7 @@ import static org.eclipse.jdt.core.dom.SingleMemberAnnotation.VALUE_PROPERTY; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.sdk.SdkVersionInfo; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.IconFactory; @@ -331,7 +332,8 @@ class AddSuppressAnnotation implements IMarkerResolution2 { } int api = -1; - if (id.equals(ApiDetector.UNSUPPORTED.getId())) { + if (id.equals(ApiDetector.UNSUPPORTED.getId()) || + id.equals(ApiDetector.INLINED.getId())) { String message = marker.getAttribute(IMarker.MESSAGE, null); if (message != null) { Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ @@ -343,7 +345,8 @@ class AddSuppressAnnotation implements IMarkerResolution2 { } Issue issue = EclipseLintClient.getRegistry().getIssue(id); - boolean isClassDetector = issue != null && issue.getScope().contains(Scope.CLASS_FILE); + boolean isClassDetector = issue != null && issue.getImplementation().getScope().contains( + Scope.CLASS_FILE); // Don't offer to suppress (with an annotation) the annotation checks if (issue == AnnotationDetector.ISSUE) { @@ -407,7 +410,7 @@ class AddSuppressAnnotation implements IMarkerResolution2 { // @TargetApi is only valid on methods and classes, not fields etc && (body instanceof MethodDeclaration || body instanceof TypeDeclaration)) { - String apiString = AdtUtils.getBuildCodes(api); + String apiString = SdkVersionInfo.getBuildCode(api); if (apiString == null) { apiString = Integer.toString(api); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java index 23943d5..b77b475 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAttribute.java @@ -17,15 +17,19 @@ package com.android.ide.eclipse.adt.internal.lint; import static com.android.SdkConstants.ATTR_IGNORE; +import static com.android.SdkConstants.ATTR_TARGET_API; import static com.android.SdkConstants.DOT_XML; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.sdk.SdkVersionInfo; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.tools.lint.checks.ApiDetector; +import com.google.common.collect.Lists; import org.eclipse.core.resources.IMarker; import org.eclipse.core.runtime.CoreException; @@ -38,6 +42,12 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Fix for adding {@code tools:ignore="id"} attributes in XML files. */ @@ -47,14 +57,26 @@ class AddSuppressAttribute implements ICompletionProposal { private final IMarker mMarker; private final Element mElement; private final String mDescription; + /** + * Should it create a {@code tools:targetApi} attribute instead of a + * {@code tools:ignore} attribute? If so pass a non null API level + */ + private final String mTargetApi; + - private AddSuppressAttribute(AndroidXmlEditor editor, String id, IMarker marker, - Element element, String description) { + private AddSuppressAttribute( + @NonNull AndroidXmlEditor editor, + @NonNull String id, + @NonNull IMarker marker, + @NonNull Element element, + @NonNull String description, + @Nullable String targetApi) { mEditor = editor; mId = id; mMarker = marker; mElement = element; mDescription = description; + mTargetApi = targetApi; } @Override @@ -84,7 +106,16 @@ class AddSuppressAttribute implements ICompletionProposal { @Override public void apply(IDocument document) { - AdtUtils.setToolsAttribute(mEditor, mElement, "Suppress Lint Warning", ATTR_IGNORE, mId, + String attribute; + String value; + if (mTargetApi != null) { + attribute = ATTR_TARGET_API; + value = mTargetApi; + } else { + attribute = ATTR_IGNORE; + value = mId; + } + AdtUtils.setToolsAttribute(mEditor, mElement, mDescription, attribute, value, true /*reveal*/, true /*append*/); try { @@ -92,7 +123,7 @@ class AddSuppressAttribute implements ICompletionProposal { // (so the user doesn't have to re-run lint just to see it disappear) mMarker.delete(); } catch (CoreException e) { - AdtPlugin.log(e, "Could not add suppress annotation"); + AdtPlugin.log(e, "Could not remove marker"); } } @@ -103,17 +134,17 @@ class AddSuppressAttribute implements ICompletionProposal { * @param editor the associated editor containing the marker * @param marker the marker to create fixes for * @param id the issue id - * @return a fix for this marker, or null if unable + * @return a list of fixes for this marker, possibly empty */ - @Nullable - public static AddSuppressAttribute createFix( + @NonNull + public static List<AddSuppressAttribute> createFixes( @NonNull AndroidXmlEditor editor, @NonNull IMarker marker, @NonNull String id) { // This only applies to XML files: String fileName = marker.getResource().getName(); if (!fileName.endsWith(DOT_XML)) { - return null; + return Collections.emptyList(); } int offset = marker.getAttribute(IMarker.CHAR_START, -1); @@ -127,7 +158,7 @@ class AddSuppressAttribute implements ICompletionProposal { node = DomUtilities.getNode(editor.getStructuredDocument(), offset); } if (node == null) { - return null; + return Collections.emptyList(); } Document document = node.getOwnerDocument(); while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { @@ -136,13 +167,41 @@ class AddSuppressAttribute implements ICompletionProposal { if (node == null) { node = document.getDocumentElement(); if (node == null) { - return null; + return Collections.emptyList(); } } String desc = String.format("Add ignore '%1$s\' to element", id); Element element = (Element) node; - return new AddSuppressAttribute(editor, id, marker, element, desc); + List<AddSuppressAttribute> fixes = Lists.newArrayListWithExpectedSize(2); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, desc, null)); + + int api = -1; + if (id.equals(ApiDetector.UNSUPPORTED.getId()) + || id.equals(ApiDetector.INLINED.getId())) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + api = Integer.parseInt(matcher.group(1)); + String targetApi; + String buildCode = SdkVersionInfo.getBuildCode(api); + if (buildCode != null) { + targetApi = buildCode.toLowerCase(Locale.US); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, + String.format("Add targetApi '%1$s\' to element", targetApi), + targetApi)); + } + targetApi = Integer.toString(api); + fixes.add(new AddSuppressAttribute(editor, id, marker, element, + String.format("Add targetApi '%1$s\' to element", targetApi), + targetApi)); + } + } + } + + return fixes; } /** @@ -170,7 +229,7 @@ class AddSuppressAttribute implements ICompletionProposal { node = node.getOwnerDocument().getDocumentElement(); String desc = String.format("Add ignore '%1$s\' to element", id); Element element = (Element) node; - return new AddSuppressAttribute(editor, id, marker, element, desc); + return new AddSuppressAttribute(editor, id, marker, element, desc, null); } return null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java index 44b676f..628972f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ConvertToDpFix.java @@ -54,7 +54,7 @@ final class ConvertToDpFix extends DocumentFix implements IInputValidator { @Override protected void apply(IDocument document, IStructuredModel model, Node node, int start, int end) { - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); InputDensityDialog densityDialog = new InputDensityDialog(shell); if (densityDialog.open() == Window.OK) { int dpi = densityDialog.getDensity(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java new file mode 100644 index 0000000..9a5456b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/DosLineEndingsFix.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 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.lint; + +import com.android.ide.eclipse.adt.AdtPlugin; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + +/** Quickfix for correcting line endings in the file */ +class DosLineEndingsFix extends LintFix { + + protected DosLineEndingsFix(String id, IMarker marker) { + super(id, marker); + } + + @Override + public boolean needsFocus() { + return false; + } + + @Override + public boolean isCancelable() { + return false; + } + + @Override + public String getDisplayString() { + return "Fix line endings"; + } + + @Override + public void apply(IDocument document) { + char next = 0; + for (int i = document.getLength() - 1; i >= 0; i--) { + try { + char c = document.getChar(i); + if (c == '\r' && next != '\n') { + document.replace(i, 1, "\n"); //$NON-NLS-1$ + } + next = c; + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + return; + } + } + + deleteMarker(); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java index b3303b3..45ae2c5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintClient.java @@ -37,6 +37,7 @@ import com.android.tools.lint.client.api.IDomParser; import com.android.tools.lint.client.api.IJavaParser; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.client.api.LintClient; +import com.android.tools.lint.detector.api.ClassContext; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.DefaultPosition; import com.android.tools.lint.detector.api.Detector; @@ -59,9 +60,13 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.compiler.CompilationResult; import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; @@ -100,6 +105,7 @@ import java.util.List; import java.util.Map; import java.util.WeakHashMap; +import lombok.ast.TypeReference; import lombok.ast.ecj.EcjTreeConverter; import lombok.ast.grammar.ParseProblem; import lombok.ast.grammar.Source; @@ -282,6 +288,19 @@ public class EclipseLintClient extends LintClient implements IDomParser { return null; } + @Override + @NonNull + public String getProjectName(@NonNull Project project) { + // Initialize the lint project's name to the name of the Eclipse project, + // which might differ from the directory name + IProject eclipseProject = getProject(project); + if (eclipseProject != null) { + return eclipseProject.getName(); + } + + return super.getProjectName(project); + } + @NonNull @Override public Configuration getConfiguration(@NonNull Project project) { @@ -526,6 +545,9 @@ public class EclipseLintClient extends LintClient implements IDomParser { */ private static Pair<Integer, Integer> adjustOffsets(IDocument doc, int startOffset, int endOffset) { + int originalStart = startOffset; + int originalEnd = endOffset; + if (doc != null) { while (endOffset > startOffset && endOffset < doc.getLength()) { try { @@ -547,6 +569,9 @@ public class EclipseLintClient extends LintClient implements IDomParser { char c = doc.getChar(lineEnd); if (c == '\n' || c == '\r') { endOffset = lineEnd; + if (endOffset > 0 && doc.getChar(endOffset - 1) == '\r') { + endOffset--; + } break; } } catch (BadLocationException e) { @@ -557,6 +582,13 @@ public class EclipseLintClient extends LintClient implements IDomParser { } } + if (startOffset >= endOffset) { + // Selecting nothing (for example, for the mangled CRLF delimiter issue selecting + // just the newline) + // In that case, use the real range + return Pair.of(originalStart, originalEnd); + } + return Pair.of(startOffset, endOffset); } @@ -583,8 +615,8 @@ public class EclipseLintClient extends LintClient implements IDomParser { return ""; } - String summary = issue.getDescription(); - String explanation = issue.getExplanationAsSimpleText(); + String summary = issue.getDescription(Issue.OutputFormat.TEXT); + String explanation = issue.getExplanation(Issue.OutputFormat.TEXT); StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20); try { @@ -872,6 +904,125 @@ public class EclipseLintClient extends LintClient implements IDomParser { return Sdk.getCurrent().getTargets(); } + private boolean mSearchForSuperClasses; + + /** + * Sets whether this client should search for super types on its own. This + * is typically not needed when doing a full lint run (because lint will + * look at all classes and libraries), but is useful during incremental + * analysis when lint is only looking at a subset of classes. In that case, + * we want to use Eclipse's data structures for super classes. + * + * @param search whether to use a custom Eclipse search for super class + * names + */ + public void setSearchForSuperClasses(boolean search) { + mSearchForSuperClasses = search; + } + + /** + * Whether this lint client is searching for super types. See + * {@link #setSearchForSuperClasses(boolean)} for details. + * + * @return whether the client will search for super types + */ + public boolean getSearchForSuperClasses() { + return mSearchForSuperClasses; + } + + @Override + @Nullable + public String getSuperClass(@NonNull Project project, @NonNull String name) { + if (!mSearchForSuperClasses) { + // Super type search using the Eclipse index is potentially slow, so + // only do this when necessary + return null; + } + + IProject eclipseProject = getProject(project); + if (eclipseProject == null) { + return null; + } + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); + if (javaProject == null) { + return null; + } + + String typeFqcn = ClassContext.getFqcn(name); + IType type = javaProject.findType(typeFqcn); + if (type != null) { + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType superType = hierarchy.getSuperclass(type); + if (superType != null) { + String key = superType.getKey(); + if (!key.isEmpty() + && key.charAt(0) == 'L' + && key.charAt(key.length() - 1) == ';') { + return key.substring(1, key.length() - 1); + } else { + String fqcn = superType.getFullyQualifiedName(); + return ClassContext.getInternalName(fqcn); + } + } + } + } catch (JavaModelException e) { + log(Severity.INFORMATIONAL, e, null); + } catch (CoreException e) { + log(Severity.INFORMATIONAL, e, null); + } + + return null; + } + + @Override + @Nullable + public Boolean isSubclassOf( + @NonNull Project project, + @NonNull String name, @NonNull + String superClassName) { + if (!mSearchForSuperClasses) { + // Super type search using the Eclipse index is potentially slow, so + // only do this when necessary + return null; + } + + IProject eclipseProject = getProject(project); + if (eclipseProject == null) { + return null; + } + + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); + if (javaProject == null) { + return null; + } + + String typeFqcn = ClassContext.getFqcn(name); + IType type = javaProject.findType(typeFqcn); + if (type != null) { + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + IType[] allSupertypes = hierarchy.getAllSuperclasses(type); + if (allSupertypes != null) { + String target = 'L' + superClassName + ';'; + for (IType superType : allSupertypes) { + if (target.equals(superType.getKey())) { + return Boolean.TRUE; + } + } + return Boolean.FALSE; + } + } + } catch (JavaModelException e) { + log(Severity.INFORMATIONAL, e, null); + } catch (CoreException e) { + log(Severity.INFORMATIONAL, e, null); + } + + return null; + } + private static class LazyLocation extends Location implements Location.Handle { private final IStructuredDocument mDocument; private final IndexedRegion mRegion; @@ -1063,6 +1214,19 @@ public class EclipseLintClient extends LintClient implements IDomParser { @NonNull lombok.ast.Node compilationUnit) { } + @Override + @Nullable + public lombok.ast.Node resolve(@NonNull JavaContext context, + @NonNull lombok.ast.Node node) { + return null; + } + + @Override + @Nullable + public TypeReference getType(@NonNull JavaContext context, @NonNull lombok.ast.Node node) { + return null; + } + /* Handle for creating positions cheaply and returning full fledged locations later */ private class LocationHandle implements Handle { private File mFile; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java index d69412b..43cd48d 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/EclipseLintRunner.java @@ -127,7 +127,9 @@ public class EclipseLintRunner { @Nullable IResource source, boolean show) { if (resources != null && !resources.isEmpty()) { - resources = addLibraries(resources); + if (!AdtPrefs.getPrefs().getSkipLibrariesFromLint()) { + resources = addLibraries(resources); + } cancelCurrentJobs(false); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java index 196743b..7eafd43 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/ExtractStringFix.java @@ -16,7 +16,7 @@ package com.android.ide.eclipse.adt.internal.lint; import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; @@ -28,6 +28,7 @@ import org.eclipse.jface.text.TextSelection; import org.eclipse.ltk.ui.refactoring.RefactoringWizard; import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.IEditorPart; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; @@ -59,16 +60,13 @@ final class ExtractStringFix extends DocumentFix { @Override protected void apply(IDocument document, IStructuredModel model, Node node, int start, int end) { - // Invoke refactoring - LayoutEditorDelegate delegate = - LayoutEditorDelegate.fromEditor(AdtUtils.getActiveEditor()); - - if (delegate != null) { + IEditorPart editorPart = AdtUtils.getActiveEditor(); + if (editorPart instanceof AndroidXmlEditor) { IFile file = (IFile) mMarker.getResource(); ITextSelection selection = new TextSelection(start, end - start); ExtractStringRefactoring refactoring = - new ExtractStringRefactoring(file, delegate.getEditor(), selection); + new ExtractStringRefactoring(file, editorPart, selection); RefactoringWizard wizard = new ExtractStringWizard(refactoring, file.getProject()); RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); try { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java index df8d9af..ebb9a59 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintDeltaProcessor.java @@ -167,9 +167,10 @@ public class LintDeltaProcessor implements Runnable { @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { - if (flags == IResourceDelta.MARKERS) { - // ONLY the markers changed. Ignore these since they happen + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject || flags == IResourceDelta.MARKERS) { + // If not an Android project or ONLY the markers changed. + // Ignore these since they happen // when we add markers for lint errors found in the current file, // which would cause us to repeatedly enter this method over and over // again. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java index 7683f8d..401703e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFix.java @@ -21,9 +21,11 @@ import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.tools.lint.checks.AccessibilityDetector; import com.android.tools.lint.checks.DetectMissingPrefix; +import com.android.tools.lint.checks.DosLineEndingDetector; import com.android.tools.lint.checks.HardcodedValuesDetector; import com.android.tools.lint.checks.InefficientWeightDetector; import com.android.tools.lint.checks.ManifestOrderDetector; +import com.android.tools.lint.checks.MissingIdDetector; import com.android.tools.lint.checks.ObsoleteLayoutParamsDetector; import com.android.tools.lint.checks.PxUsageDetector; import com.android.tools.lint.checks.ScrollViewChildDetector; @@ -35,6 +37,7 @@ import com.android.tools.lint.checks.TypographyDetector; import com.android.tools.lint.checks.UseCompoundDrawableDetector; import com.android.tools.lint.checks.UselessViewDetector; import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Issue.OutputFormat; import org.eclipse.core.resources.IMarker; import org.eclipse.core.runtime.CoreException; @@ -107,7 +110,7 @@ abstract class LintFix implements ICompletionProposal { public String getAdditionalProposalInfo() { Issue issue = EclipseLintClient.getRegistry().getIssue(mId); if (issue != null) { - return issue.getExplanationAsHtml(); + return issue.getExplanation(OutputFormat.HTML); } return null; @@ -150,6 +153,7 @@ abstract class LintFix implements ICompletionProposal { sFixes.put(AccessibilityDetector.ISSUE.getId(), SetAttributeFix.class); sFixes.put(InefficientWeightDetector.BASELINE_WEIGHTS.getId(), SetAttributeFix.class); sFixes.put(ManifestOrderDetector.ALLOW_BACKUP.getId(), SetAttributeFix.class); + sFixes.put(MissingIdDetector.ISSUE.getId(), SetAttributeFix.class); sFixes.put(HardcodedValuesDetector.ISSUE.getId(), ExtractStringFix.class); sFixes.put(UselessViewDetector.USELESS_LEAF.getId(), RemoveUselessViewFix.class); sFixes.put(UselessViewDetector.USELESS_PARENT.getId(), RemoveUselessViewFix.class); @@ -168,6 +172,7 @@ abstract class LintFix implements ICompletionProposal { sFixes.put(UseCompoundDrawableDetector.ISSUE.getId(), UseCompoundDrawableDetectorFix.class); sFixes.put(TypoDetector.ISSUE.getId(), TypoFix.class); + sFixes.put(DosLineEndingDetector.ISSUE.getId(), DosLineEndingsFix.class); // ApiDetector.UNSUPPORTED is provided as a marker resolution rather than // a quick assistant (the marker resolution adds a suitable @TargetApi annotation) } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java index 5d2a2bb..ce5fd55 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintFixGenerator.java @@ -27,6 +27,7 @@ import com.android.tools.lint.client.api.Configuration; import com.android.tools.lint.client.api.DefaultConfiguration; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Issue.OutputFormat; import com.android.tools.lint.detector.api.Project; import com.android.tools.lint.detector.api.Severity; import com.android.utils.SdkUtils; @@ -189,11 +190,7 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi String message = marker.getAttribute(IMarker.MESSAGE, null); proposals.add(new MoreInfoProposal(id, message)); - ICompletionProposal fix = AddSuppressAttribute.createFix(editor, marker, id); - if (fix != null) { - proposals.add(fix); - } - + proposals.addAll(AddSuppressAttribute.createFixes(editor, marker, id)); proposals.add(new SuppressProposal(file, id, false)); proposals.add(new SuppressProposal(file.getProject(), id, true /* all */)); proposals.add(new SuppressProposal(file, id, true /* all */)); @@ -296,11 +293,11 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi assert isXml; if (part instanceof AndroidXmlEditor) { AndroidXmlEditor editor = (AndroidXmlEditor) part; - AddSuppressAttribute fix = AddSuppressAttribute.createFix(editor, + List<AddSuppressAttribute> fixes = AddSuppressAttribute.createFixes(editor, marker, id); - if (fix != null) { + if (fixes.size() > 0) { IStructuredDocument document = editor.getStructuredDocument(); - fix.apply(document); + fixes.get(0).apply(document); } } } @@ -492,11 +489,12 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi sb.append('\n').append('\n'); sb.append("Issue Explanation:"); sb.append('\n'); - if (issue.getExplanation() != null) { + String explanation = issue.getExplanation(Issue.OutputFormat.TEXT); + if (explanation != null && !explanation.isEmpty()) { sb.append('\n'); - sb.append(issue.getExplanationAsSimpleText()); + sb.append(explanation); } else { - sb.append(issue.getDescription()); + sb.append(issue.getDescription(Issue.OutputFormat.TEXT)); } if (issue.getMoreInfo() != null) { @@ -505,13 +503,13 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi sb.append(issue.getMoreInfo()); } - MessageDialog.openInformation(AdtPlugin.getDisplay().getActiveShell(), "More Info", + MessageDialog.openInformation(AdtPlugin.getShell(), "More Info", sb.toString()); } @Override public String getDisplayString() { - return "Explain Issue"; + return String.format("Explain Issue (%1$s)", mId); } // ---- Implements MarkerResolution2 ---- @@ -547,7 +545,8 @@ public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssi public String getAdditionalProposalInfo() { return "Provides more information about this issue." + "<br><br>" //$NON-NLS-1$ - + EclipseLintClient.getRegistry().getIssue(mId).getExplanationAsHtml(); + + EclipseLintClient.getRegistry().getIssue(mId).getExplanation( + OutputFormat.HTML); } @Override diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java index 2851c47..bd1eb72 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/LintJob.java @@ -140,10 +140,11 @@ final class LintJob extends Job { if (issue == null) { continue; } - if (issue.isAdequate(scope)) { + if (issue.getImplementation().isAdequate(scope)) { marker.delete(); } } + mClient.setSearchForSuperClasses(true); } else { EclipseLintClient.clearMarkers(mResources); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java index a0d414e..1de903e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/RunLintAction.java @@ -21,6 +21,7 @@ import static com.android.SdkConstants.DOT_XML; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.tools.lint.detector.api.LintUtils; import org.eclipse.core.resources.IFile; @@ -80,7 +81,7 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, List<IProject> projects = AdtUtils.getSelectedProjects(selection); if (projects.isEmpty() && warn) { - MessageDialog.openWarning(AdtPlugin.getDisplay().getActiveShell(), "Lint", + MessageDialog.openWarning(AdtPlugin.getShell(), "Lint", "Could not run Lint: Select an Android project first."); } @@ -110,7 +111,8 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, IconFactory iconFactory = IconFactory.getInstance(); ImageDescriptor allIcon = iconFactory.getImageDescriptor("lintrun"); //$NON-NLS-1$ - LintMenuAction allAction = new LintMenuAction("Check All Projects", allIcon, false, null); + LintMenuAction allAction = new LintMenuAction("Check All Projects", allIcon, + ACTION_RUN, null); addAction(allAction); addSeparator(); @@ -121,7 +123,7 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, IProject p = project.getProject(); ImageDescriptor icon = ImageDescriptor.createFromImage(provider.getImage(p)); String label = String.format("Check %1$s", p.getName()); - LintMenuAction projectAction = new LintMenuAction(label, icon, false, p); + LintMenuAction projectAction = new LintMenuAction(label, icon, ACTION_RUN, p); addAction(projectAction); } @@ -131,7 +133,8 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, // Currently only supported for XML files if (file != null && LintUtils.endsWith(file.getName(), DOT_XML)) { ImageDescriptor icon = ImageDescriptor.createFromImage(provider.getImage(file)); - IAction fileAction = new LintMenuAction("Check Current File", icon, false, file); + IAction fileAction = new LintMenuAction("Check Current File", icon, ACTION_RUN, + file); addSeparator(); addAction(fileAction); @@ -140,10 +143,17 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); ImageDescriptor clear = images.getImageDescriptor(ISharedImages.IMG_ELCL_REMOVEALL); - LintMenuAction clearAction = new LintMenuAction("Clear Lint Warnings", clear, true, null); + LintMenuAction clearAction = new LintMenuAction("Clear Lint Warnings", clear, ACTION_CLEAR, + null); addSeparator(); addAction(clearAction); + LintMenuAction excludeAction = new LintMenuAction("Skip Library Project Dependencies", + allIcon, ACTION_TOGGLE_EXCLUDE, null); + addSeparator(); + addAction(excludeAction); + excludeAction.setChecked(AdtPrefs.getPrefs().getSkipLibrariesFromLint()); + return mMenu; } @@ -161,32 +171,44 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, return null; } + private static final int ACTION_RUN = 1; + private static final int ACTION_CLEAR = 2; + private static final int ACTION_TOGGLE_EXCLUDE = 3; + /** * Actions in the pulldown context menu: run lint or clear lint markers on * the given resource */ private static class LintMenuAction extends Action { - private final boolean mClear; private final IResource mResource; + private final int mAction; /** * Creates a new context menu action * * @param text the label * @param descriptor the icon - * @param clear if true, clear lint markers otherwise check the resource + * @param action the action to run: run lint, clear, or toggle exclude libraries * @param resource the resource to check or clear markers for, where * null means all projects */ - private LintMenuAction(String text, ImageDescriptor descriptor, boolean clear, + private LintMenuAction(String text, ImageDescriptor descriptor, int action, IResource resource) { - super(text, descriptor); - mClear = clear; + super(text, action == ACTION_TOGGLE_EXCLUDE ? AS_CHECK_BOX : AS_PUSH_BUTTON); + if (descriptor != null) { + setImageDescriptor(descriptor); + } + mAction = action; mResource = resource; } @Override public void run() { + if (mAction == ACTION_TOGGLE_EXCLUDE) { + AdtPrefs prefs = AdtPrefs.getPrefs(); + prefs.setSkipLibrariesFromLint(!prefs.getSkipLibrariesFromLint()); + return; + } List<IResource> resources = new ArrayList<IResource>(); if (mResource == null) { // All projects @@ -198,9 +220,10 @@ public class RunLintAction implements IObjectActionDelegate, IMenuCreator, resources.add(mResource); } EclipseLintRunner.cancelCurrentJobs(false); - if (mClear) { + if (mAction == ACTION_CLEAR) { EclipseLintClient.clearMarkers(resources); } else { + assert mAction == ACTION_RUN; EclipseLintRunner.startLint(resources, null, null, false /*fatalOnly*/, true /*show*/); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java index 1d743b8..627eeb8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetAttributeFix.java @@ -15,22 +15,31 @@ */ package com.android.ide.eclipse.adt.internal.lint; +import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_ALLOW_BACKUP; import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION; +import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_INPUT_TYPE; import static com.android.SdkConstants.ATTR_PERMISSION; import static com.android.SdkConstants.ATTR_TRANSLATABLE; +import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.VALUE_FALSE; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.tools.lint.checks.AccessibilityDetector; import com.android.tools.lint.checks.InefficientWeightDetector; import com.android.tools.lint.checks.ManifestOrderDetector; +import com.android.tools.lint.checks.MissingIdDetector; import com.android.tools.lint.checks.SecurityDetector; import com.android.tools.lint.checks.TextFieldDetector; import com.android.tools.lint.checks.TranslationDetector; import org.eclipse.core.resources.IMarker; +import org.eclipse.ui.IEditorPart; +import org.w3c.dom.Element; /** Shared fix class for various builtin attributes */ final class SetAttributeFix extends SetPropertyFix { @@ -52,6 +61,8 @@ final class SetAttributeFix extends SetPropertyFix { return ATTR_TRANSLATABLE; } else if (mId.equals(ManifestOrderDetector.ALLOW_BACKUP.getId())) { return ATTR_ALLOW_BACKUP; + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + return ATTR_ID; } else { assert false : mId; return ""; @@ -81,6 +92,8 @@ final class SetAttributeFix extends SetPropertyFix { return "Mark this as a non-translatable resource"; } else if (mId.equals(ManifestOrderDetector.ALLOW_BACKUP.getId())) { return "Set the allowBackup attribute to true or false"; + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + return "Set the ID attribute"; } else { assert false : mId; return ""; @@ -115,13 +128,24 @@ final class SetAttributeFix extends SetPropertyFix { } @Override - protected String getProposal() { + protected String getProposal(Element element) { if (mId.equals(InefficientWeightDetector.BASELINE_WEIGHTS.getId())) { return VALUE_FALSE; } else if (mId.equals(TranslationDetector.MISSING.getId())) { return VALUE_FALSE; + } else if (mId.equals(TextFieldDetector.ISSUE.getId())) { + return element.getAttributeNS(ANDROID_URI, ATTR_INPUT_TYPE); + } else if (mId.equals(MissingIdDetector.ISSUE.getId())) { + IEditorPart editor = AdtUtils.getActiveEditor(); + if (editor instanceof AndroidXmlEditor) { + AndroidXmlEditor xmlEditor = (AndroidXmlEditor) editor; + return DescriptorsUtils.getFreeWidgetId(xmlEditor.getUiRootNode(), + "fragment"); //$NON-NLS-1$ + } else { + return NEW_ID_PREFIX; + } } - return super.getProposal(); + return super.getProposal(element); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java index ee049ca..a2b79c3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/SetPropertyFix.java @@ -51,7 +51,7 @@ abstract class SetPropertyFix extends DocumentFix { /** Whether it's in the android: namespace */ protected abstract boolean isAndroidAttribute(); - protected String getProposal() { + protected String getProposal(Element element) { return invokeCodeCompletion() ? "" : "TODO"; //$NON-NLS-1$ } @@ -71,7 +71,7 @@ abstract class SetPropertyFix extends DocumentFix { if (node instanceof Element) { Element element = (Element) node; - String proposal = getProposal(); + String proposal = getProposal(element); String localAttribute = getAttribute(); String prefix = null; if (isAndroidAttribute()) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java index 4358410..8a83364 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/TypoFix.java @@ -105,7 +105,7 @@ final class TypoFix extends DocumentFix { String message = mMarker.getAttribute(IMarker.MESSAGE, ""); String typo = TypoDetector.getTypo(message); List<String> replacements = TypoDetector.getSuggestions(message); - if (!replacements.isEmpty() && typo != null) { + if (replacements != null && !replacements.isEmpty() && typo != null) { List<LintFix> allFixes = new ArrayList<LintFix>(replacements.size()); for (String replacement : replacements) { TypoFix fix = new TypoFix(mId, mMarker); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java index 8526ad9..aed4bd4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AdtPrefs.java @@ -17,9 +17,11 @@ package com.android.ide.eclipse.adt.internal.preferences; +import com.android.annotations.NonNull; +import com.android.ide.common.xml.XmlAttributeSortOrder; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewMode; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.internal.build.DebugKeyProvider; import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; @@ -30,6 +32,7 @@ import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.PropertyChangeEvent; import java.io.File; +import java.util.Locale; public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_SDK_DIR = AdtPlugin.PLUGIN_ID + ".sdk"; //$NON-NLS-1$ @@ -69,6 +72,10 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { public final static String PREFS_LINT_SEVERITIES = AdtPlugin.PLUGIN_ID + ".lintSeverities"; //$NON-NLS-1$ public final static String PREFS_FIX_LEGACY_EDITORS = AdtPlugin.PLUGIN_ID + ".fixLegacyEditors"; //$NON-NLS-1$ public final static String PREFS_SHARED_LAYOUT_EDITOR = AdtPlugin.PLUGIN_ID + ".sharedLayoutEditor"; //$NON-NLS-1$ + public final static String PREFS_PREVIEWS = AdtPlugin.PLUGIN_ID + ".previews"; //$NON-NLS-1$ + public final static String PREFS_SKIP_LINT_LIBS = AdtPlugin.PLUGIN_ID + ".skipLintLibs"; //$NON-NLS-1$ + public final static String PREFS_AUTO_PICK_TARGET = AdtPlugin.PLUGIN_ID + ".autoPickTarget"; //$NON-NLS-1$ + public final static String PREFS_REFACTOR_IDS = AdtPlugin.PLUGIN_ID + ".refactorIds"; //$NON-NLS-1$ /** singleton instance */ private final static AdtPrefs sThis = new AdtPrefs(); @@ -97,9 +104,12 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { private boolean mFormatOnSave; private boolean mLintOnSave; private boolean mLintOnExport; - private AttributeSortOrder mAttributeSort; + private XmlAttributeSortOrder mAttributeSort; private boolean mSharedLayoutEditor; + private boolean mAutoPickTarget; + private RenderPreviewMode mPreviewMode = RenderPreviewMode.NONE; private int mPreferXmlEditor; + private boolean mSkipLibrariesFromLint; public static enum BuildVerbosity { /** Build verbosity "Always". Those messages are always displayed, even in silent mode */ @@ -226,11 +236,11 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { if (property == null || PREFS_ATTRIBUTE_SORT.equals(property)) { String order = mStore.getString(PREFS_ATTRIBUTE_SORT); - mAttributeSort = AttributeSortOrder.LOGICAL; - if (AttributeSortOrder.ALPHABETICAL.key.equals(order)) { - mAttributeSort = AttributeSortOrder.ALPHABETICAL; - } else if (AttributeSortOrder.NO_SORTING.key.equals(order)) { - mAttributeSort = AttributeSortOrder.NO_SORTING; + mAttributeSort = XmlAttributeSortOrder.LOGICAL; + if (XmlAttributeSortOrder.ALPHABETICAL.key.equals(order)) { + mAttributeSort = XmlAttributeSortOrder.ALPHABETICAL; + } else if (XmlAttributeSortOrder.NO_SORTING.key.equals(order)) { + mAttributeSort = XmlAttributeSortOrder.NO_SORTING; } } @@ -254,6 +264,25 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { mSharedLayoutEditor = mStore.getBoolean(PREFS_SHARED_LAYOUT_EDITOR); } + if (property == null || PREFS_AUTO_PICK_TARGET.equals(property)) { + mAutoPickTarget = mStore.getBoolean(PREFS_AUTO_PICK_TARGET); + } + + if (property == null || PREFS_PREVIEWS.equals(property)) { + mPreviewMode = RenderPreviewMode.NONE; + String previewMode = mStore.getString(PREFS_PREVIEWS); + if (previewMode != null && !previewMode.isEmpty()) { + try { + mPreviewMode = RenderPreviewMode.valueOf(previewMode.toUpperCase(Locale.US)); + } catch (IllegalArgumentException iae) { + // Ignore: Leave it as RenderPreviewMode.NONE + } + } + } + + if (property == null || PREFS_SKIP_LINT_LIBS.equals(property)) { + mSkipLibrariesFromLint = mStore.getBoolean(PREFS_SKIP_LINT_LIBS); + } } /** @@ -330,13 +359,13 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { /** * Returns the sort order to be applied to the attributes (one of which can - * be {@link AttributeSortOrder#NO_SORTING}). + * be {@link com.android.ide.common.xml.XmlAttributeSortOrder#NO_SORTING}). * * @return the sort order to apply to the attributes */ - public AttributeSortOrder getAttributeSort() { + public XmlAttributeSortOrder getAttributeSort() { if (mAttributeSort == null) { - return AttributeSortOrder.LOGICAL; + return XmlAttributeSortOrder.LOGICAL; } return mAttributeSort; } @@ -344,7 +373,7 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { /** * Returns whether a space should be inserted before the closing {@code >} * character in open tags and before the closing {@code />} characters in - * empty tag. Note that the {@link XmlFormatStyle#RESOURCE} style overrides + * empty tag. Note that the {@link com.android.ide.common.xml.XmlFormatStyle#RESOURCE} style overrides * this setting to make it more compact for the {@code <item>} elements. * * @return true if an empty space should be inserted before {@code >} or @@ -485,13 +514,15 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { store.setDefault(PREFS_SPACE_BEFORE_CLOSE, true); store.setDefault(PREFS_LINT_ON_SAVE, true); store.setDefault(PREFS_LINT_ON_EXPORT, true); + store.setDefault(PREFS_AUTO_PICK_TARGET, true); // Defaults already handled; no need to write into map: - //store.setDefault(PREFS_ATTRIBUTE_SORT, AttributeSortOrder.LOGICAL.key); + //store.setDefault(PREFS_ATTRIBUTE_SORT, XmlAttributeSortOrder.LOGICAL.key); //store.setDefault(PREFS_USE_ECLIPSE_INDENT, false); //store.setDefault(PREVS_REMOVE_EMPTY_LINES, false); //store.setDefault(PREFS_FORMAT_ON_SAVE, false); //store.setDefault(PREFS_SHARED_LAYOUT_EDITOR, false); + //store.setDefault(PREFS_SKIP_LINT_LIBS, false); try { store.setDefault(PREFS_DEFAULT_DEBUG_KEYSTORE, @@ -538,4 +569,79 @@ public final class AdtPrefs extends AbstractPreferenceInitializer { store.setValue(PREFS_PREFER_XML, xml); } } + + /** + * Gets the {@link RenderPreviewMode} + * + * @return the preview mode + */ + @NonNull + public RenderPreviewMode getRenderPreviewMode() { + return mPreviewMode; + } + + /** + * Sets the {@link RenderPreviewMode} + * + * @param previewMode the preview mode + */ + public void setPreviewMode(@NonNull RenderPreviewMode previewMode) { + mPreviewMode = previewMode; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + if (previewMode != RenderPreviewMode.NONE) { + store.setValue(PREFS_PREVIEWS, previewMode.name().toLowerCase(Locale.US)); + } else { + store.setToDefault(PREFS_PREVIEWS); + } + } + + /** + * Sets whether auto-pick render target mode is enabled. + * + * @return whether the layout editor should automatically pick the best render target + */ + public boolean isAutoPickRenderTarget() { + return mAutoPickTarget; + } + + /** + * Sets whether auto-pick render target mode is enabled. + * + * @param autoPick if true, auto pick the best render target in the layout editor + */ + public void setAutoPickRenderTarget(boolean autoPick) { + mAutoPickTarget = autoPick; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + if (autoPick) { + store.setToDefault(PREFS_AUTO_PICK_TARGET); + } else { + store.setValue(PREFS_AUTO_PICK_TARGET, autoPick); + } + } + + /** + * Sets whether libraries should be excluded when running lint on a project + * + * @param exclude if true, exclude library projects + */ + public void setSkipLibrariesFromLint(boolean exclude) { + if (exclude != mSkipLibrariesFromLint) { + mSkipLibrariesFromLint = exclude; + IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); + if (exclude) { + store.setValue(PREFS_SKIP_LINT_LIBS, true); + } else { + store.setToDefault(PREFS_SKIP_LINT_LIBS); + } + } + } + + /** + * Returns whether libraries should be excluded when running lint on a project + * + * @return if true, exclude library projects + */ + public boolean getSkipLibrariesFromLint() { + return mSkipLibrariesFromLint; + } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AttributeSortOrder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AttributeSortOrder.java deleted file mode 100644 index b743014..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/AttributeSortOrder.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2011 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.preferences; - -import static com.android.SdkConstants.XMLNS; - -import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode; - -import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; -import org.w3c.dom.Attr; - -import java.util.Comparator; - -/** Order to use when sorting attributes */ -@SuppressWarnings("restriction") // IndexedRegion -public enum AttributeSortOrder { - NO_SORTING("none"), //$NON-NLS-1$ - ALPHABETICAL("alpha"), //$NON-NLS-1$ - LOGICAL("logical"); //$NON-NLS-1$ - - AttributeSortOrder(String key) { - this.key = key; - } - - public final String key; - - /** - * @return a comparator for use by this attribute sort order - */ - public Comparator<Attr> getAttributeComparator() { - switch (this) { - case ALPHABETICAL: - return ALPHABETICAL_COMPARATOR; - case NO_SORTING: - return EXISTING_ORDER_COMPARATOR; - case LOGICAL: - default: - return SORTED_ORDER_COMPARATOR; - } - } - - /** Comparator which can be used to sort attributes in the coding style priority order */ - private static final Comparator<Attr> SORTED_ORDER_COMPARATOR = new Comparator<Attr>() { - @Override - public int compare(Attr attr1, Attr attr2) { - // Namespace declarations should always go first - if (XMLNS.equals(attr1.getPrefix())) { - if (XMLNS.equals(attr2.getPrefix())) { - return 0; - } - return -1; - } else if (XMLNS.equals(attr2.getPrefix())) { - return 1; - } - - // Sort by preferred attribute order - return UiAttributeNode.compareAttributes( - attr1.getPrefix(), attr1.getLocalName(), - attr2.getPrefix(), attr2.getLocalName()); - } - }; - - /** - * Comparator which can be used to "sort" attributes into their existing source order - * (which is not the same as the node map iteration order in the DOM model) - */ - private static final Comparator<Attr> EXISTING_ORDER_COMPARATOR = new Comparator<Attr>() { - @Override - public int compare(Attr attr1, Attr attr2) { - IndexedRegion region1 = (IndexedRegion) attr1; - IndexedRegion region2 = (IndexedRegion) attr2; - - return region1.getStartOffset() - region2.getStartOffset(); - } - }; - - /** - * Comparator which can be used to sort attributes into alphabetical order (but xmlns - * is always first) - */ - private static final Comparator<Attr> ALPHABETICAL_COMPARATOR = new Comparator<Attr>() { - @Override - public int compare(Attr attr1, Attr attr2) { - // Namespace declarations should always go first - if (XMLNS.equals(attr1.getPrefix())) { - if (XMLNS.equals(attr2.getPrefix())) { - return 0; - } - return -1; - } else if (XMLNS.equals(attr2.getPrefix())) { - return 1; - } - - // Sort by name rather than localname to ensure we sort by namespaces first, - // then by names. - return attr1.getName().compareTo(attr2.getName()); - } - }; -}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/BuildPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/BuildPreferencePage.java index d5aa30a..f5f4633 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/BuildPreferencePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/BuildPreferencePage.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.preferences; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; +import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.internal.build.DebugKeyProvider; import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; @@ -25,6 +26,7 @@ import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; import org.eclipse.jface.preference.BooleanFieldEditor; import org.eclipse.jface.preference.FieldEditorPreferencePage; import org.eclipse.jface.preference.FileFieldEditor; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.RadioGroupFieldEditor; import org.eclipse.jface.preference.StringFieldEditor; import org.eclipse.swt.widgets.Composite; @@ -36,6 +38,7 @@ import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.PrivateKey; +import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Date; @@ -46,9 +49,22 @@ import java.util.Date; public class BuildPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + private IPreferenceStore mPrefStore = null; + + // default key store + private ReadOnlyFieldEditor mDefaultKeyStore = null; + private LabelField mDefaultFingerprintMd5 = null; + private LabelField mDefaultFingerprintSha1 = null; + + // custom key store + private KeystoreFieldEditor mCustomKeyStore = null; + private LabelField mCustomFingerprintMd5 = null; + private LabelField mCustomFingerprintSha1 = null; + public BuildPreferencePage() { super(GRID); - setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore()); + mPrefStore = AdtPlugin.getDefault().getPreferenceStore(); + setPreferenceStore(mPrefStore); setDescription(Messages.BuildPreferencePage_Title); } @@ -76,12 +92,90 @@ public class BuildPreferencePage extends FieldEditorPreferencePage implements getFieldEditorParent(), true); addField(rgfe); - addField(new ReadOnlyFieldEditor(AdtPrefs.PREFS_DEFAULT_DEBUG_KEYSTORE, - Messages.BuildPreferencePage_Default_KeyStore, getFieldEditorParent())); + // default debug keystore fingerprints + Fingerprints defaultFingerprints = getFingerprints( + mPrefStore.getString(AdtPrefs.PREFS_DEFAULT_DEBUG_KEYSTORE)); + + // default debug key store fields + mDefaultKeyStore = new ReadOnlyFieldEditor(AdtPrefs.PREFS_DEFAULT_DEBUG_KEYSTORE, + Messages.BuildPreferencePage_Default_KeyStore, getFieldEditorParent()); + mDefaultFingerprintMd5 = new LabelField( + Messages.BuildPreferencePage_Default_Certificate_Fingerprint_MD5, + defaultFingerprints != null ? defaultFingerprints.md5 : "", + getFieldEditorParent()); + mDefaultFingerprintSha1 = new LabelField( + Messages.BuildPreferencePage_Default_Certificate_Fingerprint_SHA1, + defaultFingerprints != null ? defaultFingerprints.sha1 : "", + getFieldEditorParent()); + + addField(mDefaultKeyStore); + addField(mDefaultFingerprintMd5); + addField(mDefaultFingerprintSha1); + + // custom debug keystore fingerprints + Fingerprints customFingerprints = null; + + String customDebugKeystorePath = mPrefStore.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); + if (new File(customDebugKeystorePath).isFile()) { + customFingerprints = getFingerprints(customDebugKeystorePath); + } else { + // file does not exist. + setErrorMessage("Not a valid keystore path."); + } - addField(new KeystoreFieldEditor(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE, - Messages.BuildPreferencePage_Custom_Keystore, getFieldEditorParent())); + // custom debug key store fields + mCustomKeyStore = new KeystoreFieldEditor(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE, + Messages.BuildPreferencePage_Custom_Keystore, getFieldEditorParent()); + mCustomFingerprintMd5 = new LabelField( + Messages.BuildPreferencePage_Default_Certificate_Fingerprint_MD5, + customFingerprints != null ? customFingerprints.md5 : "", + getFieldEditorParent()); + mCustomFingerprintSha1 = new LabelField( + Messages.BuildPreferencePage_Default_Certificate_Fingerprint_SHA1, + customFingerprints != null ? customFingerprints.sha1 : "", + getFieldEditorParent()); + + // set fingerprint fields + mCustomKeyStore.setFingerprintMd5Field(mCustomFingerprintMd5); + mCustomKeyStore.setFingerprintSha1Field(mCustomFingerprintSha1); + + addField(mCustomKeyStore); + addField(mCustomFingerprintMd5); + addField(mCustomFingerprintSha1); + } + /** + * MD5 & SHA1 fingerprints. + */ + private static class Fingerprints { + final String md5; + final String sha1; + + Fingerprints(String md5Val, String sha1Val) { + md5 = md5Val; + sha1 = sha1Val; + } + } + + private Fingerprints getFingerprints(String keystorePath) { + // attempt to load the debug key. + try { + DebugKeyProvider keyProvider = new DebugKeyProvider(keystorePath, + null /* storeType */, null /* key gen output */); + + return new Fingerprints( + FingerprintUtils.getFingerprint(keyProvider.getCertificate(), "MD5"), + FingerprintUtils.getFingerprint(keyProvider.getCertificate(), "SHA1")); + } catch (GeneralSecurityException e) { + setErrorMessage(e.getMessage()); + } catch (IOException e) { + setErrorMessage(e.getMessage()); + } catch (KeytoolException e) { + setErrorMessage(e.getMessage()); + } catch (AndroidLocationException e) { + setErrorMessage(e.getMessage()); + } + return null; } /* @@ -93,6 +187,27 @@ public class BuildPreferencePage extends FieldEditorPreferencePage implements public void init(IWorkbench workbench) { } + /* + * (non-Javadoc) + * + * @see org.eclipse.jface.preference.FieldEditorPreferencePage#performDefaults + * (org.eclipse.jface.preference.PreferencePage#performDefaults) + */ + @Override + protected void performDefaults() { + super.performDefaults(); + + // restore the default key store fingerprints + Fingerprints defaultFingerprints = getFingerprints(mPrefStore + .getString(AdtPrefs.PREFS_DEFAULT_DEBUG_KEYSTORE)); + mDefaultFingerprintMd5.setStringValue(defaultFingerprints.md5); + mDefaultFingerprintSha1.setStringValue(defaultFingerprints.sha1); + + // set custom fingerprint fields to blank + mCustomFingerprintMd5.setStringValue(""); + mCustomFingerprintSha1.setStringValue(""); + } + /** * A read-only string field editor. */ @@ -112,9 +227,50 @@ public class BuildPreferencePage extends FieldEditorPreferencePage implements } /** + * A read-only string field. + */ + private static class LabelField extends StringFieldEditor { + private String text; + + public LabelField(String labelText, String value, Composite parent) { + super("", labelText, parent); + text = value; + } + + @Override + protected void createControl(Composite parent) { + super.createControl(parent); + + Text control = getTextControl(); + control.setEditable(false); + } + + @Override + protected void doLoad() { + setStringValue(text); + } + + @Override + protected void doStore() { + // Do nothing + } + } + + /** * Custom {@link FileFieldEditor} that checks that the keystore is valid. */ private static class KeystoreFieldEditor extends FileFieldEditor { + private StringFieldEditor fingerprintMd5 = null; + private StringFieldEditor fingerprintSha1 = null; + + public void setFingerprintMd5Field(StringFieldEditor field) { + fingerprintMd5 = field; + } + + public void setFingerprintSha1Field(StringFieldEditor field) { + fingerprintSha1 = field; + } + public KeystoreFieldEditor(String name, String label, Composite parent) { super(name, label, parent); setValidateStrategy(VALIDATE_ON_KEY_STROKE); @@ -125,6 +281,14 @@ public class BuildPreferencePage extends FieldEditorPreferencePage implements String fileName = getTextControl().getText(); fileName = fileName.trim(); + if (fingerprintMd5 != null) { + fingerprintMd5.setStringValue(""); + } + + if (fingerprintSha1 != null) { + fingerprintSha1.setStringValue(""); + } + // empty values are considered ok. if (fileName.length() > 0) { File file = new File(fileName); @@ -141,6 +305,16 @@ public class BuildPreferencePage extends FieldEditorPreferencePage implements return false; } + if (fingerprintMd5 != null) { + fingerprintMd5.setStringValue( + FingerprintUtils.getFingerprint(certificate, "MD5")); + } + + if (fingerprintSha1 != null) { + fingerprintSha1.setStringValue( + FingerprintUtils.getFingerprint(certificate, "SHA1")); + } + Date today = new Date(); if (certificate.getNotAfter().compareTo(today) < 0) { showErrorMessage("Certificate is expired!"); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java index 0fcbaa0..d33b49f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/EditorsPage.java @@ -16,9 +16,9 @@ package com.android.ide.eclipse.adt.internal.preferences; -import static com.android.ide.eclipse.adt.internal.preferences.AttributeSortOrder.ALPHABETICAL; -import static com.android.ide.eclipse.adt.internal.preferences.AttributeSortOrder.LOGICAL; -import static com.android.ide.eclipse.adt.internal.preferences.AttributeSortOrder.NO_SORTING; +import static com.android.ide.common.xml.XmlAttributeSortOrder.ALPHABETICAL; +import static com.android.ide.common.xml.XmlAttributeSortOrder.LOGICAL; +import static com.android.ide.common.xml.XmlAttributeSortOrder.NO_SORTING; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java index 0bc2bfd..02af2fd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/LintPreferencePage.java @@ -15,6 +15,9 @@ */ package com.android.ide.eclipse.adt.internal.preferences; +import static com.android.tools.lint.detector.api.Issue.OutputFormat.RAW; +import static com.android.tools.lint.detector.api.Issue.OutputFormat.TEXT; + import com.android.annotations.NonNull; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; @@ -448,8 +451,8 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer Object data = item != null ? item.getData() : null; if (data instanceof Issue) { Issue issue = (Issue) data; - String summary = issue.getDescription(); - String explanation = issue.getExplanation(); + String summary = issue.getDescription(Issue.OutputFormat.TEXT); + String explanation = issue.getExplanation(Issue.OutputFormat.TEXT); StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20); sb.append(summary); @@ -568,7 +571,7 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer || issue.getCategory().getName().toLowerCase(Locale.US).startsWith(filter) || issue.getCategory().getFullName().toLowerCase(Locale.US).startsWith(filter) || issue.getId().toLowerCase(Locale.US).contains(filter) - || issue.getDescription().toLowerCase(Locale.US).contains(filter); + || issue.getDescription(RAW).toLowerCase(Locale.US).contains(filter); } private class ContentProvider extends TreeNodeContentProvider { @@ -703,7 +706,7 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer if (columnIndex == 0) { return ((Category) element).getFullName(); } else { - return ((Category) element).getExplanation(); + return null; } } @@ -712,7 +715,7 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer case 0: return issue.getId(); case 1: - return issue.getDescription(); + return issue.getDescription(TEXT); } return null; @@ -745,4 +748,4 @@ public class LintPreferencePage extends PropertyPage implements IWorkbenchPrefer return null; } } -}
\ No newline at end of file +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/Messages.java index 1474102..70b9751 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/Messages.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/Messages.java @@ -18,8 +18,16 @@ public class Messages extends NLS { public static String BuildPreferencePage_Custom_Keystore; + public static String BuildPreferencePage_Custom_Certificate_Fingerprint_MD5; + + public static String BuildPreferencePage_Custom_Certificate_Fingerprint_SHA1; + public static String BuildPreferencePage_Default_KeyStore; + public static String BuildPreferencePage_Default_Certificate_Fingerprint_MD5; + + public static String BuildPreferencePage_Default_Certificate_Fingerprint_SHA1; + public static String BuildPreferencePage_Normal; public static String BuildPreferencePage_Silent; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/messages.properties index 865ac19..61894fd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/messages.properties +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/preferences/messages.properties @@ -5,7 +5,11 @@ BuildPreferencePage_Silent=Silent BuildPreferencePage_Normal=Normal BuildPreferencePage_Verbose=Verbose BuildPreferencePage_Default_KeyStore=Default debug keystore: +BuildPreferencePage_Default_Certificate_Fingerprint_MD5=MD5 fingerprint: +BuildPreferencePage_Default_Certificate_Fingerprint_SHA1=SHA1 fingerprint: BuildPreferencePage_Custom_Keystore=Custom debug keystore: +BuildPreferencePage_Custom_Certificate_Fingerprint_MD5=MD5 fingerprint: +BuildPreferencePage_Custom_Certificate_Fingerprint_SHA1=SHA1 fingerprint: LaunchPreferencePage_Title=Launch Settings: LaunchPreferencePage_Default_Emu_Options=Default emulator options: LaunchPreferencePage_Default_HOME_Package=Default HOME package: diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java index bb1e40a..c526edf 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidClasspathContainerInitializer.java @@ -25,6 +25,8 @@ import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.IAndroidTarget.IOptionalLibrary; +import com.google.common.collect.Maps; +import com.google.common.io.Closeables; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; @@ -51,8 +53,10 @@ import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; import java.util.HashSet; +import java.util.Map; import java.util.regex.Pattern; /** @@ -526,22 +530,36 @@ public class AndroidClasspathContainerInitializer extends BaseClasspathContainer return androidSourceProperty; } + /** + * Cache results for testURL: Some are expensive to compute, and this is + * called repeatedly (perhaps for each open project) + */ + private static final Map<String, Boolean> sRecentUrlValidCache = + Maps.newHashMapWithExpectedSize(4); + + @SuppressWarnings("resource") // Eclipse does not handle Closeables#closeQuietly private static boolean testURL(String androidApiURL) { + Boolean cached = sRecentUrlValidCache.get(androidApiURL); + if (cached != null) { + return cached.booleanValue(); + } boolean valid = false; InputStream is = null; try { URL testURL = new URL(androidApiURL); - is = testURL.openStream(); + URLConnection connection = testURL.openConnection(); + // Only try for 5 seconds (though some implementations ignore this flag) + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + is = connection.getInputStream(); valid = true; } catch (Exception ignore) { } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ignore) { - } - } + Closeables.closeQuietly(is); } + + sRecentUrlValidCache.put(androidApiURL, valid); + return valid; } @@ -568,7 +586,8 @@ public class AndroidClasspathContainerInitializer extends BaseClasspathContainer // project that have been resolved before the sdk was loaded // will have a ProjectState where the IAndroidTarget is null // so we load the target now that the SDK is loaded. - IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject)); + IAndroidTarget target = currentSdk.loadTargetAndBuildTools( + Sdk.getProjectState(iProject)); if (target == null) { // this is really not supposed to happen. This would mean there are cached paths, // but project.properties was deleted. Keep the project in the list to force diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java index fb8742d..52870a4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java @@ -31,6 +31,7 @@ import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFileWrapper; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.build.ApkCreationException; import com.android.sdklib.build.DuplicateFileException; import com.android.sdklib.internal.project.ProjectProperties; @@ -123,13 +124,33 @@ public final class ExportHelper { } }); - BuildHelper helper = new BuildHelper(project, + ProjectState projectState = Sdk.getProjectState(project); + + // get the jumbo mode option + String forceJumboStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_FORCEJUMBO); + Boolean jumbo = Boolean.valueOf(forceJumboStr); + + String dexMergerStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_DISABLE_MERGER); + Boolean dexMerger = Boolean.valueOf(dexMergerStr); + + BuildToolInfo buildToolInfo = projectState.getBuildToolInfo(); + if (buildToolInfo == null) { + buildToolInfo = Sdk.getCurrent().getLatestBuildTool(); + } + + if (buildToolInfo == null) { + throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, + "No Build Tools installed in the SDK.")); + } + + BuildHelper helper = new BuildHelper(project, buildToolInfo, fakeStream, fakeStream, + jumbo.booleanValue(), + dexMerger.booleanValue(), debugMode, false /*verbose*/, null /*resourceMarker*/); // get the list of library projects - ProjectState projectState = Sdk.getProjectState(project); List<IProject> libProjects = projectState.getFullLibraryProjects(); // Step 1. Package the resources. diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java index aa0e736..5b0d185 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/LibraryClasspathContainerInitializer.java @@ -384,7 +384,10 @@ public class LibraryClasspathContainerInitializer extends BaseClasspathContainer for (IResource member : members) { if (member.getType() == IResource.FILE && SdkConstants.EXT_JAR.equalsIgnoreCase(member.getFileExtension())) { - jarFiles.add(member.getLocation().toFile()); + IPath location = member.getLocation(); + if (location != null) { + jarFiles.add(location.toFile()); + } } } } catch (CoreException e) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidDocumentChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidDocumentChange.java deleted file mode 100644 index 3949789..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidDocumentChange.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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.refactoring.changes; - -import com.android.SdkConstants; -import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; -import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener; -import com.android.ide.eclipse.adt.internal.refactoring.core.RefactoringUtil; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.DocumentChange; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; -import org.eclipse.text.edits.ReplaceEdit; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.Map; - -/** - * A text change that operates on android manifest using WTP SSE model. - * It is base class for Rename Package and Rename Type changes -*/ -@SuppressWarnings("restriction") -public class AndroidDocumentChange extends DocumentChange { - - protected IFile mAndroidManifest; - - protected String mAppPackage; - - protected IStructuredModel mModel; - - protected IDocument mDocument; - - protected Map<String, String> mElements; - - protected String mNewName; - - protected String mOldName; - - protected ITextFileBufferManager mManager; - - /** - * Creates a new <code>AndroidDocumentChange</code> for the given - * {@link IDocument}. - * - * @param document the document this change is working on - */ - public AndroidDocumentChange(IDocument document) { - super(SdkConstants.FN_ANDROID_MANIFEST_XML, document); - } - - @Override - public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - RefactoringStatus status = super.isValid(pm); - if (mModel == null) { - status.addFatalError("File " + SdkConstants.FN_ANDROID_MANIFEST_XML + " is invalid."); - } else { - mAppPackage = getAppPackage(); - if (mAppPackage == null) { - status.addFatalError("Invalid package in the " - + SdkConstants.FN_ANDROID_MANIFEST_XML + " file."); - } - } - BasicXmlErrorListener errorListener = new BasicXmlErrorListener(); - AndroidManifestHelper.parseForError(mAndroidManifest, errorListener); - - if (errorListener.mHasXmlError == true) { - status.addFatalError("File " + SdkConstants.FN_ANDROID_MANIFEST_XML + " is invalid."); - } - return status; - } - - /** - * Finds the attribute with values oldName - * - * @param xmlDoc the document - * @param element the element - * @param attributeName the attribute - * @param oldName the value - * - * @return the attribute - */ - private Attr findAttribute(IDOMDocument xmlDoc, String element, String attributeName, - String oldName) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, attributeName); - if (attribute != null) { - String value = attribute.getValue(); - if (value != null) { - String fullName = AndroidManifest.combinePackageAndClassName( - getAppPackage(), value); - if (fullName != null && fullName.equals(oldName)) { - return attribute; - } - } - } - } - } - return null; - } - - /** - * Returns the attribute value - * - * @param xmlDoc the document - * @param element the element - * @param attributeName the attribute - * - * @return the attribute value - */ - protected String getElementAttribute(IDOMDocument xmlDoc, String element, - String attributeName, boolean useNamespace) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute; - if (useNamespace) { - attribute = RefactoringUtil.findAndroidAttributes(attributes, attributeName); - } else { - attribute = (Attr) attributes.getNamedItem(attributeName); - } - if (attribute != null) { - return attribute.getValue(); - } - } - } - return null; - } - - /** - * Returns the SSE model for a document - * - * @param document the document - * @return the model - * - */ - protected IStructuredModel getModel(IDocument document) { - if (mModel != null) { - return mModel; - } - IStructuredModel model; - model = StructuredModelManager.getModelManager().getExistingModelForRead(document); - if (model == null) { - if (document instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) document; - model = StructuredModelManager.getModelManager() - .getModelForRead(structuredDocument); - } - } - return model; - } - - @Override - public void setTextType(String type) { - super.setTextType(mAndroidManifest.getFileExtension()); - } - - /** - * Returns the SSE DOM document - * - * @return the attribute value - */ - protected IDOMDocument getDOMDocument() { - IDOMModel xmlModel = (IDOMModel) mModel; - IDOMDocument xmlDoc = xmlModel.getDocument(); - return xmlDoc; - } - - /** - * Returns the application package - * - * @return the package name - */ - protected String getAppPackage() { - if (mAppPackage == null) { - IDOMDocument xmlDoc = getDOMDocument(); - mAppPackage = getElementAttribute(xmlDoc, AndroidManifest.NODE_MANIFEST, - AndroidManifest.ATTRIBUTE_PACKAGE, false); - } - return mAppPackage; - } - - /** - * Returns the text change that set new value of attribute - * - * @param attribute the attribute - * @param newValue the new value - * - * @return the text change - */ - protected TextEdit createTextEdit(Attr attribute, String newValue) { - if (attribute == null) - return null; - - if (attribute instanceof IDOMAttr) { - IDOMAttr domAttr = (IDOMAttr) attribute; - String region = domAttr.getValueRegionText(); - int offset = domAttr.getValueRegionStartOffset(); - if (region != null && region.length() >= 2) { - return new ReplaceEdit(offset + 1, region.length() - 2, newValue); - } - } - return null; - } - - /** - * Returns the text change that change the value of attribute from oldValue to newValue - * and combine package - * - * @param elementName the element name - * @param attributeName the attribute name - * @param oldValue the old value - * @param newValue the new value - * - * @return the text change - */ - protected TextEdit createTextEdit(String elementName, String attributeName, String oldValue, - String newValue) { - return createTextEdit(elementName, attributeName, oldValue, newValue, true); - } - - /** - * Returns the text change that change the value of attribute from oldValue to newValue - * - * @param elementName the element name - * @param attributeName the attribute name - * @param oldName the old value - * @param newName the new value - * @param combinePackage combine package ? - * - * @return the text change - */ - protected TextEdit createTextEdit(String elementName, String attributeName, String oldName, - String newName, boolean combinePackage) { - IDOMDocument xmlDoc = getDOMDocument(); - String name = null; - Attr attr = findAttribute(xmlDoc, elementName, attributeName, oldName); - if (attr != null) { - name = attr.getValue(); - } - if (name != null) { - String newValue; - if (combinePackage) { - newValue = AndroidManifest.extractActivityName(newName, getAppPackage()); - } else { - newValue = newName; - } - if (newValue != null) { - TextEdit edit = createTextEdit(attr, newValue); - return edit; - } - } - return null; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChange.java deleted file mode 100644 index 834a57c..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChange.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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.refactoring.changes; - -import com.android.SdkConstants; -import com.android.ide.eclipse.adt.internal.refactoring.core.RefactoringUtil; - -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.DocumentChange; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.ReplaceEdit; -import org.eclipse.text.edits.TextEdit; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * A text change that operates on android layout using WTP SSE model. - * It is base class for Rename Package and Rename Type changes -*/ -@SuppressWarnings("restriction") -public class AndroidLayoutChange extends DocumentChange { - - private IDocument mDocument; - - private ITextFileBufferManager mManager; - - private IFile mFile; - - private IStructuredModel mModel; - - private Set<AndroidLayoutChangeDescription> mChanges; - - /** - * Creates a new <code>AndroidLayoutChange</code> - * - * @param file the layout file - * @param document the document - * @param manager the buffer manager - * @param changes the list of changes - */ - public AndroidLayoutChange(IFile file, IDocument document, ITextFileBufferManager manager, - Set<AndroidLayoutChangeDescription> changes) { - super("", document); //$NON-NLS-1$ - mFile = file; - mDocument = document; - mManager = manager; - mChanges = changes; - try { - this.mModel = getModel(document); - } catch (Exception ignore) { - } - if (mModel != null) { - addEdits(); - } - } - - @Override - public String getName() { - return mFile.getName(); - } - - @Override - public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - RefactoringStatus status = super.isValid(pm); - if (mModel == null) { - status.addFatalError("Invalid the " + getName() + " file."); - } - return status; - } - - @Override - public void setTextType(String type) { - super.setTextType(mFile.getFileExtension()); - } - - @Override - public void dispose() { - super.dispose(); - RefactoringUtil.fixModel(mModel, mDocument); - - if (mManager != null) { - try { - mManager.disconnect(mFile.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - } - - // ---- - - /** - * Adds text edits for this change - */ - private void addEdits() { - MultiTextEdit multiEdit = new MultiTextEdit(); - for (AndroidLayoutChangeDescription change : mChanges) { - if (!change.isStandalone()) { - TextEdit edit = createTextEdit(SdkConstants.VIEW, - SdkConstants.ATTR_CLASS, - change.getClassName(), - change.getNewName()); - if (edit != null) { - multiEdit.addChild(edit); - } - } else { - List<TextEdit> edits = createElementTextEdit(change.getClassName(), - change.getNewName()); - for (TextEdit edit : edits) { - multiEdit.addChild(edit); - } - } - } - setEdit(multiEdit); - } - - /** - * Returns the text changes which change class (custom layout viewer) in layout file - * - * @param className the class name - * @param newName the new class name - * - * @return list of text changes - */ - private List<TextEdit> createElementTextEdit(String className, String newName) { - IDOMDocument xmlDoc = getDOMDocument(); - List<TextEdit> edits = new ArrayList<TextEdit>(); - NodeList nodes = xmlDoc.getElementsByTagName(className); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - if (node instanceof IDOMElement) { - IDOMElement domNode = (IDOMElement) node; - IStructuredDocumentRegion firstRegion = domNode.getFirstStructuredDocumentRegion(); - if (firstRegion != null) { - int offset = firstRegion.getStartOffset(); - edits.add(new ReplaceEdit(offset + 1, className.length(), newName)); - } - IStructuredDocumentRegion endRegion = domNode.getEndStructuredDocumentRegion(); - if (endRegion != null) { - int offset = endRegion.getStartOffset(); - edits.add(new ReplaceEdit(offset + 2, className.length(), newName)); - } - } - - } - return edits; - } - - /** - * Returns the SSE DOM document - * - * @return the attribute value - */ - private IDOMDocument getDOMDocument() { - IDOMModel xmlModel = (IDOMModel) mModel; - IDOMDocument xmlDoc = xmlModel.getDocument(); - return xmlDoc; - } - - /** - * Returns the text change that set new value of attribute - * - * @param attribute the attribute - * @param newValue the new value - * - * @return the text change - */ - private TextEdit createTextEdit(Attr attribute, String newValue) { - if (attribute == null) - return null; - - if (attribute instanceof IDOMAttr) { - IDOMAttr domAttr = (IDOMAttr) attribute; - String region = domAttr.getValueRegionText(); - int offset = domAttr.getValueRegionStartOffset(); - if (region != null && region.length() >= 2) { - return new ReplaceEdit(offset + 1, region.length() - 2, newValue); - } - } - return null; - } - - - /** - * Returns the text change that change the value of attribute from oldValue to newValue - * - * @param elementName the element name - * @param argumentName the attribute name - * @param oldName the old value - * @param newName the new value - * - * @return the text change - */ - private TextEdit createTextEdit(String elementName, String argumentName, String oldName, - String newName) { - IDOMDocument xmlDoc = getDOMDocument(); - String name = null; - Attr attr = findAttribute(xmlDoc, elementName, argumentName, oldName); - if (attr != null) { - name = attr.getValue(); - } - if (name != null && newName != null) { - TextEdit edit = createTextEdit(attr, newName); - return edit; - } - return null; - } - - /** - * Finds the attribute with values oldName - * - * @param xmlDoc the document - * @param element the element - * @param attributeName the attribute - * @param oldValue the value - * - * @return the attribute - */ - private Attr findAttribute(IDOMDocument xmlDoc, String element, String attributeName, - String oldValue) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, attributeName); - if (attribute != null) { - String value = attribute.getValue(); - if (value != null && value.equals(oldValue)) { - return attribute; - } - } - } - } - return null; - } - - /** - * Returns the SSE model for a document - * - * @param document the document - * @return the model - */ - private IStructuredModel getModel(IDocument document) { - - IModelManager manager = StructuredModelManager.getModelManager(); - IStructuredModel model = manager.getExistingModelForRead(document); - if (model == null && document instanceof IStructuredDocument) { - model = manager.getModelForRead((IStructuredDocument) document); - } - - return model; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChangeDescription.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChangeDescription.java deleted file mode 100644 index a12885d..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutChangeDescription.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.refactoring.changes; - -/** - * This class describes the text changes of android layout files - * - */ -public class AndroidLayoutChangeDescription { - - private String mClassName; - - private String mNewName; - - private int mType; - - /** - * the view layout - */ - public static final int VIEW_TYPE = 0; - - /** - * the standalone layout - */ - public static final int STANDALONE_TYPE = 1; - - /** - * Creates a new <code>AndroidDocumentChange</code> - * - * @param className the old layout class name - * @param newName the new layout class name - * @param type the layout type; valid value are VIEW_TYPE and STANDALONE_TYPE - */ - public AndroidLayoutChangeDescription(String className, String newName, int type) { - this.mClassName = className; - this.mNewName = newName; - this.mType = type; - } - - /** - * @return the old class name - */ - public String getClassName() { - return mClassName; - } - - /** - * @return the new class name - */ - public String getNewName() { - return mNewName; - } - - /** - * @return the layout type - */ - public int getType() { - return mType; - } - - /** - * @return true if the layout is standalone - */ - public boolean isStandalone() { - return mType == STANDALONE_TYPE; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mClassName == null) ? 0 : mClassName.hashCode()); - result = prime * result + ((mNewName == null) ? 0 : mNewName.hashCode()); - result = prime * result + mType; - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AndroidLayoutChangeDescription other = (AndroidLayoutChangeDescription) obj; - if (mClassName == null) { - if (other.mClassName != null) - return false; - } else if (!mClassName.equals(other.mClassName)) - return false; - if (mNewName == null) { - if (other.mNewName != null) - return false; - } else if (!mNewName.equals(other.mNewName)) - return false; - if (mType != other.mType) - return false; - return true; - } - - @Override - public String toString() { - return "AndroidLayoutChangeDescription [className=" + mClassName + ", newName=" + mNewName - + ", type=" + mType + "]"; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutFileChanges.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutFileChanges.java deleted file mode 100644 index 13f7d6e..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidLayoutFileChanges.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.refactoring.changes; - -import org.eclipse.core.resources.IFile; - -import java.util.HashSet; -import java.util.Set; - -/** - * Set of layout files with required text changes - * - */ -public class AndroidLayoutFileChanges { - private IFile mFile; - - private Set<AndroidLayoutChangeDescription> mChanges = - new HashSet<AndroidLayoutChangeDescription>(); - - /** - * Creates a new <code>AndroidLayoutFileChanges</code> - * - * @param file the layout file - */ - public AndroidLayoutFileChanges(IFile file) { - this.mFile = file; - } - - /** - * Return the layout file - * - * @return the file - */ - public IFile getFile() { - return mFile; - } - - /** - * Return the text changes - * - * @return the set of changes - */ - public Set<AndroidLayoutChangeDescription> getChanges() { - return mChanges; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidPackageRenameChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidPackageRenameChange.java deleted file mode 100644 index 590629a..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidPackageRenameChange.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.refactoring.changes; - -import com.android.ide.eclipse.adt.internal.refactoring.core.FixImportsJob; -import com.android.ide.eclipse.adt.internal.refactoring.core.RefactoringUtil; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.TextEdit; - -import java.util.Map; -import java.util.Set; - -/** - * A text change that operates on android manifest when execute Java Rename package refactoring -*/ -public class AndroidPackageRenameChange extends AndroidDocumentChange { - - private boolean mIsPackage; - - /** - * Creates a new <code>AndroidPackageRenameChange</code> - * - * @param androidManifest the android manifest file - * @param manager the text buffer manager - * @param document the document - * @param elements the elements - * @param newName the new name - * @param oldName the old name - * @param isPackage is the application package - */ - public AndroidPackageRenameChange(IFile androidManifest, ITextFileBufferManager manager, - IDocument document, Map<String, String> elements, String newName, String oldName, - boolean isPackage) { - super(document); - this.mDocument = document; - this.mIsPackage = isPackage; - this.mElements = elements; - this.mNewName = newName; - this.mOldName = oldName; - this.mManager = manager; - this.mAndroidManifest = androidManifest; - try { - this.mModel = getModel(document); - } catch (Exception ignore) { - } - if (mModel != null) { - this.mAppPackage = getAppPackage(); - addEdits(); - } - } - - /** - * Adds text edits for this change - */ - private void addEdits() { - MultiTextEdit multiEdit = new MultiTextEdit(); - - if (mIsPackage) { - TextEdit edit = createTextEdit(AndroidManifest.NODE_MANIFEST, - AndroidManifest.ATTRIBUTE_PACKAGE, mOldName, mNewName, false); - if (edit != null) { - multiEdit.addChild(edit); - } - } - Set<String> keys = mElements.keySet(); - for (String key : keys) { - String value = mElements.get(key); - String oldValue = AndroidManifest.combinePackageAndClassName(mAppPackage, value); - String newValue = oldValue.replaceFirst(mOldName, mNewName); - TextEdit edit = createTextEdit(key, AndroidManifest.ATTRIBUTE_NAME, oldValue, - newValue); - if (edit != null) { - multiEdit.addChild(edit); - } - if (AndroidManifest.NODE_ACTIVITY.equals(key)) { - TextEdit alias = createTextEdit(AndroidManifest.NODE_ACTIVITY_ALIAS, - AndroidManifest.ATTRIBUTE_TARGET_ACTIVITY, oldValue, newValue); - if (alias != null) { - multiEdit.addChild(alias); - } - } - } - setEdit(multiEdit); - } - - @Override - public Change perform(IProgressMonitor pm) throws CoreException { - super.perform(pm); - return new AndroidPackageRenameChange(mAndroidManifest, mManager, mDocument, mElements, - mOldName, mNewName, mIsPackage); - } - - @Override - public void dispose() { - super.dispose(); - RefactoringUtil.fixModel(mModel, mDocument); - - if (mManager != null) { - try { - mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - if (mIsPackage) { - new FixImportsJob("Fix Rename Package", mAndroidManifest, mNewName).schedule(500); - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeMoveChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeMoveChange.java deleted file mode 100644 index 7dcc505..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeMoveChange.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.refactoring.changes; - -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.resources.IFile; -import org.eclipse.jface.text.IDocument; - -import java.util.Map; - -/** - * A text change that operates on android manifest when execute Java Move type refactoring -*/ -public class AndroidTypeMoveChange extends AndroidTypeRenameChange { - - /** - * Creates a new <code>AndroidTypeMoveChange</code> - * - * @param androidManifest the android manifest file - * @param manager the text buffer manager - * @param document the document - * @param elements the elements - * @param newName the new name - * @param oldName the old name - */ - public AndroidTypeMoveChange(IFile androidManifest, ITextFileBufferManager manager, - IDocument document, Map<String, String> elements, String newName, String oldName) { - super(androidManifest, manager, document, elements, newName, oldName); - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeRenameChange.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeRenameChange.java deleted file mode 100644 index 798c0d2..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/changes/AndroidTypeRenameChange.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.refactoring.changes; - -import com.android.ide.eclipse.adt.internal.refactoring.core.RefactoringUtil; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.text.edits.MultiTextEdit; -import org.eclipse.text.edits.TextEdit; - -import java.util.Map; -import java.util.Set; - -/** - * A text change that operates on android manifest when execute Java Rename type refactoring -*/ -public class AndroidTypeRenameChange extends AndroidDocumentChange { - - /** - * * Creates a new <code>AndroidTypeRenameChange</code> - * - * @param androidManifest the android manifest file - * @param manager the text buffer manager - * @param document the document - * @param elements the elements - * @param newName the new name - * @param oldName the old name - */ - public AndroidTypeRenameChange(IFile androidManifest, ITextFileBufferManager manager, - IDocument document, Map<String, String> elements, String newName, String oldName) { - super(document); - this.mDocument = document; - this.mElements = elements; - this.mNewName = newName; - this.mOldName = oldName; - this.mManager = manager; - this.mAndroidManifest = androidManifest; - try { - this.mModel = getModel(document); - } catch (Exception ignore) { - } - if (mModel != null) { - addEdits(); - } - } - - /** - * Adds text edits for this change - */ - private void addEdits() { - MultiTextEdit multiEdit = new MultiTextEdit(); - Set<String> keys = mElements.keySet(); - for (String key : keys) { - TextEdit edit = createTextEdit(key, AndroidManifest.ATTRIBUTE_NAME, mOldName, - mNewName); - if (edit != null) { - multiEdit.addChild(edit); - } - if (AndroidManifest.NODE_ACTIVITY.equals(key)) { - TextEdit alias = createTextEdit(AndroidManifest.NODE_ACTIVITY_ALIAS, - AndroidManifest.ATTRIBUTE_TARGET_ACTIVITY, mOldName, mNewName); - if (alias != null) { - multiEdit.addChild(alias); - } - TextEdit manageSpaceActivity = createTextEdit( - AndroidManifest.NODE_APPLICATION, - AndroidManifest.ATTRIBUTE_MANAGE_SPACE_ACTIVITY, mOldName, mNewName); - if (manageSpaceActivity != null) { - multiEdit.addChild(manageSpaceActivity); - } - } - } - setEdit(multiEdit); - } - - @Override - public Change perform(IProgressMonitor pm) throws CoreException { - super.perform(pm); - return new AndroidTypeRenameChange(mAndroidManifest, mManager, mDocument, mElements, - mOldName, mNewName); - } - - @Override - public void dispose() { - super.dispose(); - RefactoringUtil.fixModel(mModel, mDocument); - - if (mManager != null) { - try { - mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidPackageRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidPackageRenameParticipant.java deleted file mode 100644 index 52b49a7..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidPackageRenameParticipant.java +++ /dev/null @@ -1,528 +0,0 @@ -/* - * 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.refactoring.core; - -import com.android.SdkConstants; -import com.android.ide.common.xml.ManifestData; -import com.android.ide.eclipse.adt.AdtConstants; -import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChange; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChangeDescription; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutFileChanges; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidPackageRenameChange; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.FileBuffers; -import org.eclipse.core.filebuffers.ITextFileBuffer; -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IPath; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IPackageFragment; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jdt.core.search.IJavaSearchConstants; -import org.eclipse.jdt.core.search.IJavaSearchScope; -import org.eclipse.jdt.core.search.SearchEngine; -import org.eclipse.jdt.core.search.SearchMatch; -import org.eclipse.jdt.core.search.SearchParticipant; -import org.eclipse.jdt.core.search.SearchPattern; -import org.eclipse.jdt.core.search.SearchRequestor; -import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange; -import org.eclipse.jdt.internal.corext.util.JavaModelUtil; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.ltk.core.refactoring.CompositeChange; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A participant to participate in refactorings that rename a package in an Android project. - * The class updates android manifest and the layout file - * The user can suppress refactoring by disabling the "Update references" checkbox - * <p> - * Rename participants are registered via the extension point <code> - * org.eclipse.ltk.core.refactoring.renameParticipants</code>. - * Extensions to this extension point must therefore extend - * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>. - * </p> - */ -@SuppressWarnings("restriction") -public class AndroidPackageRenameParticipant extends AndroidRenameParticipant { - - private IPackageFragment mPackageFragment; - - private boolean mIsPackage; - - private Set<AndroidLayoutFileChanges> mFileChanges = new HashSet<AndroidLayoutFileChanges>(); - - @Override - public Change createChange(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - if (pm.isCanceled()) { - return null; - } - if (!getArguments().getUpdateReferences()) - return null; - IPath pkgPath = mPackageFragment.getPath(); - IJavaProject javaProject = (IJavaProject) mPackageFragment - .getAncestor(IJavaElement.JAVA_PROJECT); - IProject project = javaProject.getProject(); - IPath genPath = project.getFullPath().append(SdkConstants.FD_GEN_SOURCES); - if (genPath.isPrefixOf(pkgPath)) { - RefactoringUtil.logInfo(getName() + ": Cannot rename generated package."); - return null; - } - CompositeChange result = new CompositeChange(getName()); - if (mAndroidManifest.exists()) { - if (mAndroidElements.size() > 0 || mIsPackage) { - getDocument(); - Change change = new AndroidPackageRenameChange(mAndroidManifest, mManager, - mDocument, mAndroidElements, mNewName, mOldName, mIsPackage); - if (change != null) { - result.add(change); - } - } - if (mIsPackage) { - Change genChange = getGenPackageChange(pm); - if (genChange != null) { - result.add(genChange); - } - } - // add layoutChange - for (AndroidLayoutFileChanges fileChange : mFileChanges) { - IFile file = fileChange.getFile(); - ITextFileBufferManager lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - Change layoutChange = new AndroidLayoutChange(file, lDocument, lManager, - fileChange.getChanges()); - if (layoutChange != null) { - result.add(layoutChange); - } - } - } - return (result.getChildren().length == 0) ? null : result; - } - - /** - * Returns Android gen package text change - * - * @param pm the progress monitor - * - * @return Android gen package text change - * @throws CoreException - * @throws OperationCanceledException - */ - public Change getGenPackageChange(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - if (mIsPackage) { - IPackageFragment genJavaPackageFragment = getGenPackageFragment(); - if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) { - return new RenamePackageChange(genJavaPackageFragment, mNewName, true); - } - } - return null; - } - - /** - * Return the gen package fragment - * - */ - private IPackageFragment getGenPackageFragment() throws JavaModelException { - IJavaProject javaProject = (IJavaProject) mPackageFragment - .getAncestor(IJavaElement.JAVA_PROJECT); - if (javaProject != null && javaProject.isOpen()) { - IProject project = javaProject.getProject(); - IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); - if (genFolder.exists()) { - String javaPackagePath = mAppPackage.replace(".", "/"); - IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath); - IPackageFragment genPackageFragment = javaProject - .findPackageFragment(genJavaPackagePath); - return genPackageFragment; - } - } - return null; - } - - @Override - public String getName() { - return "Android Package Rename"; - } - - @Override - protected boolean initialize(final Object element) { - mIsPackage = false; - try { - if (element instanceof IPackageFragment) { - mPackageFragment = (IPackageFragment) element; - if (!mPackageFragment.containsJavaResources()) - return false; - IJavaProject javaProject = (IJavaProject) mPackageFragment - .getAncestor(IJavaElement.JAVA_PROJECT); - IProject project = javaProject.getProject(); - IResource manifestResource = project.findMember(AdtConstants.WS_SEP - + SdkConstants.FN_ANDROID_MANIFEST_XML); - - if (manifestResource == null || !manifestResource.exists() - || !(manifestResource instanceof IFile)) { - RefactoringUtil.logInfo("Invalid or missing the " - + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + project.getName() - + " project."); - return false; - } - mAndroidManifest = (IFile) manifestResource; - String packageName = mPackageFragment.getElementName(); - ManifestData manifestData; - manifestData = AndroidManifestHelper.parseForData(mAndroidManifest); - if (manifestData == null) { - return false; - } - mAppPackage = manifestData.getPackage(); - mOldName = packageName; - mNewName = getArguments().getNewName(); - if (mOldName == null || mNewName == null) { - return false; - } - - if (RefactoringUtil.isRefactorAppPackage() - && mAppPackage != null - && mAppPackage.equals(packageName)) { - mIsPackage = true; - } - mAndroidElements = addAndroidElements(); - try { - final IType type = javaProject.findType(SdkConstants.CLASS_VIEW); - SearchPattern pattern = SearchPattern.createPattern("*", - IJavaSearchConstants.TYPE, IJavaSearchConstants.DECLARATIONS, - SearchPattern.R_REGEXP_MATCH); - IJavaSearchScope scope =SearchEngine.createJavaSearchScope( - new IJavaElement[] { mPackageFragment }); - final HashSet<IType> elements = new HashSet<IType>(); - SearchRequestor requestor = new SearchRequestor() { - - @Override - public void acceptSearchMatch(SearchMatch match) throws CoreException { - Object elem = match.getElement(); - if (elem instanceof IType) { - IType eType = (IType) elem; - IType[] superTypes = JavaModelUtil.getAllSuperTypes(eType, - new NullProgressMonitor()); - for (int i = 0; i < superTypes.length; i++) { - if (superTypes[i].equals(type)) { - elements.add(eType); - break; - } - } - } - - } - }; - SearchEngine searchEngine = new SearchEngine(); - searchEngine.search(pattern, new SearchParticipant[] { - SearchEngine.getDefaultSearchParticipant() - }, scope, requestor, null); - List<String> views = new ArrayList<String>(); - for (IType elem : elements) { - views.add(elem.getFullyQualifiedName()); - } - if (views.size() > 0) { - String[] classNames = views.toArray(new String[0]); - addLayoutChanges(project, classNames); - } - } catch (CoreException e) { - RefactoringUtil.log(e); - } - - return mIsPackage || mAndroidElements.size() > 0 || mFileChanges.size() > 0; - } - } catch (JavaModelException ignore) { - } - return false; - } - - /** - * Adds layout changes for project - * - * @param project the Android project - * @param classNames the layout classes - */ - private void addLayoutChanges(IProject project, String[] classNames) { - try { - IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); - IResource[] layoutMembers = resFolder.members(); - for (int j = 0; j < layoutMembers.length; j++) { - IResource resource = layoutMembers[j]; - if (resource instanceof IFolder - && resource.exists() - && resource.getName().startsWith(SdkConstants.FD_RES_LAYOUT)) { - IFolder layoutFolder = (IFolder) resource; - IResource[] members = layoutFolder.members(); - for (int i = 0; i < members.length; i++) { - IResource member = members[i]; - if ((member instanceof IFile) - && member.exists() - && member.getName().endsWith(".xml")) { //$NON-NLS-1$ - IFile file = (IFile) member; - Set<AndroidLayoutChangeDescription> changes = - parse(file, classNames); - if (changes.size() > 0) { - AndroidLayoutFileChanges fileChange = - new AndroidLayoutFileChanges(file); - fileChange.getChanges().addAll(changes); - mFileChanges.add(fileChange); - } - } - } - } - } - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - - /** - * Searches the layout file for classes - * - * @param file the Android layout file - * @param classNames the layout classes - */ - private Set<AndroidLayoutChangeDescription> parse(IFile file, String[] classNames) { - Set<AndroidLayoutChangeDescription> changes = - new HashSet<AndroidLayoutChangeDescription>(); - ITextFileBufferManager lManager = null; - try { - lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), - LocationKind.NORMALIZE, new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager(). - getExistingModelForRead(lDocument); - if (model == null) { - if (lDocument instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) lDocument; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - NodeList nodes = xmlDoc.getElementsByTagName(SdkConstants.VIEW); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Node attributeNode = attributes - .getNamedItem(SdkConstants.ATTR_CLASS); - if (attributeNode instanceof Attr) { - Attr attribute = (Attr) attributeNode; - String value = attribute.getValue(); - if (value != null) { - for (int j = 0; j < classNames.length; j++) { - String className = classNames[j]; - if (value.equals(className)) { - String newClassName = getNewClassName(className); - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription( - className, newClassName, - AndroidLayoutChangeDescription.VIEW_TYPE); - changes.add(layoutChange); - } - } - } - } - } - } - for (int j = 0; j < classNames.length; j++) { - String className = classNames[j]; - nodes = xmlDoc.getElementsByTagName(className); - for (int i = 0; i < nodes.getLength(); i++) { - String newClassName = getNewClassName(className); - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription( - className, newClassName, - AndroidLayoutChangeDescription.STANDALONE_TYPE); - changes.add(layoutChange); - } - } - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - } catch (CoreException ignore) { - } finally { - if (lManager != null) { - try { - lManager.disconnect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException ignore) { - } - } - } - return changes; - } - - /** - * Returns the new class name - * - * @param className the class name - * @return the new class name - */ - private String getNewClassName(String className) { - int lastDot = className.lastIndexOf("."); //$NON-NLS-1$ - if (lastDot < 0) { - return mNewName; - } - String name = className.substring(lastDot, className.length()); - String newClassName = mNewName + name; - return newClassName; - } - - /** - * Returns the elements (activity, receiver, service ...) - * which have to be renamed - * - * @return the android elements - */ - private Map<String, String> addAndroidElements() { - Map<String, String> androidElements = new HashMap<String, String>(); - - IDocument document; - try { - document = getDocument(); - } catch (CoreException e) { - RefactoringUtil.log(e); - if (mManager != null) { - try { - mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e1) { - RefactoringUtil.log(e1); - } - } - document = null; - return androidElements; - } - - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager().getExistingModelForRead(document); - if (model == null) { - if (document instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) document; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - add(xmlDoc, androidElements, AndroidManifest.NODE_ACTIVITY, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_APPLICATION, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_PROVIDER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_RECEIVER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_SERVICE, - AndroidManifest.ATTRIBUTE_NAME); - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - return androidElements; - } - - /** - * Adds the element (activity, receiver, service ...) to the map - * - * @param xmlDoc the document - * @param androidElements the map - * @param element the element - */ - private void add(IDOMDocument xmlDoc, Map<String, String> androidElements, String element, - String argument) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, argument); - if (attribute != null) { - String value = attribute.getValue(); - if (value != null) { - String fullName = AndroidManifest.combinePackageAndClassName(mAppPackage, - value); - if (RefactoringUtil.isRefactorAppPackage()) { - if (fullName != null && fullName.startsWith(mAppPackage)) { - boolean startWithDot = (value.charAt(0) == '.'); - boolean hasDot = (value.indexOf('.') != -1); - if (!startWithDot && hasDot) { - androidElements.put(element, value); - } - } - } else { - if (fullName != null) { - androidElements.put(element, value); - } - } - } - } - } - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidRenameParticipant.java deleted file mode 100644 index 5ddca7f..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidRenameParticipant.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.refactoring.core; - -import org.eclipse.core.filebuffers.FileBuffers; -import org.eclipse.core.filebuffers.ITextFileBuffer; -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; -import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; -import org.eclipse.ltk.core.refactoring.participants.RenameParticipant; - -import java.util.Map; - -/** - * The abstract class for Rename Package and Rename type participants - * - */ -public abstract class AndroidRenameParticipant extends RenameParticipant { - - protected IFile mAndroidManifest; - - protected ITextFileBufferManager mManager; - - protected String mOldName; - - protected String mNewName; - - protected IDocument mDocument; - - protected String mAppPackage; - - protected Map<String, String> mAndroidElements; - - @Override - public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) - throws OperationCanceledException { - return new RefactoringStatus(); - } - - /** - * @return the document - * @throws CoreException - */ - public IDocument getDocument() throws CoreException { - if (mDocument == null) { - mManager = FileBuffers.getTextFileBufferManager(); - mManager.connect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - ITextFileBuffer buffer = mManager.getTextFileBuffer(mAndroidManifest.getFullPath(), - LocationKind.NORMALIZE); - mDocument = buffer.getDocument(); - } - return mDocument; - } - - /** - * @return the android manifest file - */ - public IFile getAndroidManifest() { - return mAndroidManifest; - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeMoveParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeMoveParticipant.java deleted file mode 100644 index 25ca533..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeMoveParticipant.java +++ /dev/null @@ -1,416 +0,0 @@ -/* - * 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.refactoring.core; - -import com.android.SdkConstants; -import com.android.ide.common.xml.ManifestData; -import com.android.ide.eclipse.adt.AdtConstants; -import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChange; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChangeDescription; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutFileChanges; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidTypeMoveChange; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.FileBuffers; -import org.eclipse.core.filebuffers.ITextFileBuffer; -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IPackageFragment; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.ITypeHierarchy; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.ltk.core.refactoring.CompositeChange; -import org.eclipse.ltk.core.refactoring.RefactoringStatus; -import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; -import org.eclipse.ltk.core.refactoring.participants.MoveParticipant; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * A participant to participate in refactorings that move a type in an Android project. - * The class updates android manifest and the layout file - * The user can suppress refactoring by disabling the "Update references" checkbox - * <p> - * Rename participants are registered via the extension point <code> - * org.eclipse.ltk.core.refactoring.moveParticipants</code>. - * Extensions to this extension point must therefore extend <code>org.eclipse.ltk.core.refactoring.participants.MoveParticipant</code>. - * </p> - */ -@SuppressWarnings("restriction") -public class AndroidTypeMoveParticipant extends MoveParticipant { - - protected IFile mAndroidManifest; - - protected ITextFileBufferManager mManager; - - protected String mOldName; - - protected String mNewName; - - protected IDocument mDocument; - - protected String mJavaPackage; - - protected Map<String, String> mAndroidElements; - - private Set<AndroidLayoutFileChanges> mFileChanges = new HashSet<AndroidLayoutFileChanges>(); - - @Override - public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) - throws OperationCanceledException { - return new RefactoringStatus(); - } - - @Override - public Change createChange(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - if (pm.isCanceled()) { - return null; - } - if (!getArguments().getUpdateReferences()) - return null; - CompositeChange result = new CompositeChange(getName()); - if (mAndroidManifest.exists()) { - if (mAndroidElements.size() > 0) { - getDocument(); - Change change = new AndroidTypeMoveChange(mAndroidManifest, mManager, mDocument, - mAndroidElements, mNewName, mOldName); - if (change != null) { - result.add(change); - } - } - - for (AndroidLayoutFileChanges fileChange : mFileChanges) { - IFile file = fileChange.getFile(); - ITextFileBufferManager lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - Change layoutChange = new AndroidLayoutChange(file, lDocument, lManager, - fileChange.getChanges()); - if (layoutChange != null) { - result.add(layoutChange); - } - } - } - return (result.getChildren().length == 0) ? null : result; - - } - - /** - * @return the document - * @throws CoreException - */ - public IDocument getDocument() throws CoreException { - if (mDocument == null) { - mManager = FileBuffers.getTextFileBufferManager(); - mManager.connect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - ITextFileBuffer buffer = mManager.getTextFileBuffer(mAndroidManifest.getFullPath(), - LocationKind.NORMALIZE); - mDocument = buffer.getDocument(); - } - return mDocument; - } - - /** - * @return the android manifest file - */ - public IFile getAndroidManifest() { - return mAndroidManifest; - } - - @Override - public String getName() { - return "Android Type Move"; - } - - @Override - protected boolean initialize(Object element) { - - if (element instanceof IType) { - IType type = (IType) element; - IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT); - IProject project = javaProject.getProject(); - IResource manifestResource = project.findMember(AdtConstants.WS_SEP - + SdkConstants.FN_ANDROID_MANIFEST_XML); - - if (manifestResource == null || !manifestResource.exists() - || !(manifestResource instanceof IFile)) { - RefactoringUtil.logInfo("Invalid or missing the " - + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + project.getName() - + " project."); - return false; - } - mAndroidManifest = (IFile) manifestResource; - ManifestData manifestData; - manifestData = AndroidManifestHelper.parseForData(mAndroidManifest); - if (manifestData == null) { - return false; - } - mJavaPackage = manifestData.getPackage(); - mOldName = type.getFullyQualifiedName(); - Object destination = getArguments().getDestination(); - if (destination instanceof IPackageFragment) { - IPackageFragment packageFragment = (IPackageFragment) destination; - mNewName = packageFragment.getElementName() + "." + type.getElementName(); - } - if (mOldName == null || mNewName == null) { - return false; - } - mAndroidElements = addAndroidElements(); - try { - ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(null); - if (typeHierarchy == null) { - return false; - } - IType[] superTypes = typeHierarchy.getAllSuperclasses(type); - for (int i = 0; i < superTypes.length; i++) { - IType superType = superTypes[i]; - String className = superType.getFullyQualifiedName(); - if (className.equals(SdkConstants.CLASS_VIEW)) { - addLayoutChanges(project, type.getFullyQualifiedName()); - break; - } - } - } catch (JavaModelException ignore) { - } - return mAndroidElements.size() > 0 || mFileChanges.size() > 0; - } - return false; - } - - /** - * Adds layout changes for project - * - * @param project the Android project - * @param className the layout classes - * - */ - private void addLayoutChanges(IProject project, String className) { - try { - IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); - IFolder layoutFolder = resFolder.getFolder(SdkConstants.FD_RES_LAYOUT); - IResource[] members = layoutFolder.members(); - for (int i = 0; i < members.length; i++) { - IResource member = members[i]; - if ((member instanceof IFile) && member.exists()) { - IFile file = (IFile) member; - Set<AndroidLayoutChangeDescription> changes = parse(file, className); - if (changes.size() > 0) { - AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); - fileChange.getChanges().addAll(changes); - mFileChanges.add(fileChange); - } - } - } - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - - /** - * Searches the layout file for classes - * - * @param file the Android layout file - * @param className the layout classes - * - */ - private Set<AndroidLayoutChangeDescription> parse(IFile file, String className) { - Set<AndroidLayoutChangeDescription> changes = new HashSet<AndroidLayoutChangeDescription>(); - ITextFileBufferManager lManager = null; - try { - lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), LocationKind.NORMALIZE, new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager().getExistingModelForRead(lDocument); - if (model == null) { - if (lDocument instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) lDocument; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - NodeList nodes = xmlDoc.getElementsByTagName(SdkConstants.VIEW); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Node attributeNode = - attributes.getNamedItem(SdkConstants.ATTR_CLASS); - if (attributeNode instanceof Attr) { - Attr attribute = (Attr) attributeNode; - String value = attribute.getValue(); - if (value != null && value.equals(className)) { - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription(className, mNewName, - AndroidLayoutChangeDescription.VIEW_TYPE); - changes.add(layoutChange); - } - } - } - } - nodes = xmlDoc.getElementsByTagName(className); - for (int i = 0; i < nodes.getLength(); i++) { - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription(className, mNewName, - AndroidLayoutChangeDescription.STANDALONE_TYPE); - changes.add(layoutChange); - } - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - } catch (CoreException ignore) { - } finally { - if (lManager != null) { - try { - lManager.disconnect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException ignore) { - } - } - } - return changes; - } - - /** - * Returns the elements (activity, receiver, service ...) - * which have to be renamed - * - * @return the android elements - */ - private Map<String, String> addAndroidElements() { - Map<String, String> androidElements = new HashMap<String, String>(); - - IDocument document; - try { - document = getDocument(); - } catch (CoreException e) { - RefactoringUtil.log(e); - if (mManager != null) { - try { - mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e1) { - RefactoringUtil.log(e1); - } - } - document = null; - return androidElements; - } - - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager().getExistingModelForRead(document); - if (model == null) { - if (document instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) document; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - add(xmlDoc, androidElements, AndroidManifest.NODE_ACTIVITY, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_APPLICATION, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_PROVIDER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_RECEIVER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_SERVICE, - AndroidManifest.ATTRIBUTE_NAME); - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - return androidElements; - } - - /** - * Adds the element (activity, receiver, service ...) to the map - * - * @param xmlDoc the document - * @param androidElements the map - * @param element the element - */ - private void add(IDOMDocument xmlDoc, Map<String, String> androidElements, String element, - String argument) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, argument); - if (attribute != null) { - String value = attribute.getValue(); - if (value != null) { - String fullName = AndroidManifest.combinePackageAndClassName(mJavaPackage, - value); - if (fullName != null && fullName.equals(mOldName)) { - androidElements.put(element, value); - } - } - } - } - } - } - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeRenameParticipant.java deleted file mode 100644 index d62cc23..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/AndroidTypeRenameParticipant.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 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.refactoring.core; - -import com.android.SdkConstants; -import com.android.ide.common.xml.ManifestData; -import com.android.ide.eclipse.adt.AdtConstants; -import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChange; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutChangeDescription; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidLayoutFileChanges; -import com.android.ide.eclipse.adt.internal.refactoring.changes.AndroidTypeRenameChange; -import com.android.xml.AndroidManifest; - -import org.eclipse.core.filebuffers.FileBuffers; -import org.eclipse.core.filebuffers.ITextFileBuffer; -import org.eclipse.core.filebuffers.ITextFileBufferManager; -import org.eclipse.core.filebuffers.LocationKind; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; -import org.eclipse.jdt.core.IJavaElement; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.IType; -import org.eclipse.jdt.core.ITypeHierarchy; -import org.eclipse.jdt.core.JavaModelException; -import org.eclipse.jface.text.IDocument; -import org.eclipse.ltk.core.refactoring.Change; -import org.eclipse.ltk.core.refactoring.CompositeChange; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; -import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * A participant to participate in refactorings that rename a type in an Android project. - * The class updates android manifest and the layout file - * The user can suppress refactoring by disabling the "Update references" checkbox - * <p> - * Rename participants are registered via the extension point <code> - * org.eclipse.ltk.core.refactoring.renameParticipants</code>. - * Extensions to this extension point must therefore extend <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>. - * </p> - */ -@SuppressWarnings("restriction") -public class AndroidTypeRenameParticipant extends AndroidRenameParticipant { - - private Set<AndroidLayoutFileChanges> mFileChanges = new HashSet<AndroidLayoutFileChanges>(); - - private String mLayoutNewName; - - @Override - public Change createChange(IProgressMonitor pm) throws CoreException, - OperationCanceledException { - if (pm.isCanceled()) { - return null; - } - if (!getArguments().getUpdateReferences()) - return null; - CompositeChange result = new CompositeChange(getName()); - if (mAndroidManifest.exists()) { - if (mAndroidElements.size() > 0) { - getDocument(); - Change change = new AndroidTypeRenameChange(mAndroidManifest, mManager, mDocument, - mAndroidElements, mNewName, mOldName); - if (change != null) { - result.add(change); - } - } - // add layoutChange - for (AndroidLayoutFileChanges fileChange : mFileChanges) { - IFile file = fileChange.getFile(); - ITextFileBufferManager lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - Change layoutChange = new AndroidLayoutChange(file, lDocument, lManager, - fileChange.getChanges()); - if (layoutChange != null) { - result.add(layoutChange); - } - } - } - return (result.getChildren().length == 0) ? null : result; - - } - - @Override - public String getName() { - return "Android Type Rename"; - } - - @Override - protected boolean initialize(Object element) { - - if (element instanceof IType) { - IType type = (IType) element; - IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT); - IProject project = javaProject.getProject(); - IResource manifestResource = project.findMember(AdtConstants.WS_SEP - + SdkConstants.FN_ANDROID_MANIFEST_XML); - - if (manifestResource == null || !manifestResource.exists() - || !(manifestResource instanceof IFile)) { - RefactoringUtil.logInfo("Invalid or missing the " - + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + project.getName() - + " project."); - return false; - } - mAndroidManifest = (IFile) manifestResource; - ManifestData manifestData; - manifestData = AndroidManifestHelper.parseForData(mAndroidManifest); - if (manifestData == null) { - return false; - } - mAppPackage = manifestData.getPackage(); - mOldName = type.getFullyQualifiedName(); - String packageName = type.getPackageFragment().getElementName(); - mNewName = getArguments().getNewName(); - if (packageName != null) { - mLayoutNewName = packageName + "." + getArguments().getNewName(); //$NON-NLS-1$ - } else { - mLayoutNewName = getArguments().getNewName(); - } - if (mOldName == null || mNewName == null) { - return false; - } - if (!RefactoringUtil.isRefactorAppPackage() && mNewName.indexOf(".") == -1) { //$NON-NLS-1$ - mNewName = packageName + "." + mNewName; //$NON-NLS-1$ - } - mAndroidElements = addAndroidElements(); - try { - ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(null); - if (typeHierarchy == null) { - return false; - } - IType[] superTypes = typeHierarchy.getAllSuperclasses(type); - for (int i = 0; i < superTypes.length; i++) { - IType superType = superTypes[i]; - String className = superType.getFullyQualifiedName(); - if (className.equals(SdkConstants.CLASS_VIEW)) { - addLayoutChanges(project, type.getFullyQualifiedName()); - break; - } - } - } catch (JavaModelException ignore) { - } - - return mAndroidElements.size() > 0 || mFileChanges.size() > 0; - } - return false; - } - - /** - * Adds layout changes for project - * - * @param project the Android project - * @param className the layout classes - * - */ - private void addLayoutChanges(IProject project, String className) { - try { - IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); - IFolder layoutFolder = resFolder.getFolder(SdkConstants.FD_RES_LAYOUT); - IResource[] members = layoutFolder.members(); - for (int i = 0; i < members.length; i++) { - IResource member = members[i]; - if ((member instanceof IFile) && member.exists()) { - IFile file = (IFile) member; - Set<AndroidLayoutChangeDescription> changes = parse(file, className); - if (changes.size() > 0) { - AndroidLayoutFileChanges fileChange = new AndroidLayoutFileChanges(file); - fileChange.getChanges().addAll(changes); - mFileChanges.add(fileChange); - } - } - } - } catch (CoreException e) { - RefactoringUtil.log(e); - } - } - - /** - * Searches the layout file for classes - * - * @param file the Android layout file - * @param className the layout classes - * - */ - private Set<AndroidLayoutChangeDescription> parse(IFile file, String className) { - Set<AndroidLayoutChangeDescription> changes = new HashSet<AndroidLayoutChangeDescription>(); - ITextFileBufferManager lManager = null; - try { - lManager = FileBuffers.getTextFileBufferManager(); - lManager.connect(file.getFullPath(), LocationKind.NORMALIZE, new NullProgressMonitor()); - ITextFileBuffer buffer = lManager.getTextFileBuffer(file.getFullPath(), - LocationKind.NORMALIZE); - IDocument lDocument = buffer.getDocument(); - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager().getExistingModelForRead(lDocument); - if (model == null) { - if (lDocument instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) lDocument; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - NodeList nodes = xmlDoc - .getElementsByTagName(SdkConstants.VIEW); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Node attributeNode = - attributes.getNamedItem(SdkConstants.ATTR_CLASS); - if (attributeNode instanceof Attr) { - Attr attribute = (Attr) attributeNode; - String value = attribute.getValue(); - if (value != null && value.equals(className)) { - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription(className, mLayoutNewName, - AndroidLayoutChangeDescription.VIEW_TYPE); - changes.add(layoutChange); - } - } - } - } - nodes = xmlDoc.getElementsByTagName(className); - for (int i = 0; i < nodes.getLength(); i++) { - AndroidLayoutChangeDescription layoutChange = - new AndroidLayoutChangeDescription(className, mLayoutNewName, - AndroidLayoutChangeDescription.STANDALONE_TYPE); - changes.add(layoutChange); - } - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - } catch (CoreException ignore) { - } finally { - if (lManager != null) { - try { - lManager.disconnect(file.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException ignore) { - } - } - } - return changes; - } - - /** - * Returns the elements (activity, receiver, service ...) - * which have to be renamed - * - * @return the android elements - * - */ - private Map<String, String> addAndroidElements() { - Map<String, String> androidElements = new HashMap<String, String>(); - - IDocument document; - try { - document = getDocument(); - } catch (CoreException e) { - RefactoringUtil.log(e); - if (mManager != null) { - try { - mManager.disconnect(mAndroidManifest.getFullPath(), LocationKind.NORMALIZE, - new NullProgressMonitor()); - } catch (CoreException e1) { - RefactoringUtil.log(e1); - } - } - document = null; - return androidElements; - } - - IStructuredModel model = null; - try { - model = StructuredModelManager.getModelManager().getExistingModelForRead(document); - if (model == null) { - if (document instanceof IStructuredDocument) { - IStructuredDocument structuredDocument = (IStructuredDocument) document; - model = StructuredModelManager.getModelManager().getModelForRead( - structuredDocument); - } - } - if (model != null) { - IDOMModel xmlModel = (IDOMModel) model; - IDOMDocument xmlDoc = xmlModel.getDocument(); - add(xmlDoc, androidElements, AndroidManifest.NODE_ACTIVITY, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_APPLICATION, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_PROVIDER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_RECEIVER, - AndroidManifest.ATTRIBUTE_NAME); - add(xmlDoc, androidElements, AndroidManifest.NODE_SERVICE, - AndroidManifest.ATTRIBUTE_NAME); - } - } finally { - if (model != null) { - model.releaseFromRead(); - } - } - - return androidElements; - } - - /** - * (non-Javadoc) Adds the element (activity, receiver, service ...) to the map - * - * @param xmlDoc the document - * @param androidElements the map - * @param element the element - */ - private void add(IDOMDocument xmlDoc, Map<String, String> androidElements, String element, - String argument) { - NodeList nodes = xmlDoc.getElementsByTagName(element); - for (int i = 0; i < nodes.getLength(); i++) { - Node node = nodes.item(i); - NamedNodeMap attributes = node.getAttributes(); - if (attributes != null) { - Attr attribute = RefactoringUtil.findAndroidAttributes(attributes, argument); - if (attribute != null) { - String value = attribute.getValue(); - if (value != null) { - String fullName = AndroidManifest.combinePackageAndClassName(mAppPackage, - value); - if (fullName != null && fullName.equals(mOldName)) { - androidElements.put(element, value); - } - } - } - } - } - } - - - -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/RefactoringUtil.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/RefactoringUtil.java deleted file mode 100644 index 1121081..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/RefactoringUtil.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.refactoring.core; - -import com.android.SdkConstants; -import com.android.ide.eclipse.adt.AdtPlugin; - -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.jface.text.IDocument; -import org.eclipse.wst.sse.core.StructuredModelManager; -import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; -import org.w3c.dom.Attr; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; - -/** - * The utility class for android refactoring - * - */ -@SuppressWarnings("restriction") -public class RefactoringUtil { - - private static boolean sRefactorAppPackage = false; - - /** - * Releases SSE read model; saves SSE model if exists edit model - * Called in dispose method of refactoring change classes - * - * @param model the SSE model - * @param document the document - */ - public static void fixModel(IStructuredModel model, IDocument document) { - if (model != null) { - model.releaseFromRead(); - } - model = null; - if (document == null) { - return; - } - try { - model = StructuredModelManager.getModelManager().getExistingModelForEdit(document); - if (model != null) { - model.save(); - } - } catch (UnsupportedEncodingException e1) { - // ignore - } catch (IOException e1) { - // ignore - } catch (CoreException e1) { - // ignore - } finally { - if (model != null) { - model.releaseFromEdit(); - } - } - } - - /** - * Finds attribute by name in android namespace - * - * @param attributes the attributes collection - * @param localName the local part of the qualified name - * - * @return the first attribute with this name in android namespace - */ - public static Attr findAndroidAttributes(final NamedNodeMap attributes, - final String localName) { - Attr attribute = null; - for (int j = 0; j < attributes.getLength(); j++) { - Node attNode = attributes.item(j); - if (attNode instanceof Attr) { - Attr attr = (Attr) attNode; - String name = attr.getLocalName(); - String namespace = attr.getNamespaceURI(); - if (SdkConstants.NS_RESOURCES.equals(namespace) - && name != null - && name.equals(localName)) { - attribute = attr; - break; - } - } - } - return attribute; - } - - /** - * Logs the error message - * - * @param message the message - */ - public static void logError(String message) { - AdtPlugin.log(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message); - } - - /** - * Logs the info message - * - * @param message the message - */ - public static void logInfo(String message) { - AdtPlugin.log(IStatus.INFO, AdtPlugin.PLUGIN_ID, message); - } - - /** - * Logs the the exception - * - * @param e the exception - */ - public static void log(Throwable e) { - AdtPlugin.log(e, e.getMessage()); - } - - /** - * @return true if Rename/Move package needs to change the application package - * default is false - * - */ - public static boolean isRefactorAppPackage() { - return sRefactorAppPackage; - } - - /** - * @param refactorAppPackage true if Rename/Move package needs to change the application package - */ - public static void setRefactorAppPackage(boolean refactorAppPackage) { - RefactoringUtil.sRefactorAppPackage = refactorAppPackage; - } -} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java new file mode 100644 index 0000000..b821777 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidPackageRenameParticipant.java @@ -0,0 +1,547 @@ +/* + * 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PACKAGE; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.SdkConstants.VIEW_TAG; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.corext.refactoring.changes.RenamePackageChange; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.FileStatusContext; +import org.eclipse.ltk.core.refactoring.NullChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.RefactoringStatusContext; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor; +import org.eclipse.ltk.core.refactoring.participants.RenameParticipant; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A participant to participate in refactorings that rename a package in an Android project. + * The class updates android manifest and the layout file + * The user can suppress refactoring by disabling the "Update references" checkbox + * <p> + * Rename participants are registered via the extension point <code> + * org.eclipse.ltk.core.refactoring.renameParticipants</code>. + * Extensions to this extension point must therefore extend + * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>. + * </p> + */ +@SuppressWarnings("restriction") +public class AndroidPackageRenameParticipant extends RenameParticipant { + + private IProject mProject; + private IFile mManifestFile; + private IPackageFragment mPackageFragment; + private String mOldPackage; + private String mNewPackage; + private String mAppPackage; + private boolean mRefactoringAppPackage; + + @Override + public String getName() { + return "Android Package Rename"; + } + + @Override + public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) + throws OperationCanceledException { + if (mAppPackage.equals(mOldPackage) && !mRefactoringAppPackage) { + IRegion region = null; + Document document = DomUtilities.getDocument(mManifestFile); + if (document != null && document.getDocumentElement() != null) { + Attr attribute = document.getDocumentElement().getAttributeNode(ATTR_PACKAGE); + if (attribute instanceof IndexedRegion) { + IndexedRegion ir = (IndexedRegion) attribute; + int start = ir.getStartOffset(); + region = new Region(start, ir.getEndOffset() - start); + } + } + if (region == null) { + region = new Region(0, 0); + } + // There's no line wrapping in the error dialog, so split up the message into + // individually digestible pieces of information + RefactoringStatusContext ctx = new FileStatusContext(mManifestFile, region); + RefactoringStatus status = RefactoringStatus.createInfoStatus( + "You are refactoring the same package as your application's " + + "package (specified in the manifest).\n", ctx); + status.addInfo( + "Note that this refactoring does NOT also update your " + + "application package.", ctx); + status.addInfo("The application package defines your application's identity.", ctx); + status.addInfo( + "If you change it, then it is considered to be a different application.", ctx); + status.addInfo("(Users of the previous version cannot update to the new version.)", + ctx); + status.addInfo( + "The application package, and the package containing the code, can differ.", + ctx); + status.addInfo( + "To really change application package, " + + "choose \"Android Tools\" > \"Rename Application Package.\" " + + "from the project context menu.", ctx); + return status; + } + + return new RefactoringStatus(); + } + + @Override + protected boolean initialize(final Object element) { + mRefactoringAppPackage = false; + try { + // Only propose this refactoring if the "Update References" checkbox is set. + if (!getArguments().getUpdateReferences()) { + return false; + } + + if (element instanceof IPackageFragment) { + mPackageFragment = (IPackageFragment) element; + if (!mPackageFragment.containsJavaResources()) { + return false; + } + IJavaProject javaProject = (IJavaProject) mPackageFragment + .getAncestor(IJavaElement.JAVA_PROJECT); + mProject = javaProject.getProject(); + IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP + + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (manifestResource == null || !manifestResource.exists() + || !(manifestResource instanceof IFile)) { + RefactoringUtil.logInfo("Invalid or missing the " + + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + + mProject.getName() + " project."); + return false; + } + mManifestFile = (IFile) manifestResource; + String packageName = mPackageFragment.getElementName(); + ManifestData manifestData; + manifestData = AndroidManifestHelper.parseForData(mManifestFile); + if (manifestData == null) { + return false; + } + mAppPackage = manifestData.getPackage(); + mOldPackage = packageName; + mNewPackage = getArguments().getNewName(); + if (mOldPackage == null || mNewPackage == null) { + return false; + } + + if (RefactoringUtil.isRefactorAppPackage() + && mAppPackage != null + && mAppPackage.equals(packageName)) { + mRefactoringAppPackage = true; + } + + return true; + } + } catch (JavaModelException ignore) { + } + return false; + } + + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + if (pm.isCanceled()) { + return null; + } + if (!getArguments().getUpdateReferences()) { + return null; + } + + RefactoringProcessor p = getProcessor(); + if (p instanceof RenameCompilationUnitProcessor) { + RenameTypeProcessor rtp = + ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor(); + if (rtp != null) { + String pattern = rtp.getFilePatterns(); + boolean updQualf = rtp.getUpdateQualifiedNames(); + if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$ + // Do not propose this refactoring if the + // "Update fully qualified names in non-Java files" option is + // checked and the file patterns mention XML. [c.f. SDK bug 21589] + return null; + } + } + } + + IPath pkgPath = mPackageFragment.getPath(); + IPath genPath = mProject.getFullPath().append(SdkConstants.FD_GEN_SOURCES); + if (genPath.isPrefixOf(pkgPath)) { + RefactoringUtil.logInfo(getName() + ": Cannot rename generated package."); + return null; + } + CompositeChange result = new CompositeChange(getName()); + result.markAsSynthetic(); + + addManifestFileChanges(result); + + // Update layout files; we don't just need to react to custom view + // changes, we need to update fragment references and even tool:context activity + // references + addLayoutFileChanges(mProject, result); + + // Also update in dependent projects + ProjectState projectState = Sdk.getProjectState(mProject); + if (projectState != null) { + Collection<ProjectState> parentProjects = projectState.getFullParentProjects(); + for (ProjectState parentProject : parentProjects) { + IProject project = parentProject.getProject(); + addLayoutFileChanges(project, result); + } + } + + if (mRefactoringAppPackage) { + Change genChange = getGenPackageChange(pm); + if (genChange != null) { + result.add(genChange); + } + + return new NullChange("Update Imports") { + @Override + public Change perform(IProgressMonitor monitor) throws CoreException { + FixImportsJob job = new FixImportsJob("Fix Rename Package", + mManifestFile, mNewPackage); + job.schedule(500); + + // Not undoable: just return null instead of an undo-change. + return null; + } + }; + } + + return (result.getChildren().length == 0) ? null : result; + } + + /** + * Returns Android gen package text change + * + * @param pm the progress monitor + * + * @return Android gen package text change + * @throws CoreException if an error happens + * @throws OperationCanceledException if the operation is canceled + */ + public Change getGenPackageChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + if (mRefactoringAppPackage) { + IPackageFragment genJavaPackageFragment = getGenPackageFragment(); + if (genJavaPackageFragment != null && genJavaPackageFragment.exists()) { + return new RenamePackageChange(genJavaPackageFragment, mNewPackage, true); + } + } + return null; + } + + /** + * Return the gen package fragment + */ + private IPackageFragment getGenPackageFragment() throws JavaModelException { + IJavaProject javaProject = (IJavaProject) mPackageFragment + .getAncestor(IJavaElement.JAVA_PROJECT); + if (javaProject != null && javaProject.isOpen()) { + IProject project = javaProject.getProject(); + IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES); + if (genFolder.exists()) { + String javaPackagePath = mAppPackage.replace('.', '/'); + IPath genJavaPackagePath = genFolder.getFullPath().append(javaPackagePath); + IPackageFragment genPackageFragment = javaProject + .findPackageFragment(genJavaPackagePath); + return genPackageFragment; + } + } + return null; + } + + /** + * Returns the new class name + * + * @param fqcn the fully qualified class name in the renamed package + * @return the new class name + */ + private String getNewClassName(String fqcn) { + assert isInRenamedPackage(fqcn) : fqcn; + int lastDot = fqcn.lastIndexOf('.'); + if (lastDot < 0) { + return mNewPackage; + } + String name = fqcn.substring(lastDot, fqcn.length()); + String newClassName = mNewPackage + name; + return newClassName; + } + + private void addManifestFileChanges(CompositeChange result) { + addXmlFileChanges(mManifestFile, result, true); + } + + private void addLayoutFileChanges(IProject project, CompositeChange result) { + try { + // Update references in XML resource files + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + IResource[] folders = resFolder.members(); + for (IResource folder : folders) { + String folderName = folder.getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); + if (folderType != ResourceFolderType.LAYOUT) { + continue; + } + if (!(folder instanceof IFolder)) { + continue; + } + IResource[] files = ((IFolder) folder).members(); + for (int i = 0; i < files.length; i++) { + IResource member = files[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + String fileName = member.getName(); + + if (SdkUtils.endsWith(fileName, DOT_XML)) { + addXmlFileChanges(file, result, false); + } + } + } + } + } catch (CoreException e) { + RefactoringUtil.log(e); + } + } + + private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = modelManager.getExistingModelForRead(file); + if (model == null) { + model = modelManager.getModelForRead(file); + } + if (model != null) { + IStructuredDocument document = model.getStructuredDocument(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Element root = domModel.getDocument().getDocumentElement(); + if (root != null) { + List<TextEdit> edits = new ArrayList<TextEdit>(); + if (isManifest) { + addManifestReplacements(edits, root, document); + } else { + addLayoutReplacements(edits, root, document); + } + if (!edits.isEmpty()) { + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); + TextFileChange change = new TextFileChange(file.getName(), file); + change.setTextType(EXT_XML); + change.setEdit(rootEdit); + changes.add(change); + } + } + } else { + return false; + } + } + + return true; + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return false; + } + + private boolean isInRenamedPackage(String fqcn) { + return fqcn.startsWith(mOldPackage) + && fqcn.length() > mOldPackage.length() + && fqcn.indexOf('.', mOldPackage.length() + 1) == -1; + } + + private void addLayoutReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + String tag = element.getTagName(); + if (isInRenamedPackage(tag)) { + int start = RefactoringUtil.getTagNameRangeStart(element, document); + if (start != -1) { + int end = start + tag.length(); + edits.add(new ReplaceEdit(start, end - start, getNewClassName(tag))); + } + } else { + Attr classNode = null; + if (tag.equals(VIEW_TAG)) { + classNode = element.getAttributeNode(ATTR_CLASS); + } else if (tag.equals(VIEW_FRAGMENT)) { + classNode = element.getAttributeNode(ATTR_CLASS); + if (classNode == null) { + classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); + } + } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) { + classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT); + if (classNode != null && classNode.getValue().startsWith(".")) { //$NON-NLS-1$ + classNode = null; + } + } + if (classNode != null) { + String fqcn = classNode.getValue(); + if (isInRenamedPackage(fqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + fqcn.length(); + edits.add(new ReplaceEdit(start, end - start, getNewClassName(fqcn))); + } + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addLayoutReplacements(edits, (Element) child, document); + } + } + } + + private void addManifestReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + if (mRefactoringAppPackage && + element == element.getOwnerDocument().getDocumentElement()) { + // Update the app package declaration + Attr pkg = element.getAttributeNode(ATTR_PACKAGE); + if (pkg != null && pkg.getValue().equals(mOldPackage)) { + int start = RefactoringUtil.getAttributeValueRangeStart(pkg, document); + if (start != -1) { + int end = start + mOldPackage.length(); + edits.add(new ReplaceEdit(start, end - start, mNewPackage)); + } + } + } + + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attr = (Attr) attributes.item(i); + if (!RefactoringUtil.isManifestClassAttribute(attr)) { + continue; + } + + String value = attr.getValue(); + if (isInRenamedPackage(value)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + value.length(); + edits.add(new ReplaceEdit(start, end - start, getNewClassName(value))); + } + } else if (value.startsWith(".")) { + // If we're renaming the app package + String fqcn = mAppPackage + value; + if (isInRenamedPackage(fqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + value.length(); + String newClassName = getNewClassName(fqcn); + if (mRefactoringAppPackage) { + newClassName = newClassName.substring(mNewPackage.length()); + } else if (newClassName.startsWith(mOldPackage) + && newClassName.charAt(mOldPackage.length()) == '.') { + newClassName = newClassName.substring(mOldPackage.length()); + } + + if (!newClassName.equals(value)) { + edits.add(new ReplaceEdit(start, end - start, newClassName)); + } + } + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addManifestReplacements(edits, (Element) child, document); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java new file mode 100644 index 0000000..2146184 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeMoveParticipant.java @@ -0,0 +1,362 @@ +/* + * 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.SdkConstants.VIEW_TAG; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.MoveParticipant; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A participant to participate in refactorings that move a type in an Android project. + * The class updates android manifest and the layout file + * The user can suppress refactoring by disabling the "Update references" checkbox + * <p> + * Rename participants are registered via the extension point <code> + * org.eclipse.ltk.core.refactoring.moveParticipants</code>. + * Extensions to this extension point must therefore extend <code>org.eclipse.ltk.core.refactoring.participants.MoveParticipant</code>. + * </p> + */ +@SuppressWarnings("restriction") +public class AndroidTypeMoveParticipant extends MoveParticipant { + + private IProject mProject; + protected IFile mManifestFile; + protected String mOldFqcn; + protected String mNewFqcn; + protected String mAppPackage; + + @Override + public String getName() { + return "Android Type Move"; + } + + @Override + public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) + throws OperationCanceledException { + return new RefactoringStatus(); + } + + @Override + protected boolean initialize(Object element) { + if (element instanceof IType) { + IType type = (IType) element; + IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT); + mProject = javaProject.getProject(); + IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP + + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (manifestResource == null || !manifestResource.exists() + || !(manifestResource instanceof IFile)) { + RefactoringUtil.logInfo("Invalid or missing the " + + SdkConstants.FN_ANDROID_MANIFEST_XML + " in the " + mProject.getName() + + " project."); + return false; + } + mManifestFile = (IFile) manifestResource; + ManifestData manifestData; + manifestData = AndroidManifestHelper.parseForData(mManifestFile); + if (manifestData == null) { + return false; + } + mAppPackage = manifestData.getPackage(); + mOldFqcn = type.getFullyQualifiedName(); + Object destination = getArguments().getDestination(); + if (destination instanceof IPackageFragment) { + IPackageFragment packageFragment = (IPackageFragment) destination; + mNewFqcn = packageFragment.getElementName() + "." + type.getElementName(); + } else if (destination instanceof IResource) { + try { + IPackageFragment[] fragments = javaProject.getPackageFragments(); + for (IPackageFragment fragment : fragments) { + IResource resource = fragment.getResource(); + if (resource.equals(destination)) { + mNewFqcn = fragment.getElementName() + '.' + type.getElementName(); + break; + } + } + } catch (JavaModelException e) { + // pass + } + } + return mOldFqcn != null && mNewFqcn != null; + } + + return false; + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + if (pm.isCanceled()) { + return null; + } + if (!getArguments().getUpdateReferences()) { + return null; + } + CompositeChange result = new CompositeChange(getName()); + result.markAsSynthetic(); + + addManifestFileChanges(result); + + // Update layout files; we don't just need to react to custom view + // changes, we need to update fragment references and even tool:context activity + // references + addLayoutFileChanges(mProject, result); + + // Also update in dependent projects + ProjectState projectState = Sdk.getProjectState(mProject); + if (projectState != null) { + Collection<ProjectState> parentProjects = projectState.getFullParentProjects(); + for (ProjectState parentProject : parentProjects) { + IProject project = parentProject.getProject(); + addLayoutFileChanges(project, result); + } + } + + return (result.getChildren().length == 0) ? null : result; + } + + private void addManifestFileChanges(CompositeChange result) { + addXmlFileChanges(mManifestFile, result, true); + } + + private void addLayoutFileChanges(IProject project, CompositeChange result) { + try { + // Update references in XML resource files + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + IResource[] folders = resFolder.members(); + for (IResource folder : folders) { + String folderName = folder.getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); + if (folderType != ResourceFolderType.LAYOUT) { + continue; + } + if (!(folder instanceof IFolder)) { + continue; + } + IResource[] files = ((IFolder) folder).members(); + for (int i = 0; i < files.length; i++) { + IResource member = files[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + String fileName = member.getName(); + + if (SdkUtils.endsWith(fileName, DOT_XML)) { + addXmlFileChanges(file, result, false); + } + } + } + } + } catch (CoreException e) { + RefactoringUtil.log(e); + } + } + + private boolean addXmlFileChanges(IFile file, CompositeChange changes, boolean isManifest) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = modelManager.getExistingModelForRead(file); + if (model == null) { + model = modelManager.getModelForRead(file); + } + if (model != null) { + IStructuredDocument document = model.getStructuredDocument(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Element root = domModel.getDocument().getDocumentElement(); + if (root != null) { + List<TextEdit> edits = new ArrayList<TextEdit>(); + if (isManifest) { + addManifestReplacements(edits, root, document); + } else { + addLayoutReplacements(edits, root, document); + } + if (!edits.isEmpty()) { + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); + TextFileChange change = new TextFileChange(file.getName(), file); + change.setTextType(EXT_XML); + change.setEdit(rootEdit); + changes.add(change); + } + } + } else { + return false; + } + } + + return true; + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return false; + } + + private void addLayoutReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + String tag = element.getTagName(); + if (tag.equals(mOldFqcn)) { + int start = RefactoringUtil.getTagNameRangeStart(element, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } else if (tag.equals(VIEW_TAG)) { + Attr classNode = element.getAttributeNode(ATTR_CLASS); + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } else if (tag.equals(VIEW_FRAGMENT)) { + Attr classNode = element.getAttributeNode(ATTR_CLASS); + if (classNode == null) { + classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); + } + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) { + Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT); + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addLayoutReplacements(edits, (Element) child, document); + } + } + } + + private void addManifestReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attr = (Attr) attributes.item(i); + if (!RefactoringUtil.isManifestClassAttribute(attr)) { + continue; + } + + String value = attr.getValue(); + if (value.equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } else if (value.startsWith(".")) { //$NON-NLS-1$ + String fqcn = mAppPackage + value; + if (fqcn.equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + value.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addManifestReplacements(edits, (Element) child, document); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java new file mode 100644 index 0000000..7843ab3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/AndroidTypeRenameParticipant.java @@ -0,0 +1,529 @@ +/* + * 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_CLASS; +import static com.android.SdkConstants.ATTR_CONTEXT; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.CLASS_VIEW; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.R_CLASS; +import static com.android.SdkConstants.TOOLS_URI; +import static com.android.SdkConstants.VIEW_FRAGMENT; +import static com.android.SdkConstants.VIEW_TAG; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameCompilationUnitProcessor; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor; +import org.eclipse.ltk.core.refactoring.participants.RenameParticipant; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A participant to participate in refactorings that rename a type in an Android project. + * The class updates android manifest and the layout file + * The user can suppress refactoring by disabling the "Update references" checkbox. + * <p> + * Rename participants are registered via the extension point <code> + * org.eclipse.ltk.core.refactoring.renameParticipants</code>. + * Extensions to this extension point must therefore extend + * <code>org.eclipse.ltk.core.refactoring.participants.RenameParticipant</code>. + */ +@SuppressWarnings("restriction") +public class AndroidTypeRenameParticipant extends RenameParticipant { + private IProject mProject; + private IFile mManifestFile; + private String mOldFqcn; + private String mNewFqcn; + private String mOldSimpleName; + private String mNewSimpleName; + private String mOldDottedName; + private String mNewDottedName; + private boolean mIsCustomView; + + /** + * Set while we are creating an embedded Java refactoring. This could cause a recursive + * invocation of the XML renaming refactoring to react to the field, so this is flag + * during the call to the Java processor, and is used to ignore requests for adding in + * field reactions during that time. + */ + private static boolean sIgnore; + + @Override + public String getName() { + return "Android Type Rename"; + } + + @Override + public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) + throws OperationCanceledException { + return new RefactoringStatus(); + } + + @Override + protected boolean initialize(Object element) { + if (sIgnore) { + return false; + } + + if (element instanceof IType) { + IType type = (IType) element; + IJavaProject javaProject = (IJavaProject) type.getAncestor(IJavaElement.JAVA_PROJECT); + mProject = javaProject.getProject(); + IResource manifestResource = mProject.findMember(AdtConstants.WS_SEP + + SdkConstants.FN_ANDROID_MANIFEST_XML); + + if (manifestResource == null || !manifestResource.exists() + || !(manifestResource instanceof IFile)) { + RefactoringUtil.logInfo( + String.format("Invalid or missing file %1$s in project %2$s", + SdkConstants.FN_ANDROID_MANIFEST_XML, + mProject.getName())); + return false; + } + + try { + IType classView = javaProject.findType(CLASS_VIEW); + if (classView != null) { + ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); + if (hierarchy.contains(classView)) { + mIsCustomView = true; + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + mManifestFile = (IFile) manifestResource; + ManifestData manifestData; + manifestData = AndroidManifestHelper.parseForData(mManifestFile); + if (manifestData == null) { + return false; + } + mOldSimpleName = type.getElementName(); + mOldDottedName = '.' + mOldSimpleName; + mOldFqcn = type.getFullyQualifiedName(); + String packageName = type.getPackageFragment().getElementName(); + mNewSimpleName = getArguments().getNewName(); + mNewDottedName = '.' + mNewSimpleName; + if (packageName != null) { + mNewFqcn = packageName + mNewDottedName; + } else { + mNewFqcn = mNewSimpleName; + } + if (mOldFqcn == null || mNewFqcn == null) { + return false; + } + if (!RefactoringUtil.isRefactorAppPackage() && mNewFqcn.indexOf('.') == -1) { + mNewFqcn = packageName + mNewDottedName; + } + return true; + } + return false; + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException, + OperationCanceledException { + if (pm.isCanceled()) { + return null; + } + + // Only propose this refactoring if the "Update References" checkbox is set. + if (!getArguments().getUpdateReferences()) { + return null; + } + + RefactoringProcessor p = getProcessor(); + if (p instanceof RenameCompilationUnitProcessor) { + RenameTypeProcessor rtp = + ((RenameCompilationUnitProcessor) p).getRenameTypeProcessor(); + if (rtp != null) { + String pattern = rtp.getFilePatterns(); + boolean updQualf = rtp.getUpdateQualifiedNames(); + if (updQualf && pattern != null && pattern.contains("xml")) { //$NON-NLS-1$ + // Do not propose this refactoring if the + // "Update fully qualified names in non-Java files" option is + // checked and the file patterns mention XML. [c.f. SDK bug 21589] + return null; + } + } + } + + CompositeChange result = new CompositeChange(getName()); + + // Only show the children in the refactoring preview dialog + result.markAsSynthetic(); + + addManifestFileChanges(mManifestFile, result); + addLayoutFileChanges(mProject, result); + addJavaChanges(mProject, result, pm); + + // Also update in dependent projects + // TODO: Also do the Java elements, if they are in Jar files, since the library + // projects do this (and the JDT refactoring does not include them) + ProjectState projectState = Sdk.getProjectState(mProject); + if (projectState != null) { + Collection<ProjectState> parentProjects = projectState.getFullParentProjects(); + for (ProjectState parentProject : parentProjects) { + IProject project = parentProject.getProject(); + IResource manifestResource = project.findMember(AdtConstants.WS_SEP + + SdkConstants.FN_ANDROID_MANIFEST_XML); + if (manifestResource != null && manifestResource.exists() + && manifestResource instanceof IFile) { + addManifestFileChanges((IFile) manifestResource, result); + } + addLayoutFileChanges(project, result); + addJavaChanges(project, result, pm); + } + } + + // Look for the field change on the R.java class; it's a derived file + // and will generate file modified manually warnings. Disable it. + RenameResourceParticipant.disableRClassChanges(result); + + return (result.getChildren().length == 0) ? null : result; + } + + private void addJavaChanges(IProject project, CompositeChange result, IProgressMonitor monitor) { + if (!mIsCustomView) { + return; + } + + // Also rename styleables, if any + try { + // Find R class + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + ManifestInfo info = ManifestInfo.get(project); + info.getPackage(); + String rFqcn = info.getPackage() + '.' + R_CLASS; + IType styleable = javaProject.findType(rFqcn + '.' + ResourceType.STYLEABLE.getName()); + if (styleable != null) { + IField[] fields = styleable.getFields(); + CompositeChange fieldChanges = null; + for (IField field : fields) { + String name = field.getElementName(); + if (name.equals(mOldSimpleName) || name.startsWith(mOldSimpleName) + && name.length() > mOldSimpleName.length() + && name.charAt(mOldSimpleName.length()) == '_') { + // Rename styleable fields + String newName = name.equals(mOldSimpleName) ? mNewSimpleName : + mNewSimpleName + name.substring(mOldSimpleName.length()); + RenameRefactoring refactoring = + RenameResourceParticipant.createFieldRefactoring(field, + newName, true); + + try { + sIgnore = true; + RefactoringStatus status = refactoring.checkAllConditions(monitor); + if (status != null && !status.hasError()) { + Change fieldChange = refactoring.createChange(monitor); + if (fieldChange != null) { + if (fieldChanges == null) { + fieldChanges = new CompositeChange( + "Update custom view styleable fields"); + // Disable these changes. They sometimes end up + // editing the wrong offsets. It looks like Eclipse + // doesn't ensure that after applying each change it + // also adjusts the other field offsets. I poked around + // and couldn't find a way to do this properly, but + // at least by listing the diffs here it shows what should + // be done. + fieldChanges.setEnabled(false); + } + // Disable change: see comment above. + fieldChange.setEnabled(false); + fieldChanges.add(fieldChange); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + sIgnore = false; + } + } + } + if (fieldChanges != null) { + result.add(fieldChanges); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + + private void addManifestFileChanges(IFile manifestFile, CompositeChange result) { + addXmlFileChanges(manifestFile, result, null); + } + + private void addLayoutFileChanges(IProject project, CompositeChange result) { + try { + // Update references in XML resource files + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + IResource[] folders = resFolder.members(); + for (IResource folder : folders) { + String folderName = folder.getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); + if (folderType != ResourceFolderType.LAYOUT && + folderType != ResourceFolderType.VALUES) { + continue; + } + if (!(folder instanceof IFolder)) { + continue; + } + IResource[] files = ((IFolder) folder).members(); + for (int i = 0; i < files.length; i++) { + IResource member = files[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + String fileName = member.getName(); + + if (SdkUtils.endsWith(fileName, DOT_XML)) { + addXmlFileChanges(file, result, folderType); + } + } + } + } + } catch (CoreException e) { + RefactoringUtil.log(e); + } + } + + private boolean addXmlFileChanges(IFile file, CompositeChange changes, + ResourceFolderType folderType) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = modelManager.getExistingModelForRead(file); + if (model == null) { + model = modelManager.getModelForRead(file); + } + if (model != null) { + IStructuredDocument document = model.getStructuredDocument(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Element root = domModel.getDocument().getDocumentElement(); + if (root != null) { + List<TextEdit> edits = new ArrayList<TextEdit>(); + if (folderType == null) { + assert file.getName().equals(ANDROID_MANIFEST_XML); + addManifestReplacements(edits, root, document); + } else if (folderType == ResourceFolderType.VALUES) { + addValueReplacements(edits, root, document); + } else { + assert folderType == ResourceFolderType.LAYOUT; + addLayoutReplacements(edits, root, document); + } + if (!edits.isEmpty()) { + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); + TextFileChange change = new TextFileChange(file.getName(), file); + change.setTextType(EXT_XML); + change.setEdit(rootEdit); + changes.add(change); + } + } + } else { + return false; + } + } + + return true; + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return false; + } + + private void addLayoutReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + String tag = element.getTagName(); + if (tag.equals(mOldFqcn)) { + int start = RefactoringUtil.getTagNameRangeStart(element, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } else if (tag.equals(VIEW_TAG)) { + // TODO: Handle inner classes ($ vs .) ? + Attr classNode = element.getAttributeNode(ATTR_CLASS); + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } else if (tag.equals(VIEW_FRAGMENT)) { + Attr classNode = element.getAttributeNode(ATTR_CLASS); + if (classNode == null) { + classNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); + } + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } + } else if (element.hasAttributeNS(TOOLS_URI, ATTR_CONTEXT)) { + Attr classNode = element.getAttributeNodeNS(TOOLS_URI, ATTR_CONTEXT); + if (classNode != null && classNode.getValue().equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } else if (classNode != null && classNode.getValue().equals(mOldDottedName)) { + int start = RefactoringUtil.getAttributeValueRangeStart(classNode, document); + if (start != -1) { + int end = start + mOldDottedName.length(); + edits.add(new ReplaceEdit(start, end - start, mNewDottedName)); + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addLayoutReplacements(edits, (Element) child, document); + } + } + } + + private void addValueReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element root, + @NonNull IStructuredDocument document) { + // Look for styleable renames for custom views + String declareStyleable = ResourceType.DECLARE_STYLEABLE.getName(); + List<Element> topLevel = DomUtilities.getChildren(root); + for (Element element : topLevel) { + String tag = element.getTagName(); + if (declareStyleable.equals(tag)) { + Attr nameNode = element.getAttributeNode(ATTR_NAME); + if (nameNode != null && mOldSimpleName.equals(nameNode.getValue())) { + int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document); + if (start != -1) { + int end = start + mOldSimpleName.length(); + edits.add(new ReplaceEdit(start, end - start, mNewSimpleName)); + } + } + } + } + } + + private void addManifestReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document) { + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attr = (Attr) attributes.item(i); + if (!RefactoringUtil.isManifestClassAttribute(attr)) { + continue; + } + + String value = attr.getValue(); + if (value.equals(mOldFqcn)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + mOldFqcn.length(); + edits.add(new ReplaceEdit(start, end - start, mNewFqcn)); + } + } else if (value.equals(mOldDottedName)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + mOldDottedName.length(); + edits.add(new ReplaceEdit(start, end - start, mNewDottedName)); + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addManifestReplacements(edits, (Element) child, document); + } + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/FixImportsJob.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java index 3b63acf..552e6a8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactoring/core/FixImportsJob.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/FixImportsJob.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ide.eclipse.adt.internal.refactoring.core; +package com.android.ide.eclipse.adt.internal.refactorings.core; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java new file mode 100644 index 0000000..04ebcfa --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RefactoringUtil.java @@ -0,0 +1,224 @@ +/* + * 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_BACKUP_AGENT; +import static com.android.xml.AndroidManifest.ATTRIBUTE_MANAGE_SPACE_ACTIVITY; +import static com.android.xml.AndroidManifest.ATTRIBUTE_PARENT_ACTIVITY_NAME; +import static com.android.xml.AndroidManifest.ATTRIBUTE_TARGET_ACTIVITY; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.xml.AndroidManifest; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * The utility class for android refactoring + * + */ +@SuppressWarnings("restriction") +public class RefactoringUtil { + + private static boolean sRefactorAppPackage = false; + + /** + * Releases SSE read model; saves SSE model if exists edit model + * Called in dispose method of refactoring change classes + * + * @param model the SSE model + * @param document the document + */ + public static void fixModel(IStructuredModel model, IDocument document) { + if (model != null) { + model.releaseFromRead(); + } + model = null; + if (document == null) { + return; + } + try { + model = StructuredModelManager.getModelManager().getExistingModelForEdit(document); + if (model != null) { + model.save(); + } + } catch (UnsupportedEncodingException e1) { + // ignore + } catch (IOException e1) { + // ignore + } catch (CoreException e1) { + // ignore + } finally { + if (model != null) { + model.releaseFromEdit(); + } + } + } + + /** + * Logs the info message + * + * @param message the message + */ + public static void logInfo(String message) { + AdtPlugin.log(IStatus.INFO, AdtPlugin.PLUGIN_ID, message); + } + + /** + * Logs the the exception + * + * @param e the exception + */ + public static void log(Throwable e) { + AdtPlugin.log(e, e.getMessage()); + } + + /** + * @return true if Rename/Move package needs to change the application package + * default is false + * + */ + public static boolean isRefactorAppPackage() { + return sRefactorAppPackage; + } + + /** + * @param refactorAppPackage true if Rename/Move package needs to change the application package + */ + public static void setRefactorAppPackage(boolean refactorAppPackage) { + RefactoringUtil.sRefactorAppPackage = refactorAppPackage; + } + + /** + * Returns the range of the attribute value in the given document + * + * @param attr the attribute to look up + * @param document the document containing the attribute + * @return the range of the value text, not including quotes, in the document + */ + public static int getAttributeValueRangeStart( + @NonNull Attr attr, + @NonNull IDocument document) { + IndexedRegion region = (IndexedRegion) attr; + int potentialStart = attr.getName().length() + 2; // + 2: add =" + String text; + try { + text = document.get(region.getStartOffset(), + region.getEndOffset() - region.getStartOffset()); + } catch (BadLocationException e) { + return -1; + } + String value = attr.getValue(); + int index = text.indexOf(value, potentialStart); + if (index != -1) { + return region.getStartOffset() + index; + } else { + return -1; + } + } + + /** + * Returns the start of the tag name of the given element + * + * @param element the element to look up + * @param document the document containing the attribute + * @return the index of the start tag in the document + */ + public static int getTagNameRangeStart( + @NonNull Element element, + @NonNull IDocument document) { + IndexedRegion region = (IndexedRegion) element; + int potentialStart = 1; // add '<' + String text; + try { + text = document.get(region.getStartOffset(), + region.getEndOffset() - region.getStartOffset()); + } catch (BadLocationException e) { + return -1; + } + int index = text.indexOf(element.getTagName(), potentialStart); + if (index != -1) { + return region.getStartOffset() + index; + } else { + return -1; + } + } + + /** + * Returns whether the given manifest attribute should be considered to describe + * a class name. These will be eligible for refactoring when classes are renamed + * or moved. + * + * @param attribute the manifest attribute + * @return true if this attribute can describe a class + */ + public static boolean isManifestClassAttribute(@NonNull Attr attribute) { + return isManifestClassAttribute( + attribute.getOwnerElement().getTagName(), + attribute.getNamespaceURI(), + attribute.getLocalName()); + } + + /** + * Returns whether the given manifest attribute should be considered to describe + * a class name. These will be eligible for refactoring when classes are renamed + * or moved. + * + * @param tag the tag, if known + * @param uri the attribute namespace, if any + * @param name the attribute local name, if any + * @return true if this attribute can describe a class + */ + public static boolean isManifestClassAttribute( + @Nullable String tag, + @Nullable String uri, + @Nullable String name) { + if (name == null) { + return false; + } + + if ((name.equals(ATTR_NAME) + && (AndroidManifest.NODE_ACTIVITY.equals(tag) + || AndroidManifest.NODE_APPLICATION.equals(tag) + || AndroidManifest.NODE_INSTRUMENTATION.equals(tag) + || AndroidManifest.NODE_PROVIDER.equals(tag) + || AndroidManifest.NODE_SERVICE.equals(tag) + || AndroidManifest.NODE_RECEIVER.equals(tag))) + || name.equals(ATTRIBUTE_TARGET_ACTIVITY) + || name.equals(ATTRIBUTE_MANAGE_SPACE_ACTIVITY) + || name.equals(ATTRIBUTE_BACKUP_AGENT) + || name.equals(ATTRIBUTE_PARENT_ACTIVITY_NAME)) { + return ANDROID_URI.equals(uri); + } + + return false; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java new file mode 100644 index 0000000..6779fd3 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourcePage.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.R_CLASS; + +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.resources.ResourceType; + +import org.eclipse.jdt.internal.ui.refactoring.TextInputWizardPage; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +import java.util.Set; + +@SuppressWarnings("restriction") // JDT refactoring UI +class RenameResourcePage extends TextInputWizardPage implements SelectionListener { + private Label mXmlLabel; + private Label mJavaLabel; + private Button mUpdateReferences; + private boolean mCanClear; + private ResourceType mType; + private ResourceNameValidator mValidator; + + /** + * Create the wizard. + * @param type the type of the resource to be renamed + * @param initial initial renamed value + * @param canClear whether the dialog should allow clearing the field + */ + public RenameResourcePage(ResourceType type, String initial, boolean canClear) { + super(type.getName(), true, initial); + mType = type; + mCanClear = canClear; + + mValidator = ResourceNameValidator.create(false /*allowXmlExtension*/, + (Set<String>) null, mType); + } + + @SuppressWarnings("unused") // SWT constructors aren't really unused, they have side effects + @Override + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NULL); + setControl(container); + initializeDialogUnits(container); + container.setLayout(new GridLayout(2, false)); + Label nameLabel = new Label(container, SWT.NONE); + nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + nameLabel.setText("New Name:"); + Text text = super.createTextInputField(container); + text.selectAll(); + text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + Label xmlLabel = new Label(container, SWT.NONE); + xmlLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + xmlLabel.setText("XML:"); + mXmlLabel = new Label(container, SWT.NONE); + mXmlLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + Label javaLabel = new Label(container, SWT.NONE); + javaLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + javaLabel.setText("Java:"); + mJavaLabel = new Label(container, SWT.NONE); + mJavaLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); + new Label(container, SWT.NONE); + new Label(container, SWT.NONE); + mUpdateReferences = new Button(container, SWT.CHECK); + mUpdateReferences.setSelection(true); + mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); + mUpdateReferences.setText("Update References"); + mUpdateReferences.addSelectionListener(this); + + Dialog.applyDialogFont(container); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + RenameResourceProcessor processor = getProcessor(); + String newName = processor.getNewName(); + if (newName != null && newName.length() > 0 + && !newName.equals(getInitialValue())) { + Text textField = getTextField(); + textField.setText(newName); + textField.setSelection(0, newName.length()); + } + } + + super.setVisible(visible); + } + + @Override + protected RefactoringStatus validateTextField(String newName) { + if (newName.isEmpty() && isEmptyInputValid()) { + getProcessor().setNewName(""); + return RefactoringStatus.createWarningStatus( + "The resource definition will be deleted"); + } + + String error = mValidator.isValid(newName); + if (error != null) { + return RefactoringStatus.createErrorStatus(error); + } + + RenameResourceProcessor processor = getProcessor(); + processor.setNewName(newName); + return processor.checkNewName(newName); + } + + private RenameResourceProcessor getProcessor() { + RenameRefactoring refactoring = (RenameRefactoring) getRefactoring(); + return (RenameResourceProcessor) refactoring.getProcessor(); + } + + @Override + protected boolean isEmptyInputValid() { + return mCanClear; + } + + @Override + protected boolean isInitialInputValid() { + RenameResourceProcessor processor = getProcessor(); + return processor.getNewName() != null + && !processor.getNewName().equals(processor.getCurrentName()); + } + + @Override + protected void textModified(String text) { + super.textModified(text); + if (mXmlLabel != null && mJavaLabel != null) { + String xml = PREFIX_RESOURCE_REF + mType.getName() + '/' + text; + String java = R_CLASS + '.' + mType.getName() + '.' + text; + if (text.isEmpty()) { + xml = java = ""; + } + mXmlLabel.setText(xml); + mJavaLabel.setText(java); + } + } + + // ---- Implements SelectionListener ---- + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.getSource() == mUpdateReferences) { + RenameResourceProcessor processor = getProcessor(); + boolean update = mUpdateReferences.getSelection(); + processor.setUpdateReferences(update); + } + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java new file mode 100644 index 0000000..438e822 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceParticipant.java @@ -0,0 +1,752 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_ID; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_RES; +import static com.android.SdkConstants.FN_RESOURCE_CLASS; +import static com.android.SdkConstants.NEW_ID_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.R_CLASS; +import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TOOLS_URI; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameFieldProcessor; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.CompositeChange; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.TextChange; +import org.eclipse.ltk.core.refactoring.TextFileChange; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.RenameParticipant; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.ReplaceEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A rename participant handling renames of resources (such as R.id.foo and R.layout.bar). + * This reacts to refactorings of fields in the R inner classes (such as R.id), and updates + * the XML files as appropriate; renaming .xml files, updating XML attributes, resource + * references in style declarations, and so on. + */ +@SuppressWarnings("restriction") // WTP API +public class RenameResourceParticipant extends RenameParticipant { + /** The project we're refactoring in */ + private @NonNull IProject mProject; + + /** The type of the resource we're refactoring, such as {@link ResourceType#ID} */ + private @NonNull ResourceType mType; + /** + * The type of the resource folder we're refactoring in, such as + * {@link ResourceFolderType#VALUES}. When refactoring non value files, we need to + * rename the files as well. + */ + private @NonNull ResourceFolderType mFolderType; + + /** The previous name of the resource */ + private @NonNull String mOldName; + + /** The new name of the resource */ + private @NonNull String mNewName; + + /** Whether references to the resource should be updated */ + private boolean mUpdateReferences; + + /** A match pattern to look for in XML, such as {@code @attr/foo} */ + private @NonNull String mXmlMatch1; + + /** A match pattern to look for in XML, such as {@code ?attr/foo} */ + private @Nullable String mXmlMatch2; + + /** A match pattern to look for in XML, such as {@code ?foo} */ + private @Nullable String mXmlMatch3; + + /** The value to replace a reference to {@link #mXmlMatch1} with, such as {@code @attr/bar} */ + private @NonNull String mXmlNewValue1; + + /** The value to replace a reference to {@link #mXmlMatch2} with, such as {@code ?attr/bar} */ + private @Nullable String mXmlNewValue2; + + /** The value to replace a reference to {@link #mXmlMatch3} with, such as {@code ?bar} */ + private @Nullable String mXmlNewValue3; + + /** + * If non null, this refactoring was initiated as a file rename of an XML file (and if + * null, we are just reacting to a Java field rename) + */ + private IFile mRenamedFile; + + /** + * If renaming a field, we need to create an embedded field refactoring to update the + * Java sources referring to the corresponding R class field. This is stored as an + * instance such that we can have it participate in both the condition check methods + * as well as the {@link #createChange(IProgressMonitor)} refactoring operation. + */ + private RenameRefactoring mFieldRefactoring; + + /** + * Set while we are creating an embedded Java refactoring. This could cause a recursive + * invocation of the XML renaming refactoring to react to the field, so this is flag + * during the call to the Java processor, and is used to ignore requests for adding in + * field reactions during that time. + */ + private static boolean sIgnore; + + /** + * Creates a new {@linkplain RenameResourceParticipant} + */ + public RenameResourceParticipant() { + } + + @Override + public String getName() { + return "Android Rename Field Participant"; + } + + @Override + protected boolean initialize(Object element) { + if (sIgnore) { + return false; + } + + if (element instanceof IField) { + IField field = (IField) element; + IType declaringType = field.getDeclaringType(); + if (declaringType != null) { + if (R_CLASS.equals(declaringType.getParent().getElementName())) { + String typeName = declaringType.getElementName(); + mType = ResourceType.getEnum(typeName); + if (mType != null) { + mUpdateReferences = getArguments().getUpdateReferences(); + mFolderType = AdtUtils.getFolderTypeFor(mType); + IJavaProject javaProject = (IJavaProject) field.getAncestor( + IJavaElement.JAVA_PROJECT); + mProject = javaProject.getProject(); + mOldName = field.getElementName(); + mNewName = getArguments().getNewName(); + mFieldRefactoring = null; + mRenamedFile = null; + createXmlSearchPatterns(); + return true; + } + } + } + + return false; + } else if (element instanceof IFile) { + IFile file = (IFile) element; + mProject = file.getProject(); + if (BaseProjectHelper.isAndroidProject(mProject)) { + IPath path = file.getFullPath(); + int segments = path.segmentCount(); + if (segments == 4 && path.segment(1).equals(FD_RES)) { + String parentName = file.getParent().getName(); + mFolderType = ResourceFolderType.getFolderType(parentName); + if (mFolderType != null && mFolderType != ResourceFolderType.VALUES) { + mType = AdtUtils.getResourceTypeFor(mFolderType); + if (mType != null) { + mUpdateReferences = getArguments().getUpdateReferences(); + mProject = file.getProject(); + mOldName = AdtUtils.stripAllExtensions(file.getName()); + mNewName = AdtUtils.stripAllExtensions(getArguments().getNewName()); + mRenamedFile = file; + createXmlSearchPatterns(); + + mFieldRefactoring = null; + IField field = getResourceField(mProject, mType, mOldName); + if (field != null) { + mFieldRefactoring = createFieldRefactoring(field); + } else { + // no corresponding field; aapt has not run yet. Perhaps user has + // turned off auto build. + mFieldRefactoring = null; + } + + return true; + } + } + } + } + } else if (element instanceof String) { + String uri = (String) element; + if (uri.startsWith(PREFIX_RESOURCE_REF) && !uri.startsWith(ANDROID_PREFIX)) { + RenameResourceProcessor processor = (RenameResourceProcessor) getProcessor(); + mProject = processor.getProject(); + mType = processor.getType(); + mFolderType = AdtUtils.getFolderTypeFor(mType); + mOldName = processor.getCurrentName(); + mNewName = processor.getNewName(); + assert uri.endsWith(mOldName) && uri.contains(mType.getName()) : uri; + mUpdateReferences = getArguments().getUpdateReferences(); + if (mNewName.isEmpty()) { + mUpdateReferences = false; + } + mRenamedFile = null; + createXmlSearchPatterns(); + mFieldRefactoring = null; + if (!mNewName.isEmpty()) { + IField field = getResourceField(mProject, mType, mOldName); + if (field != null) { + mFieldRefactoring = createFieldRefactoring(field); + } + } + + return true; + } + } + + return false; + } + + /** Create nested Java refactoring which updates the R field references, if applicable */ + private RenameRefactoring createFieldRefactoring(IField field) { + return createFieldRefactoring(field, mNewName, mUpdateReferences); + } + + /** + * Create nested Java refactoring which updates the R field references, if + * applicable + * + * @param field the field to be refactored + * @param newName the new name + * @param updateReferences whether references should be updated + * @return a new rename refactoring + */ + public static RenameRefactoring createFieldRefactoring( + @NonNull IField field, + @NonNull String newName, + boolean updateReferences) { + RenameFieldProcessor processor = new RenameFieldProcessor(field); + processor.setRenameGetter(false); + processor.setRenameSetter(false); + RenameRefactoring refactoring = new RenameRefactoring(processor); + processor.setUpdateReferences(updateReferences); + processor.setUpdateTextualMatches(false); + processor.setNewElementName(newName); + try { + if (refactoring.isApplicable()) { + return refactoring; + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + private void createXmlSearchPatterns() { + // Set up search strings for the attribute iterator. This will + // identify string matches for mXmlMatch1, 2 and 3, and when matched, + // will add a replacement edit for mXmlNewValue1, 2, or 3. + mXmlMatch2 = null; + mXmlNewValue2 = null; + mXmlMatch3 = null; + mXmlNewValue3 = null; + + String typeName = mType.getName(); + if (mUpdateReferences) { + mXmlMatch1 = PREFIX_RESOURCE_REF + typeName + '/' + mOldName; + mXmlNewValue1 = PREFIX_RESOURCE_REF + typeName + '/' + mNewName; + if (mType == ResourceType.ID) { + mXmlMatch2 = NEW_ID_PREFIX + mOldName; + mXmlNewValue2 = NEW_ID_PREFIX + mNewName; + } else if (mType == ResourceType.ATTR) { + // When renaming @attr/foo, also edit ?attr/foo + mXmlMatch2 = PREFIX_THEME_REF + typeName + '/' + mOldName; + mXmlNewValue2 = PREFIX_THEME_REF + typeName + '/' + mNewName; + // as well as ?foo + mXmlMatch3 = PREFIX_THEME_REF + mOldName; + mXmlNewValue3 = PREFIX_THEME_REF + mNewName; + } + } else if (mType == ResourceType.ID) { + mXmlMatch1 = NEW_ID_PREFIX + mOldName; + mXmlNewValue1 = NEW_ID_PREFIX + mNewName; + } + } + + @Override + public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) + throws OperationCanceledException { + if (mRenamedFile != null && getArguments().getNewName().indexOf('.') == -1 + && mRenamedFile.getName().indexOf('.') != -1) { + return RefactoringStatus.createErrorStatus( + String.format("You must include the file extension (%1$s?)", + mRenamedFile.getName().substring(mRenamedFile.getName().indexOf('.')))); + } + + // Ensure that the new name is valid + if (mNewName != null && !mNewName.isEmpty()) { + ResourceNameValidator validator = ResourceNameValidator.create(false, mProject, mType); + String error = validator.isValid(mNewName); + if (error != null) { + return RefactoringStatus.createErrorStatus(error); + } + } + + if (mFieldRefactoring != null) { + try { + sIgnore = true; + return mFieldRefactoring.checkAllConditions(pm); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + sIgnore = false; + } + } + + return new RefactoringStatus(); + } + + @Override + public Change createChange(IProgressMonitor monitor) throws CoreException, + OperationCanceledException { + if (monitor.isCanceled()) { + return null; + } + + CompositeChange result = new CompositeChange("Update resource references"); + + // Only show the children in the refactoring preview dialog + result.markAsSynthetic(); + + addResourceFileChanges(result, mProject, monitor); + + // If renaming resources in a library project, also offer to rename references + // in including projects + if (mUpdateReferences) { + ProjectState projectState = Sdk.getProjectState(mProject); + if (projectState != null && projectState.isLibrary()) { + List<ProjectState> parentProjects = projectState.getParentProjects(); + for (ProjectState state : parentProjects) { + IProject project = state.getProject(); + CompositeChange nested = new CompositeChange( + String.format("Update references in %1$s", project.getName())); + addResourceFileChanges(nested, project, monitor); + if (nested.getChildren().length > 0) { + result.add(nested); + } + } + } + } + + if (mFieldRefactoring != null) { + // We have to add in Java field refactoring + try { + sIgnore = true; + addJavaChanges(result, monitor); + } finally { + sIgnore = false; + } + } else { + // Disable field refactoring added by the default Java field rename handler + disableExistingResourceFileChange(); + } + + return (result.getChildren().length == 0) ? null : result; + } + + /** + * Adds all changes to resource files (typically XML but also renaming drawable files + * + * @param project the Android project + * @param className the layout classes + */ + private void addResourceFileChanges( + CompositeChange change, + IProject project, + IProgressMonitor monitor) + throws OperationCanceledException { + if (monitor.isCanceled()) { + return; + } + + try { + // Update resource references in the manifest + IFile manifest = project.getFile(SdkConstants.ANDROID_MANIFEST_XML); + if (manifest != null) { + addResourceXmlChanges(manifest, change, null); + } + + // Update references in XML resource files + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + IResource[] folders = resFolder.members(); + for (IResource folder : folders) { + if (!(folder instanceof IFolder)) { + continue; + } + String folderName = folder.getName(); + ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); + IResource[] files = ((IFolder) folder).members(); + for (int i = 0; i < files.length; i++) { + IResource member = files[i]; + if ((member instanceof IFile) && member.exists()) { + IFile file = (IFile) member; + String fileName = member.getName(); + + if (SdkUtils.endsWith(fileName, DOT_XML)) { + addResourceXmlChanges(file, change, folderType); + } + + if ((mRenamedFile == null || !mRenamedFile.equals(file)) + && fileName.startsWith(mOldName) + && fileName.length() > mOldName.length() + && fileName.charAt(mOldName.length()) == '.' + && mFolderType != ResourceFolderType.VALUES + && mFolderType == folderType) { + // Rename this file + String newFile = mNewName + fileName.substring(mOldName.length()); + IPath path = file.getFullPath(); + change.add(new RenameResourceChange(path, newFile)); + } + } + } + } + } catch (CoreException e) { + RefactoringUtil.log(e); + } + } + + private void addJavaChanges(CompositeChange result, IProgressMonitor monitor) + throws CoreException, OperationCanceledException { + if (monitor.isCanceled()) { + return; + } + + RefactoringStatus status = mFieldRefactoring.checkAllConditions(monitor); + if (status != null && !status.hasError()) { + Change fieldChanges = mFieldRefactoring.createChange(monitor); + if (fieldChanges != null) { + result.add(fieldChanges); + + // Look for the field change on the R.java class; it's a derived file + // and will generate file modified manually warnings. Disable it. + disableRClassChanges(fieldChanges); + } + } + } + + private boolean addResourceXmlChanges( + IFile file, + CompositeChange changes, + ResourceFolderType folderType) { + IModelManager modelManager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = modelManager.getExistingModelForRead(file); + if (model == null) { + model = modelManager.getModelForRead(file); + } + if (model != null) { + IStructuredDocument document = model.getStructuredDocument(); + if (model instanceof IDOMModel) { + IDOMModel domModel = (IDOMModel) model; + Element root = domModel.getDocument().getDocumentElement(); + if (root != null) { + List<TextEdit> edits = new ArrayList<TextEdit>(); + addReplacements(edits, root, document, folderType); + if (!edits.isEmpty()) { + MultiTextEdit rootEdit = new MultiTextEdit(); + rootEdit.addChildren(edits.toArray(new TextEdit[edits.size()])); + TextFileChange change = new TextFileChange(file.getName(), file); + change.setTextType(EXT_XML); + change.setEdit(rootEdit); + changes.add(change); + } + } + } else { + return false; + } + } + + return true; + } catch (IOException e) { + AdtPlugin.log(e, null); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } finally { + if (model != null) { + model.releaseFromRead(); + } + } + + return false; + } + + private void addReplacements( + @NonNull List<TextEdit> edits, + @NonNull Element element, + @NonNull IStructuredDocument document, + @Nullable ResourceFolderType folderType) { + String tag = element.getTagName(); + if (folderType == ResourceFolderType.VALUES) { + // Look for + // <item name="main_layout" type="layout">...</item> + // <item name="myid" type="id"/> + // <string name="mystring">...</string> + // etc + if (tag.equals(mType.getName()) + || (tag.equals(TAG_ITEM) + && (mType == ResourceType.ID + || mType.getName().equals(element.getAttribute(ATTR_TYPE))))) { + Attr nameNode = element.getAttributeNode(ATTR_NAME); + if (nameNode != null && nameNode.getValue().equals(mOldName)) { + int start = RefactoringUtil.getAttributeValueRangeStart(nameNode, document); + if (start != -1) { + int end = start + mOldName.length(); + edits.add(new ReplaceEdit(start, end - start, mNewName)); + } + } + } + } + + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attr = (Attr) attributes.item(i); + String value = attr.getValue(); + + // If not updating references, only update XML matches that define the id + if (!mUpdateReferences && (!ATTR_ID.equals(attr.getLocalName()) || + !ANDROID_URI.equals(attr.getNamespaceURI()))) { + + if (TOOLS_URI.equals(attr.getNamespaceURI()) && value.equals(mXmlMatch1)) { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + mXmlMatch1.length(); + edits.add(new ReplaceEdit(start, end - start, mXmlNewValue1)); + } + } + + continue; + } + + // Replace XML attribute reference, such as + // android:id="@+id/oldName" => android:id="+id/newName" + + String match = null; + String matchedValue = null; + + if (value.equals(mXmlMatch1)) { + match = mXmlMatch1; + matchedValue = mXmlNewValue1; + } else if (value.equals(mXmlMatch2)) { + match = mXmlMatch2; + matchedValue = mXmlNewValue2; + } else if (value.equals(mXmlMatch3)) { + match = mXmlMatch3; + matchedValue = mXmlNewValue3; + } else { + continue; + } + + if (match != null) { + if (mNewName.isEmpty() && ATTR_ID.equals(attr.getLocalName()) && + ANDROID_URI.equals(attr.getNamespaceURI())) { + // Delete attribute + IndexedRegion region = (IndexedRegion) attr; + int start = region.getStartOffset(); + int end = region.getEndOffset(); + edits.add(new ReplaceEdit(start, end - start, "")); + } else { + int start = RefactoringUtil.getAttributeValueRangeStart(attr, document); + if (start != -1) { + int end = start + match.length(); + edits.add(new ReplaceEdit(start, end - start, matchedValue)); + } + } + } + } + + NodeList children = element.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + addReplacements(edits, (Element) child, document, folderType); + } else if (child.getNodeType() == Node.TEXT_NODE && mUpdateReferences) { + // Replace XML text, such as @color/custom_theme_color in + // <item name="android:windowBackground">@color/custom_theme_color</item> + // + String text = child.getNodeValue(); + int index = getFirstNonBlankIndex(text); + if (index != -1) { + String match = null; + String matchedValue = null; + if (mXmlMatch1 != null + && text.startsWith(mXmlMatch1) && text.trim().equals(mXmlMatch1)) { + match = mXmlMatch1; + matchedValue = mXmlNewValue1; + } else if (mXmlMatch2 != null + && text.startsWith(mXmlMatch2) && text.trim().equals(mXmlMatch2)) { + match = mXmlMatch2; + matchedValue = mXmlNewValue2; + } else if (mXmlMatch3 != null + && text.startsWith(mXmlMatch3) && text.trim().equals(mXmlMatch3)) { + match = mXmlMatch3; + matchedValue = mXmlNewValue3; + } + if (match != null) { + IndexedRegion region = (IndexedRegion) child; + int start = region.getStartOffset() + index; + int end = start + match.length(); + edits.add(new ReplaceEdit(start, end - start, matchedValue)); + } + } + } + } + } + + /** + * Returns the index of the first non-space character in the string, or -1 + * if the string is empty or has only whitespace + * + * @param s the string to check + * @return the index of the first non whitespace character + */ + private int getFirstNonBlankIndex(String s) { + for (int i = 0, n = s.length(); i < n; i++) { + if (!Character.isWhitespace(s.charAt(i))) { + return i; + } + } + + return -1; + } + + /** + * Initiates a renaming of a resource item + * + * @param project the project containing the resource references + * @param type the type of resource + * @param name the name of the resource + * @return false if initiating the rename failed + */ + @Nullable + private static IField getResourceField( + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String name) { + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + if (javaProject == null) { + return null; + } + + String pkg = ManifestInfo.get(project).getPackage(); + // TODO: Rename in all libraries too? + IType t = javaProject.findType(pkg + '.' + R_CLASS + '.' + type.getName()); + if (t == null) { + return null; + } + + return t.getField(name); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + /** + * Searches for existing changes in the refactoring which modifies the R + * field to rename it. it's derived so performing this change will generate + * a "generated code was modified manually" warning + */ + private void disableExistingResourceFileChange() { + IFolder genFolder = mProject.getFolder(SdkConstants.FD_GEN_SOURCES); + if (genFolder != null && genFolder.exists()) { + ManifestInfo manifestInfo = ManifestInfo.get(mProject); + String pkg = manifestInfo.getPackage(); + if (pkg != null) { + IFile rFile = genFolder.getFile(pkg.replace('.', '/') + '/' + FN_RESOURCE_CLASS); + TextChange change = getTextChange(rFile); + if (change != null) { + change.setEnabled(false); + } + } + } + } + + /** + * Searches for existing changes in the refactoring which modifies the R + * field to rename it. it's derived so performing this change will generate + * a "generated code was modified manually" warning + * + * @param change the change to disable R file changes in + */ + public static void disableRClassChanges(Change change) { + if (change.getName().equals(FN_RESOURCE_CLASS)) { + change.setEnabled(false); + } + // Look for the field change on the R.java class; it's a derived file + // and will generate file modified manually warnings. Disable it. + if (change instanceof CompositeChange) { + for (Change outer : ((CompositeChange) change).getChildren()) { + disableRClassChanges(outer); + } + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java new file mode 100644 index 0000000..5ea9941 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceProcessor.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtConstants; +import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.resources.ResourceType; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.ltk.core.refactoring.Change; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext; +import org.eclipse.ltk.core.refactoring.participants.ParticipantManager; +import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant; +import org.eclipse.ltk.core.refactoring.participants.RenameArguments; +import org.eclipse.ltk.core.refactoring.participants.RenameProcessor; +import org.eclipse.ltk.core.refactoring.participants.SharableParticipants; + +/** + * A rename processor for Android resources. + */ +public class RenameResourceProcessor extends RenameProcessor { + private IProject mProject; + private ResourceType mType; + private String mCurrentName; + private String mNewName; + private boolean mUpdateReferences = true; + private ResourceNameValidator mValidator; + private RenameArguments mRenameArguments; + + /** + * Creates a new rename resource processor. + * + * @param project the project containing the renamed resource + * @param type the type of the resource + * @param currentName the current name of the resource + * @param newName the new name of the resource, or null if not known + */ + public RenameResourceProcessor( + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String currentName, + @Nullable String newName) { + mProject = project; + mType = type; + mCurrentName = currentName; + mNewName = newName != null ? newName : currentName; + mUpdateReferences= true; + mValidator = ResourceNameValidator.create(false, mProject, mType); + } + + /** + * Returns the project containing the renamed resource + * + * @return the project containing the renamed resource + */ + @NonNull + public IProject getProject() { + return mProject; + } + + /** + * Returns the new resource name + * + * @return the new resource name + */ + @NonNull + public String getNewName() { + return mNewName; + } + + /** + * Returns the current name of the resource + * + * @return the current name of the resource + */ + public String getCurrentName() { + return mCurrentName; + } + + /** + * Returns the type of the resource + * + * @return the type of the resource + */ + @NonNull + public ResourceType getType() { + return mType; + } + + /** + * Sets the new name + * + * @param newName the new name + */ + public void setNewName(@NonNull String newName) { + mNewName = newName; + } + + /** + * Returns {@code true} if the refactoring processor also updates references + * + * @return {@code true} if the refactoring processor also updates references + */ + public boolean isUpdateReferences() { + return mUpdateReferences; + } + + /** + * Specifies if the refactoring processor also updates references. The + * default behavior is to update references. + * + * @param updateReferences {@code true} if the refactoring processor should + * also updates references + */ + public void setUpdateReferences(boolean updateReferences) { + mUpdateReferences = updateReferences; + } + + /** + * Checks the given new potential name and returns a {@link RefactoringStatus} indicating + * whether the potential new name is valid + * + * @param name the name to check + * @return a {@link RefactoringStatus} with the validation result + */ + public RefactoringStatus checkNewName(String name) { + String error = mValidator.isValid(name); + if (error != null) { + return RefactoringStatus.createFatalErrorStatus(error); + } + + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException { + return new RefactoringStatus(); + } + + @Override + public RefactoringStatus checkFinalConditions(IProgressMonitor pm, + CheckConditionsContext context) throws CoreException { + pm.beginTask("", 1); + try { + mRenameArguments = new RenameArguments(getNewName(), isUpdateReferences()); + return new RefactoringStatus(); + } finally { + pm.done(); + } + } + + @Override + public Change createChange(IProgressMonitor pm) throws CoreException { + pm.beginTask("", 1); + try { + // Added by {@link RenameResourceParticipant} + return null; + } finally { + pm.done(); + } + } + + @Override + public Object[] getElements() { + return new Object[0]; + } + + @Override + public String getIdentifier() { + return "com.android.ide.renameResourceProcessor"; //$NON-NLS-1$ + } + + @Override + public String getProcessorName() { + return "Rename Android Resource"; + } + + @Override + public boolean isApplicable() { + return true; + } + + @Override + public RefactoringParticipant[] loadParticipants(RefactoringStatus status, + SharableParticipants shared) throws CoreException { + String[] affectedNatures = new String[] { AdtConstants.NATURE_DEFAULT }; + String url = PREFIX_RESOURCE_REF + mType.getName() + '/' + mCurrentName; + return ParticipantManager.loadRenameParticipants(status, this, url, mRenameArguments, + null, affectedNatures, shared); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java new file mode 100644 index 0000000..6ffe25d --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceWizard.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.resources.ResourceType; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.internal.ui.IJavaHelpContextIds; +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameRefactoringWizard; +import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.ltk.core.refactoring.RefactoringStatus; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.widgets.Shell; + +/** + * Rename refactoring wizard for Android resources such as {@code @id/foo} + */ +@SuppressWarnings("restriction") // JDT refactoring UI +public class RenameResourceWizard extends RenameRefactoringWizard { + private ResourceType mType; + private boolean mCanClear; + + /** + * Constructs a new {@linkplain RenameResourceWizard} + * + * @param refactoring the refactoring + * @param type the type of resource being renamed + * @param canClear whether the user can clear the value + */ + public RenameResourceWizard( + @NonNull RenameRefactoring refactoring, + @NonNull ResourceType type, + boolean canClear) { + super(refactoring, + "Rename Resource", + "Enter the new name for this resource", + JavaPluginImages.DESC_WIZBAN_REFACTOR_FIELD, + IJavaHelpContextIds.RENAME_FIELD_WIZARD_PAGE); + mType = type; + mCanClear = canClear; + } + + @Override + protected void addUserInputPages() { + RenameRefactoring refactoring = (RenameRefactoring) getRefactoring(); + RenameResourceProcessor processor = (RenameResourceProcessor) refactoring.getProcessor(); + String name = processor.getNewName(); + addPage(new RenameResourcePage(mType, name, mCanClear)); + } + + /** + * Initiates a renaming of a resource item + * + * @param shell the shell to parent the dialog to + * @param project the project containing the resource references + * @param type the type of resource + * @param currentName the name of the resource + * @param newName the new name, or null if not known + * @param canClear whether the name is allowed to be cleared + * @return false if initiating the rename failed + */ + public static RenameResult renameResource( + @NonNull Shell shell, + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull String currentName, + @Nullable String newName, + boolean canClear) { + try { + RenameResourceProcessor processor = new RenameResourceProcessor(project, type, + currentName, newName); + RenameRefactoring refactoring = new RenameRefactoring(processor); + if (!refactoring.isApplicable()) { + return RenameResult.unavailable(); + } + + if (!show(refactoring, processor, shell, type, canClear)) { + return RenameResult.canceled(); + } + return RenameResult.name(processor.getNewName()); + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return RenameResult.unavailable(); + } + + /** + * Show a refactoring dialog for the given resource refactoring operation + * + * @param refactoring the rename refactoring + * @param processor the field processor + * @param parent the parent shell + * @param type the resource type + * @param canClear whether the user is allowed to clear/reset the name to + * nothing + * @return true if the refactoring was performed, and false if it was + * canceled + * @throws CoreException if an unexpected error occurs + */ + private static boolean show( + @NonNull RenameRefactoring refactoring, + @NonNull RenameResourceProcessor processor, + @NonNull Shell parent, + @NonNull ResourceType type, + boolean canClear) throws CoreException { + RefactoringSaveHelper saveHelper = new RefactoringSaveHelper( + RefactoringSaveHelper.SAVE_REFACTORING); + if (!saveHelper.saveEditors(parent)) { + return false; + } + + try { + RenameResourceWizard wizard = new RenameResourceWizard(refactoring, type, canClear); + RefactoringWizardOpenOperation operation = new RefactoringWizardOpenOperation(wizard); + String dialogTitle = wizard.getDefaultPageTitle(); + int result = operation.run(parent, dialogTitle == null ? "" : dialogTitle); + RefactoringStatus status = operation.getInitialConditionCheckingStatus(); + if (status.hasFatalError()) { + return false; + } + if (result == RefactoringWizardOpenOperation.INITIAL_CONDITION_CHECKING_FAILED + || result == IDialogConstants.CANCEL_ID) { + saveHelper.triggerIncrementalBuild(); + return false; + } + + // Save modified resources; need to trigger R file regeneration + saveHelper.saveEditors(parent); + + return true; + } catch (InterruptedException e) { + return false; // Canceled + } + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java new file mode 100644 index 0000000..3b1fa52 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResourceXmlTextAction.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import static com.android.SdkConstants.ANDROID_MANIFEST_XML; +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_THEME_PREFIX; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.TAG_ITEM; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.resources.ResourceRepository; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.resources.ResourceType; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.internal.corext.refactoring.rename.RenameTypeProcessor; +import org.eclipse.jdt.internal.ui.refactoring.reorg.RenameTypeWizard; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.ltk.core.refactoring.participants.RenameRefactoring; +import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.ui.texteditor.ITextEditorExtension; +import org.eclipse.ui.texteditor.ITextEditorExtension2; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.List; + +/** + * Text action for XML files to invoke renaming + * <p> + * TODO: Handle other types of renaming: invoking class renaming when editing + * class names in layout files and manifest files, renaming attribute names when + * editing a styleable attribute, etc. + */ +@SuppressWarnings("restriction") // Java rename refactoring +public final class RenameResourceXmlTextAction extends Action { + private final ITextEditor mEditor; + + /** + * Creates a new {@linkplain RenameResourceXmlTextAction} + * + * @param editor the associated editor + */ + public RenameResourceXmlTextAction(@NonNull ITextEditor editor) { + super("Rename"); + mEditor = editor; + } + + @Override + public void run() { + if (!validateEditorInputState()) { + return; + } + IFile file = getFile(); + if (file == null) { + return; + } + IProject project = file.getProject(); + if (project == null) { + return; + } + IDocument document = getDocument(); + if (document == null) { + return; + } + ITextSelection selection = getSelection(); + if (selection == null) { + return; + } + + Pair<ResourceType, String> resource = findResource(document, selection.getOffset()); + + if (resource == null) { + resource = findItemDefinition(document, selection.getOffset()); + } + + if (resource != null) { + ResourceType type = resource.getFirst(); + String name = resource.getSecond(); + Shell shell = mEditor.getSite().getShell(); + boolean canClear = false; + + RenameResourceWizard.renameResource(shell, project, type, name, null, canClear); + return; + } + + String className = findClassName(document, file, selection.getOffset()); + if (className != null) { + assert className.equals(className.trim()); + IType type = findType(className, project); + if (type != null) { + RenameTypeProcessor processor = new RenameTypeProcessor(type); + //processor.setNewElementName(className); + processor.setUpdateQualifiedNames(true); + processor.setUpdateSimilarDeclarations(false); + //processor.setMatchStrategy(?); + //processor.setFilePatterns(patterns); + processor.setUpdateReferences(true); + + RenameRefactoring refactoring = new RenameRefactoring(processor); + RenameTypeWizard wizard = new RenameTypeWizard(refactoring); + RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); + try { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + op.run(window.getShell(), wizard.getDefaultPageTitle()); + } catch (InterruptedException e) { + } + } + + return; + } + + // Fallback: tell user the cursor isn't in the right place + MessageDialog.openInformation(mEditor.getSite().getShell(), + "Rename", + "Operation unavailable on the current selection.\n" + + "Select an Android resource name or class."); + } + + private boolean validateEditorInputState() { + if (mEditor instanceof ITextEditorExtension2) + return ((ITextEditorExtension2) mEditor).validateEditorInputState(); + else if (mEditor instanceof ITextEditorExtension) + return !((ITextEditorExtension) mEditor).isEditorInputReadOnly(); + else if (mEditor != null) + return mEditor.isEditable(); + else + return false; + } + + /** + * Searches for a resource URL around the caret, such as {@code @string/foo} + * + * @param document the document to search in + * @param offset the offset to search at + * @return a resource pair, or null if not found + */ + @Nullable + public static Pair<ResourceType,String> findResource(@NonNull IDocument document, int offset) { + try { + int max = document.getLength(); + if (offset >= max) { + offset = max - 1; + } else if (offset < 0) { + offset = 0; + } else if (offset > 0) { + // If the caret is right after a resource name (meaning getChar(offset) points + // to the following character), back up + char c = document.getChar(offset); + if (!isValidResourceNameChar(c)) { + offset--; + } + } + + int start = offset; + boolean valid = true; + for (; start >= 0; start--) { + char c = document.getChar(start); + if (c == '@' || c == '?') { + break; + } else if (!isValidResourceNameChar(c)) { + valid = false; + break; + } + } + if (valid) { + // Search forwards for the end + int end = start + 1; + for (; end < max; end++) { + char c = document.getChar(end); + if (!isValidResourceNameChar(c)) { + break; + } + } + if (end > start + 1) { + String url = document.get(start, end - start); + + // Don't allow renaming framework resources -- @android:string/ok etc + if (url.startsWith(ANDROID_PREFIX) || url.startsWith(ANDROID_THEME_PREFIX)) { + return null; + } + + return ResourceRepository.parseResource(url); + } + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + private static boolean isValidResourceNameChar(char c) { + return c == '@' || c == '?' || c == '/' || c == '+' || Character.isJavaIdentifierPart(c); + } + + /** + * Searches for an item definition around the caret, such as + * {@code <string name="foo">My String</string>} + */ + private Pair<ResourceType, String> findItemDefinition(IDocument document, int offset) { + Node node = DomUtilities.getNode(document, offset); + if (node == null) { + return null; + } + if (node.getNodeType() == Node.TEXT_NODE) { + node = node.getParentNode(); + } + if (node == null || node.getNodeType() != Node.ELEMENT_NODE) { + return null; + } + + Element element = (Element) node; + String name = element.getAttribute(ATTR_NAME); + if (name == null || name.isEmpty()) { + return null; + } + String typeString = element.getTagName(); + if (TAG_ITEM.equals(typeString)) { + typeString = element.getAttribute(ATTR_TYPE); + if (typeString == null || typeString.isEmpty()) { + return null; + } + } + ResourceType type = ResourceType.getEnum(typeString); + if (type != null) { + return Pair.of(type, name); + } + + return null; + } + + /** + * Searches for a fully qualified class name around the caret, such as {@code foo.bar.MyClass} + * + * @param document the document to search in + * @param file the file, if known + * @param offset the offset to search at + * @return a resource pair, or null if not found + */ + @Nullable + public static String findClassName( + @NonNull IDocument document, + @Nullable IFile file, + int offset) { + try { + int max = document.getLength(); + if (offset >= max) { + offset = max - 1; + } else if (offset < 0) { + offset = 0; + } else if (offset > 0) { + // If the caret is right after a resource name (meaning getChar(offset) points + // to the following character), back up + char c = document.getChar(offset); + if (Character.isJavaIdentifierPart(c)) { + offset--; + } + } + + int start = offset; + for (; start >= 0; start--) { + char c = document.getChar(start); + if (c == '"' || c == '<' || c == '/') { + start++; + break; + } else if (c != '.' && !Character.isJavaIdentifierPart(c)) { + return null; + } + } + // Search forwards for the end + int end = start + 1; + for (; end < max; end++) { + char c = document.getChar(end); + if (c != '.' && !Character.isJavaIdentifierPart(c)) { + if (c != '"' && c != '>' && !Character.isWhitespace(c)) { + return null; + } + break; + } + } + if (end > start + 1) { + String fqcn = document.get(start, end - start); + int dot = fqcn.indexOf('.'); + if (dot == -1) { // Only support fully qualified names + return null; + } + if (dot == 0) { // Special case for manifests: prepend package + if (file != null && file.getName().equals(ANDROID_MANIFEST_XML)) { + ManifestInfo info = ManifestInfo.get(file.getProject()); + return info.getPackage() + fqcn; + } + return null; + } + + return fqcn; + } + } catch (BadLocationException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + @Nullable + private IType findType(@NonNull String className, @NonNull IProject project) { + IType type = null; + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + type = javaProject.findType(className); + if (type == null || !type.exists()) { + return null; + } + if (!type.isBinary()) { + return type; + } + // See if this class is coming through a library project jar file and + // if so locate the real class + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null) { + List<IProject> libraries = projectState.getFullLibraryProjects(); + for (IProject library : libraries) { + javaProject = BaseProjectHelper.getJavaProject(library); + type = javaProject.findType(className); + if (type != null && type.exists() && !type.isBinary()) { + return type; + } + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + + return null; + } + + private ITextSelection getSelection() { + ISelectionProvider selectionProvider = mEditor.getSelectionProvider(); + if (selectionProvider == null) { + return null; + } + ISelection selection = selectionProvider.getSelection(); + if (!(selection instanceof ITextSelection)) { + return null; + } + return (ITextSelection) selection; + } + + private IDocument getDocument() { + IDocumentProvider documentProvider = mEditor.getDocumentProvider(); + if (documentProvider == null) { + return null; + } + IDocument document = documentProvider.getDocument(mEditor.getEditorInput()); + if (document == null) { + return null; + } + return document; + } + + @Nullable + private IFile getFile() { + IEditorInput input = mEditor.getEditorInput(); + if (input instanceof IFileEditorInput) { + IFileEditorInput fileInput = (IFileEditorInput) input; + return fileInput.getFile(); + } + + return null; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java new file mode 100644 index 0000000..ade346f --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/core/RenameResult.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012 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.refactorings.core; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +/** + * A result from a renaming operation + */ +public class RenameResult { + private boolean mCanceled; + private boolean mUnavailable; + private @Nullable String mName; + private boolean mClear; + + /** + * Constructs a new rename result + */ + private RenameResult() { + } + + /** + * Creates a new blank {@linkplain RenameResult} + * @return a new result + */ + @NonNull + public static RenameResult create() { + return new RenameResult(); + } + + /** + * Creates a new {@linkplain RenameResult} for a user canceled renaming operation + * @return a canceled operation + */ + @NonNull + public static RenameResult canceled() { + return new RenameResult().setCanceled(true); + } + + /** + * Creates a {@linkplain RenameResult} for a renaming operation that was + * not available (for example because the field attempted to be renamed + * does not yet exist (or does not exist any more) + * + * @return a new result + */ + @NonNull + public static RenameResult unavailable() { + return new RenameResult().setUnavailable(true); + } + + /** + * Creates a new {@linkplain RenameResult} for a successful renaming + * operation to the given name + * + * @param name the new name + * @return a new result + */ + @NonNull + public static RenameResult name(@Nullable String name) { + return new RenameResult().setName(name); + } + + /** + * Marks this result as canceled + * + * @param canceled whether the result was canceled + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setCanceled(boolean canceled) { + mCanceled = canceled; + return this; + } + + /** + * Marks this result as unavailable + * + * @param unavailable whether this result was unavailable + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setUnavailable(boolean unavailable) { + mUnavailable = unavailable; + return this; + } + + /** + * Sets the new name of the renaming operation + * + * @param name the new name + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setName(@Nullable String name) { + mName = name; + return this; + } + + /** + * Marks this result as clearing the name (reverting it back to the default) + * + * @param clear whether the name was cleared + * @return this, for constructor chaining + */ + @NonNull + public RenameResult setCleared(boolean clear) { + mClear = clear; + return this; + } + + /** + * Returns whether this result represents a canceled renaming operation + * + * @return true if the operation was canceled + */ + public boolean isCanceled() { + return mCanceled; + } + + /** + * Returns whether this result represents an unavailable renaming operation + * + * @return true if the operation was not available + */ + public boolean isUnavailable() { + return mUnavailable; + } + + /** + * Returns whether this result represents a renaming back to the default (possibly + * clear) name. In this case, {@link #getName()} will return {@code null}. + * + * @return true if the name should be reset + */ + public boolean isCleared() { + return mClear; + } + + /** + * Returns the new name. + * + * @return the new name + */ + @Nullable + public String getName() { + return mName; + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java index 14fd506..61bd06e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringContribution.java @@ -4,7 +4,7 @@ * 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 @@ -25,11 +25,11 @@ import java.util.Map; * @see ExtractStringDescriptor */ public class ExtractStringContribution extends RefactoringContribution { - + /* (non-Javadoc) * @see org.eclipse.ltk.core.refactoring.RefactoringContribution#createDescriptor(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, int) */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) @Override public RefactoringDescriptor createDescriptor( String id, @@ -42,7 +42,7 @@ public class ExtractStringContribution extends RefactoringContribution { return new ExtractStringDescriptor(project, description, comment, arguments); } - @SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") @Override public Map retrieveArgumentMap(RefactoringDescriptor descriptor) { if (descriptor instanceof ExtractStringDescriptor) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java index e9d386e..7d0f926 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ExtractStringRefactoring.java @@ -16,12 +16,11 @@ package com.android.ide.eclipse.adt.internal.refactorings.extractstring; -import static com.android.SdkConstants.AMP_ENTITY; -import static com.android.SdkConstants.LT_ENTITY; import static com.android.SdkConstants.QUOT_ENTITY; import static com.android.SdkConstants.STRING_PREFIX; import com.android.SdkConstants; +import com.android.ide.common.resources.ValueResourceParser; import com.android.ide.common.xml.ManifestData; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; @@ -1218,7 +1217,7 @@ public class ExtractStringRefactoring extends Refactoring { IStructuredModel smodel = null; // Single and double quotes must be escaped in the <string>value</string> declaration - tokenString = escapeString(tokenString); + tokenString = ValueResourceParser.escapeResourceString(tokenString); try { IStructuredDocument sdoc = null; @@ -1450,79 +1449,6 @@ public class ExtractStringRefactoring extends Refactoring { } /** - * Escape a string value to be placed in a string resource file such that it complies with - * the escaping rules described here: - * http://developer.android.com/guide/topics/resources/string-resource.html - * More examples of the escaping rules can be found here: - * http://androidcookbook.com/Recipe.seam?recipeId=2219&recipeFrom=ViewTOC - * This method assumes that the String is not escaped already. - * - * Rules: - * <ul> - * <li>Double quotes are needed if string starts or ends with at least one space. - * <li>{@code @, ?} at beginning of string have to be escaped with a backslash. - * <li>{@code ', ", \} have to be escaped with a backslash. - * <li>{@code <, >, &} have to be replaced by their predefined xml entity. - * <li>{@code \n, \t} have to be replaced by a backslash and the appropriate character. - * </ul> - * @param s the string to be escaped - * @return the escaped string as it would appear in the XML text in a values file - */ - public static String escapeString(String s) { - int n = s.length(); - if (n == 0) { - return ""; - } - - StringBuilder sb = new StringBuilder(s.length() * 2); - boolean hasSpace = s.charAt(0) == ' ' || s.charAt(n - 1) == ' '; - - if (hasSpace) { - sb.append('"'); - } else if (s.charAt(0) == '@' || s.charAt(0) == '?') { - sb.append('\\'); - } - - for (int i = 0; i < n; ++i) { - char c = s.charAt(i); - switch (c) { - case '\'': - if (!hasSpace) { - sb.append('\\'); - } - sb.append(c); - break; - case '"': - case '\\': - sb.append('\\'); - sb.append(c); - break; - case '<': - sb.append(LT_ENTITY); - break; - case '&': - sb.append(AMP_ENTITY); - break; - case '\n': - sb.append("\\n"); //$NON-NLS-1$ - break; - case '\t': - sb.append("\\t"); //$NON-NLS-1$ - break; - default: - sb.append(c); - break; - } - } - - if (hasSpace) { - sb.append('"'); - } - - return sb.toString(); - } - - /** * Computes the changes to be made to the source Android XML file and * returns a list of {@link Change}. * <p/> diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java index 2f1185c..e058ce1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/extractstring/ReplaceStringsVisitor.java @@ -191,7 +191,7 @@ class ReplaceStringsVisitor extends ASTVisitor { * * This covers the case of Activity.setTitle(int resId) vs setTitle(String str). */ - @SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") private boolean examineMethodInvocation(StringLiteral node) { ASTNode parent = null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java index 7005d82..406cebc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/ApplicationPackageNameRefactoring.java @@ -16,6 +16,10 @@ package com.android.ide.eclipse.adt.internal.refactorings.renamepackage; +import static com.android.SdkConstants.FN_BUILD_CONFIG_BASE; +import static com.android.SdkConstants.FN_MANIFEST_BASE; +import static com.android.SdkConstants.FN_RESOURCE_BASE; + import com.android.SdkConstants; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; @@ -74,7 +78,6 @@ import java.util.List; */ @SuppressWarnings("restriction") class ApplicationPackageNameRefactoring extends Refactoring { - private final IProject mProject; private final Name mOldPackageName; private final Name mNewPackageName; @@ -100,8 +103,8 @@ class ApplicationPackageNameRefactoring extends Refactoring { IMarker.PROBLEM, true, IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) { - return RefactoringStatus - .createFatalErrorStatus("Fix the errors in your project, first."); + return + RefactoringStatus.createFatalErrorStatus("Fix the errors in your project, first."); } return new RefactoringStatus(); @@ -143,21 +146,36 @@ class ApplicationPackageNameRefactoring extends Refactoring { TextEdit rewrittenImports = importVisitor.getTextEdit(); // If the import of R was potentially implicit, insert an import statement - if (cu.getPackage().getName().getFullyQualifiedName() + if (rewrittenImports != null && cu.getPackage().getName().getFullyQualifiedName() .equals(mOldPackageName.getFullyQualifiedName())) { - ImportRewrite irw = ImportRewrite.create(cu, true); - irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' - + SdkConstants.FN_RESOURCE_BASE); + UsageVisitor usageVisitor = new UsageVisitor(); + cu.accept(usageVisitor); - try { - rewrittenImports.addChild( irw.rewriteImports(null) ); - } catch (MalformedTreeException e) { - Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); - AdtPlugin.getDefault().getLog().log(s); - } catch (CoreException e) { - Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); - AdtPlugin.getDefault().getLog().log(s); + if (usageVisitor.seenAny()) { + ImportRewrite irw = ImportRewrite.create(cu, true); + if (usageVisitor.hasSeenR()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_RESOURCE_BASE); + } + if (usageVisitor.hasSeenBuildConfig()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_BUILD_CONFIG_BASE); + } + if (usageVisitor.hasSeenManifest()) { + irw.addImport(mNewPackageName.getFullyQualifiedName() + '.' + + FN_MANIFEST_BASE); + } + + try { + rewrittenImports.addChild( irw.rewriteImports(null) ); + } catch (MalformedTreeException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } catch (CoreException e) { + Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); + AdtPlugin.getDefault().getLog().log(s); + } } } @@ -405,11 +423,11 @@ class ApplicationPackageNameRefactoring extends Refactoring { Collections.reverse(mChanges); CompositeChange change = new CompositeChange("Refactoring Application package name", mChanges.toArray(new Change[mChanges.size()])); + change.markAsSynthetic(); return change; } @Override - @SuppressWarnings("unused") public boolean visit(IResource resource) throws CoreException { if (resource instanceof IFile) { IFile file = (IFile) resource; @@ -420,10 +438,10 @@ class ApplicationPackageNameRefactoring extends Refactoring { mParser.setSource(icu); CompilationUnit cu = (CompilationUnit) mParser.createAST(null); - TextEdit text_edit = updateJavaFileImports(cu); - if (text_edit.hasChildren()) { + TextEdit textEdit = updateJavaFileImports(cu); + if (textEdit != null && textEdit.hasChildren()) { MultiTextEdit edit = new MultiTextEdit(); - edit.addChild(text_edit); + edit.addChild(textEdit); TextFileChange text_file_change = new TextFileChange(file.getName(), file); text_file_change.setTextType(SdkConstants.EXT_JAVA); @@ -437,10 +455,13 @@ class ApplicationPackageNameRefactoring extends Refactoring { } else if (SdkConstants.EXT_XML.equals(file.getFileExtension())) { if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) { - - TextFileChange manifest_change = editAndroidManifest(file); - mChanges.add(manifest_change); - + // Ensure that this is the root manifest, not some other copy + // (such as the one in bin/) + IPath path = file.getFullPath(); + if (path.segmentCount() == 2) { + TextFileChange manifest_change = editAndroidManifest(file); + mChanges.add(manifest_change); + } } else { // Currently we only support Android resource XML files, @@ -475,7 +496,43 @@ class ApplicationPackageNameRefactoring extends Refactoring { } } - class ImportVisitor extends ASTVisitor { + private static class UsageVisitor extends ASTVisitor { + private boolean mSeenManifest; + private boolean mSeenR; + private boolean mSeenBuildConfig; + + @Override + public boolean visit(QualifiedName node) { + Name qualifier = node.getQualifier(); + if (qualifier.isSimpleName()) { + String name = qualifier.toString(); + if (name.equals(FN_RESOURCE_BASE)) { + mSeenR = true; + } else if (name.equals(FN_BUILD_CONFIG_BASE)) { + mSeenBuildConfig = true; + } else if (name.equals(FN_MANIFEST_BASE)) { + mSeenManifest = true; + } + } + return super.visit(node); + }; + + public boolean seenAny() { + return mSeenR || mSeenBuildConfig || mSeenManifest; + } + + public boolean hasSeenBuildConfig() { + return mSeenBuildConfig; + } + public boolean hasSeenManifest() { + return mSeenManifest; + } + public boolean hasSeenR() { + return mSeenR; + } + } + + private class ImportVisitor extends ASTVisitor { final AST mAst; final ASTRewrite mRewriter; @@ -505,8 +562,19 @@ class ApplicationPackageNameRefactoring extends Refactoring { if (importName.isQualifiedName()) { QualifiedName qualifiedImportName = (QualifiedName) importName; - if (qualifiedImportName.getName().getIdentifier() - .equals(SdkConstants.FN_RESOURCE_BASE)) { + String identifier = qualifiedImportName.getName().getIdentifier(); + if (identifier.equals(FN_RESOURCE_BASE)) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + } else if (identifier.equals(FN_BUILD_CONFIG_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { + mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, + null); + + } else if (identifier.equals(FN_MANIFEST_BASE) + && mOldPackageName.toString().equals( + qualifiedImportName.getQualifier().toString())) { mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName, null); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java index c556f14..bb475aa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/refactorings/renamepackage/RenamePackageAction.java @@ -102,7 +102,7 @@ public class RenamePackageAction implements IObjectActionDelegate { // enforce a save as a convenience. RefactoringSaveHelper save_helper = new RefactoringSaveHelper( RefactoringSaveHelper.SAVE_ALL_ALWAYS_ASK); - if (save_helper.saveEditors(AdtPlugin.getDisplay().getActiveShell())) { + if (save_helper.saveEditors(AdtPlugin.getShell())) { promptNewName(project); } } @@ -142,7 +142,7 @@ public class RenamePackageAction implements IObjectActionDelegate { } }; - InputDialog dialog = new InputDialog(AdtPlugin.getDisplay().getActiveShell(), + InputDialog dialog = new InputDialog(AdtPlugin.getShell(), "Rename Application Package", "Enter new package name:", oldPackageNameString, validator); @@ -165,7 +165,7 @@ public class RenamePackageAction implements IObjectActionDelegate { new ApplicationPackageNameRefactoringWizard(package_name_refactoring); RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard); try { - op.run(AdtPlugin.getDisplay().getActiveShell(), package_name_refactoring.getName()); + op.run(AdtPlugin.getShell(), package_name_refactoring.getName()); } catch (InterruptedException e) { Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e); AdtPlugin.getDefault().getLog().log(s); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java index 6b09b34..978980b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java @@ -34,12 +34,14 @@ import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceDeltaKind; +import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.common.resources.configuration.CountryCodeQualifier; import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.KeyboardStateQualifier; import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; import com.android.ide.common.resources.configuration.NavigationMethodQualifier; import com.android.ide.common.resources.configuration.NavigationStateQualifier; import com.android.ide.common.resources.configuration.NetworkCodeQualifier; @@ -127,6 +129,7 @@ public class ResourceHelper { sIconMap.put(NetworkCodeQualifier.class, factory.getIcon("mnc")); //$NON-NLS-1$ sIconMap.put(LanguageQualifier.class, factory.getIcon("language")); //$NON-NLS-1$ sIconMap.put(RegionQualifier.class, factory.getIcon("region")); //$NON-NLS-1$ + sIconMap.put(LayoutDirectionQualifier.class, factory.getIcon("bidi")); //$NON-NLS-1$ sIconMap.put(ScreenSizeQualifier.class, factory.getIcon("size")); //$NON-NLS-1$ sIconMap.put(ScreenRatioQualifier.class, factory.getIcon("ratio")); //$NON-NLS-1$ sIconMap.put(ScreenOrientationQualifier.class, factory.getIcon("orientation")); //$NON-NLS-1$ @@ -178,39 +181,6 @@ public class ResourceHelper { } /** - * Return the resource type of the given url, and the resource name - * - * @param url the resource url to be parsed - * @return a pair of the resource type and the resource name - */ - public static Pair<ResourceType,String> parseResource(String url) { - if (!url.startsWith(PREFIX_RESOURCE_REF)) { - return null; - } - int typeEnd = url.indexOf('/', 1); - if (typeEnd == -1) { - return null; - } - int nameBegin = typeEnd + 1; - - // Skip @ and @+ - int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$ - - int colon = url.lastIndexOf(':', typeEnd); - if (colon != -1) { - typeBegin = colon + 1; - } - String typeName = url.substring(typeBegin, typeEnd); - ResourceType type = ResourceType.getEnum(typeName); - if (type == null) { - return null; - } - String name = url.substring(nameBegin); - - return Pair.of(type, name); - } - - /** * Is this a resource that can be defined in any file within the "values" folder? * <p> * Some resource types can be defined <b>both</b> as a separate XML file as well @@ -279,7 +249,7 @@ public class ResourceHelper { return false; } - Pair<ResourceType,String> parsed = parseResource(resource); + Pair<ResourceType,String> parsed = ResourceRepository.parseResource(resource); if (parsed != null) { ResourceType type = parsed.getFirst(); String name = parsed.getSecond(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java index 6554cc2..ab5ae40 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/CompiledResourcesMonitor.java @@ -84,9 +84,9 @@ public final class CompiledResourcesMonitor implements IFileListener, IProjectLi */ @Override public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { - if (flags == IResourceDelta.MARKERS) { - // Only the markers changed: not relevant + int kind, @Nullable String extension, int flags, boolean isAndroidProject) { + if (!isAndroidProject || flags == IResourceDelta.MARKERS) { + // Not Android or only the markers changed: not relevant return; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java index 7cb4e94..674a601 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/GlobalProjectMonitor.java @@ -20,6 +20,7 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.resources.ResourceFile; import com.android.ide.common.resources.ResourceFolder; +import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; @@ -76,9 +77,10 @@ public final class GlobalProjectMonitor { * not have an extension * @param flags the {@link IResourceDelta#getFlags()} value with details * on what changed in the file + * @param isAndroidProject whether the parent project is an Android Project */ public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags); + int kind, @Nullable String extension, int flags, boolean isAndroidProject); } /** @@ -138,8 +140,9 @@ public final class GlobalProjectMonitor { * Sent when a folder changed. * @param folder The file that was changed * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()} + * @param isAndroidProject whether the parent project is an Android Project */ - public void folderChanged(IFolder folder, int kind); + public void folderChanged(IFolder folder, int kind, boolean isAndroidProject); } /** @@ -208,6 +211,8 @@ public final class GlobalProjectMonitor { private IWorkspace mWorkspace; + private boolean mIsAndroidProject; + /** * Delta visitor for resource changes. */ @@ -226,7 +231,7 @@ public final class GlobalProjectMonitor { || (bundle.kindMask & kind) != 0) { try { bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind, - r.getFileExtension(), delta.getFlags()); + r.getFileExtension(), delta.getFlags(), mIsAndroidProject); } catch (Throwable t) { AdtPlugin.log(t,"Failed to call IFileListener.fileChanged"); } @@ -240,7 +245,7 @@ public final class GlobalProjectMonitor { if (bundle.kindMask == ListenerBundle.MASK_NONE || (bundle.kindMask & kind) != 0) { try { - bundle.listener.folderChanged((IFolder)r, kind); + bundle.listener.folderChanged((IFolder)r, kind, mIsAndroidProject); } catch (Throwable t) { AdtPlugin.log(t,"Failed to call IFileListener.folderChanged"); } @@ -248,11 +253,27 @@ public final class GlobalProjectMonitor { } return true; } else if (type == IResource.PROJECT) { + IProject project = (IProject)r; + + try { + mIsAndroidProject = project.hasNature(AdtConstants.NATURE_DEFAULT); + } catch (CoreException e) { + // this can only happen if the project does not exist or is not open, neither + // of which can happen here since we are processing changes in the project + // or at worst a project post-open event. + return false; + } + + if (mIsAndroidProject == false) { + // for non android project, skip the project listeners but return true + // to visit the children and notify the IFileListeners + return true; + } + int flags = delta.getFlags(); if ((flags & IResourceDelta.OPEN) != 0) { // the project is opening or closing. - IProject project = (IProject)r; if (project.isOpen()) { // notify the listeners. @@ -491,9 +512,15 @@ public final class GlobalProjectMonitor { // notify the listeners. for (IProjectListener pl : mProjectListeners) { try { - pl.projectDeleted(project); - } catch (Throwable t) { - AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted"); + if (project.hasNature(AdtConstants.NATURE_DEFAULT)) { + try { + pl.projectDeleted(project); + } catch (Throwable t) { + AdtPlugin.log(t,"Failed to call IProjectListener.projectDeleted"); + } + } + } catch (CoreException e) { + // just ignore this project. } } } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java index ccd1666..68c2257 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ProjectResources.java @@ -16,6 +16,7 @@ package com.android.ide.eclipse.adt.internal.resources.manager; +import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.IntArrayWrapper; @@ -26,6 +27,7 @@ import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFolder; import com.android.resources.ResourceType; import com.android.util.Pair; @@ -48,6 +50,7 @@ import java.util.Map.Entry; * on the fly.</li> *</ul> */ +@SuppressWarnings("deprecation") public class ProjectResources extends ResourceRepository { // project resources are defined as 0x7FXX#### where XX is the resource type (layout, drawable, // etc...). Using FF as the type allows for 255 resource types before we get a collision @@ -65,12 +68,18 @@ public class ProjectResources extends ResourceRepository { private final IProject mProject; + public static ProjectResources create(IProject project) { + IFolder resFolder = project.getFolder(SdkConstants.FD_RESOURCES); + + return new ProjectResources(project, new IFolderWrapper(resFolder)); + } + /** * Makes a ProjectResources for a given <var>project</var>. * @param project the project. */ - public ProjectResources(IProject project) { - super(false /*isFrameworkRepository*/); + private ProjectResources(IProject project, IAbstractFolder resFolder) { + super(resFolder, false /*isFrameworkRepository*/); mProject = project; } @@ -85,6 +94,7 @@ public class ProjectResources extends ResourceRepository { @NonNull public Map<ResourceType, Map<String, ResourceValue>> getConfiguredResources( @NonNull FolderConfiguration referenceConfig) { + ensureInitialized(); Map<ResourceType, Map<String, ResourceValue>> resultMap = new EnumMap<ResourceType, Map<String, ResourceValue>>(ResourceType.class); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java index e1a12d7..e407b6a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/manager/ResourceManager.java @@ -49,7 +49,6 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.QualifiedName; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -151,14 +150,14 @@ public final class ResourceManager { /** * Returns the resources of a project. * @param project The project - * @return a ProjectResources object or null if none was found. + * @return a ProjectResources object */ public ProjectResources getProjectResources(IProject project) { synchronized (mMap) { ProjectResources resources = mMap.get(project); if (resources == null) { - resources = new ProjectResources(project); + resources = ProjectResources.create(project); mMap.put(project, resources); } @@ -253,7 +252,7 @@ public final class ResourceManager { // if it doesn't exist, we create it. if (resources == null) { - resources = new ProjectResources(project); + resources = ProjectResources.create(project); mMap.put(project, resources); } } @@ -422,6 +421,15 @@ public final class ResourceManager { for (IResourceDelta delta : projectDeltas) { if (delta.getResource() instanceof IProject) { IProject project = (IProject) delta.getResource(); + + try { + if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { + continue; + } + } catch (CoreException e) { + // only happens if the project is closed or doesn't exist. + } + IdeScanningContext context = new IdeScanningContext(getProjectResources(project), project, true); @@ -482,16 +490,11 @@ public final class ResourceManager { FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath); if (frameworkRes.exists()) { - FrameworkResources resources = new FrameworkResources(); - - try { - resources.loadResources(frameworkRes); - resources.loadPublicResources(frameworkRes, AdtPlugin.getDefault()); - return resources; - } catch (IOException e) { - // since we test that folders are folders, and files are files, this shouldn't - // happen. We can ignore it. - } + FrameworkResources resources = new FrameworkResources(frameworkRes); + + resources.loadResources(); + resources.loadPublicResources(AdtPlugin.getDefault()); + return resources; } return null; @@ -503,62 +506,13 @@ public final class ResourceManager { */ private void createProject(IProject project) { if (project.isOpen()) { - try { - if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) { - return; - } - } catch (CoreException e1) { - // can't check the nature of the project? ignore it. - return; - } - - IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES); - - ProjectResources projectResources; synchronized (mMap) { - projectResources = mMap.get(project); + ProjectResources projectResources = mMap.get(project); if (projectResources == null) { - projectResources = new ProjectResources(project); + projectResources = ProjectResources.create(project); mMap.put(project, projectResources); } } - IdeScanningContext context = new IdeScanningContext(projectResources, project, true); - - if (resourceFolder != null && resourceFolder.exists()) { - try { - IResource[] resources = resourceFolder.members(); - - for (IResource res : resources) { - if (res.getType() == IResource.FOLDER) { - IFolder folder = (IFolder)res; - ResourceFolder resFolder = projectResources.processFolder( - new IFolderWrapper(folder)); - - if (resFolder != null) { - // now we process the content of the folder - IResource[] files = folder.members(); - - for (IResource fileRes : files) { - if (fileRes.getType() == IResource.FILE) { - IFile file = (IFile)fileRes; - - context.startScanning(file); - - resFolder.processFile(new IFileWrapper(file), - ResourceHelper.getResourceDeltaKind( - IResourceDelta.ADDED), context); - - context.finishScanning(file); - } - } - } - } - } - } catch (CoreException e) { - // This happens if the project is closed or if the folder doesn't exist. - // Since we already test for that, we can ignore this exception. - } - } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java index 26afebd..2396a4c 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AdtConsoleSdkLog.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.sdk; import com.android.annotations.NonNull; +import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.utils.ILogger; @@ -28,7 +29,7 @@ public class AdtConsoleSdkLog implements ILogger { private static final String TAG = "SDK Manager"; //$NON-NLS-1$ @Override - public void error(Throwable t, String errorFormat, Object... args) { + public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { if (t != null) { AdtPlugin.logAndPrintError(t, TAG, "Error: " + errorFormat, args); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java index 161d567..c4eb37f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/AndroidJarLoader.java @@ -17,6 +17,7 @@ package com.android.ide.eclipse.adt.internal.sdk; import com.android.SdkConstants; +import com.google.common.io.Closeables; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SubMonitor; @@ -205,6 +206,7 @@ public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader * @throws InvalidAttributeValueException * @throws ClassFormatError */ + @SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly @Override public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom( String packageFilter, @@ -223,43 +225,47 @@ public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader // create streams to read the intermediary archive FileInputStream fis = new FileInputStream(mOsFrameworkLocation); ZipInputStream zis = new ZipInputStream(fis); - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - // get the name of the entry and convert to a class binary name - String entryPath = entry.getName(); - if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) { - // only accept class files - continue; - } - if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) { - // only accept stuff from the requested root package. - continue; - } - String className = entryPathToClassName(entryPath); - - Class<?> loaded_class = mClassCache.get(className); - if (loaded_class == null) { - byte[] data = mEntryCache.get(className); - if (data == null) { - // Get the class and cache it - long entrySize = entry.getSize(); - if (entrySize > Integer.MAX_VALUE) { - throw new InvalidAttributeValueException(); + try { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // get the name of the entry and convert to a class binary name + String entryPath = entry.getName(); + if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) { + // only accept class files + continue; + } + if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) { + // only accept stuff from the requested root package. + continue; + } + String className = entryPathToClassName(entryPath); + + Class<?> loaded_class = mClassCache.get(className); + if (loaded_class == null) { + byte[] data = mEntryCache.get(className); + if (data == null) { + // Get the class and cache it + long entrySize = entry.getSize(); + if (entrySize > Integer.MAX_VALUE) { + throw new InvalidAttributeValueException(); + } + data = readZipData(zis, (int)entrySize); } - data = readZipData(zis, (int)entrySize); + loaded_class = defineAndCacheClass(className, data); } - loaded_class = defineAndCacheClass(className, data); - } - for (Class<?> superClass = loaded_class.getSuperclass(); - superClass != null; - superClass = superClass.getSuperclass()) { - String superName = superClass.getCanonicalName(); - if (mClassesFound.containsKey(superName)) { - mClassesFound.get(superName).add(new ClassWrapper(loaded_class)); - break; + for (Class<?> superClass = loaded_class.getSuperclass(); + superClass != null; + superClass = superClass.getSuperclass()) { + String superName = superClass.getCanonicalName(); + if (mClassesFound.containsKey(superName)) { + mClassesFound.get(superName).add(new ClassWrapper(loaded_class)); + break; + } } } + } finally { + Closeables.closeQuietly(zis); } return mClassesFound; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java index 64053ad..4628509 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.sdk; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; @@ -160,6 +161,7 @@ public final class ProjectState { private final IProject mProject; private final ProjectProperties mProperties; private IAndroidTarget mTarget; + private BuildToolInfo mBuildToolInfo; /** * list of libraries. Access to this list must be protected by @@ -240,6 +242,23 @@ public final class ProjectState { return mTarget; } + public void setBuildToolInfo(BuildToolInfo buildToolInfo) { + mBuildToolInfo = buildToolInfo; + } + + public BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } + + /** + * Returns the build tools version from the project's properties. + * @return the value or null + */ + @Nullable + public String getBuildToolInfoVersion() { + return mProperties.getProperty(ProjectProperties.PROPERTY_BUILD_TOOLS); + } + public static class LibraryDifference { public boolean removed = false; public boolean added = false; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java index 1a299d9..74f08c2 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/Sdk.java @@ -43,18 +43,21 @@ import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState; import com.android.io.StreamException; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkManager; -import com.android.sdklib.devices.Device; import com.android.sdklib.devices.DeviceManager; import com.android.sdklib.internal.avd.AvdManager; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; +import com.android.sdklib.repository.FullRevision; import com.android.utils.ILogger; +import com.google.common.collect.Maps; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -131,7 +134,7 @@ public final class Sdk { } private final SdkManager mManager; - private final DexWrapper mDexWrapper; + private final Map<String, DexWrapper> mDexWrappers = Maps.newHashMap(); private final AvdManager mAvdManager; private final DeviceManager mDeviceManager; @@ -236,7 +239,8 @@ public final class Sdk { final ArrayList<String> logMessages = new ArrayList<String>(); ILogger log = new ILogger() { @Override - public void error(Throwable throwable, String errorFormat, Object... arg) { + public void error(@Nullable Throwable throwable, @Nullable String errorFormat, + Object... arg) { if (errorFormat != null) { logMessages.add(String.format("Error: " + errorFormat, arg)); } @@ -265,17 +269,6 @@ public final class Sdk { // get an SdkManager object for the location SdkManager manager = SdkManager.createManager(sdkLocation, log); if (manager != null) { - // load DX. - DexWrapper dexWrapper = new DexWrapper(); - String dexLocation = - sdkLocation + File.separator + - SdkConstants.OS_SDK_PLATFORM_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR; - IStatus res = dexWrapper.loadDex(dexLocation); - if (res != Status.OK_STATUS) { - log.error(null, res.getMessage()); - dexWrapper = null; - } - // create the AVD Manager AvdManager avdManager = null; try { @@ -283,7 +276,7 @@ public final class Sdk { } catch (AndroidLocationException e) { log.error(e, "Error parsing the AVDs"); } - sCurrentSdk = new Sdk(manager, dexWrapper, avdManager); + sCurrentSdk = new Sdk(manager, avdManager); return sCurrentSdk; } else { StringBuilder sb = new StringBuilder("Error Loading the SDK:\n"); @@ -329,7 +322,7 @@ public final class Sdk { * @param log The logger for the {@link SdkManager}. * @return A new {@link SdkManager} parsing the same location. */ - public @NonNull SdkManager getNewSdkManager(@NonNull ILogger log) { + public @Nullable SdkManager getNewSdkManager(@NonNull ILogger log) { return SdkManager.createManager(getSdkLocation(), log); } @@ -376,6 +369,24 @@ public final class Sdk { return mManager.getTargetFromHashString(hash); } + @Nullable + public BuildToolInfo getBuildToolInfo(@Nullable String buildToolVersion) { + if (buildToolVersion != null) { + try { + return mManager.getBuildTool(FullRevision.parseRevision(buildToolVersion)); + } catch (Exception e) { + // ignore, return null below. + } + } + + return null; + } + + @Nullable + public BuildToolInfo getLatestBuildTool() { + return mManager.getLatestBuildTool(false /*isPreview*/); + } + /** * Initializes a new project with a target. This creates the <code>project.properties</code> * file. @@ -487,7 +498,7 @@ public final class Sdk { // try to resolve the target if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) { - sCurrentSdk.loadTarget(state); + sCurrentSdk.loadTargetAndBuildTools(state); } } @@ -513,26 +524,84 @@ public final class Sdk { } /** - * Loads the {@link IAndroidTarget} for a given project. + * Loads the {@link IAndroidTarget} and BuildTools for a given project. * <p/>This method will get the target hash string from the project properties, and resolve * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}. * @param state the state representing the project to load. * @return the target that was loaded. */ @Nullable - public IAndroidTarget loadTarget(ProjectState state) { + public IAndroidTarget loadTargetAndBuildTools(ProjectState state) { IAndroidTarget target = null; if (state != null) { String hash = state.getTargetHashString(); if (hash != null) { state.setTarget(target = getTargetFromHashString(hash)); } + + String markerMessage = null; + String buildToolInfoVersion = state.getBuildToolInfoVersion(); + if (buildToolInfoVersion != null) { + BuildToolInfo buildToolsInfo = getBuildToolInfo(buildToolInfoVersion); + + if (buildToolsInfo != null) { + state.setBuildToolInfo(buildToolsInfo); + } else { + markerMessage = String.format("Unable to resolve %s property value '%s'", + ProjectProperties.PROPERTY_BUILD_TOOLS, + buildToolInfoVersion); + } + } else { + // this is ok, we'll use the latest one automatically. + state.setBuildToolInfo(null); + } + + handleBuildToolsMarker(state.getProject(), markerMessage); } return target; } /** + * Adds or edit a build tools marker from the given project. This is done through a Job. + * @param project the project + * @param markerMessage the message. if null the marker is removed. + */ + private void handleBuildToolsMarker(final IProject project, final String markerMessage) { + Job markerJob = new Job("Android SDK: Build Tools Marker") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + if (project.isAccessible()) { + // always delete existing marker first + project.deleteMarkers(AdtConstants.MARKER_BUILD_TOOLS, true, + IResource.DEPTH_ZERO); + + // add the new one if needed. + if (markerMessage != null) { + BaseProjectHelper.markProject(project, + AdtConstants.MARKER_BUILD_TOOLS, + markerMessage, IMarker.SEVERITY_ERROR, + IMarker.PRIORITY_HIGH); + } + } + } catch (CoreException e2) { + AdtPlugin.log(e2, null); + // Don't return e2.getStatus(); the job control will then produce + // a popup with this error, which isn't very interesting for the + // user. + } + + return Status.OK_STATUS; + } + }; + + // build jobs are run after other interactive jobs + markerJob.setPriority(Job.BUILD); + markerJob.schedule(); + } + + /** * Checks and loads (if needed) the data for a given target. * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}. @@ -668,8 +737,38 @@ public final class Sdk { * Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not * loaded properly, then this will return <code>null</code>. */ - public DexWrapper getDexWrapper() { - return mDexWrapper; + @Nullable + public DexWrapper getDexWrapper(@Nullable BuildToolInfo buildToolInfo) { + if (buildToolInfo == null) { + return null; + } + synchronized (LOCK) { + String dexLocation = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR); + DexWrapper dexWrapper = mDexWrappers.get(dexLocation); + + if (dexWrapper == null) { + // load DX. + dexWrapper = new DexWrapper(); + IStatus res = dexWrapper.loadDex(dexLocation); + if (res != Status.OK_STATUS) { + AdtPlugin.log(null, res.getMessage()); + dexWrapper = null; + } else { + mDexWrappers.put(dexLocation, dexWrapper); + } + } + + return dexWrapper; + } + } + + public void unloadDexWrappers() { + synchronized (LOCK) { + for (DexWrapper wrapper : mDexWrappers.values()) { + wrapper.unload(); + } + mDexWrappers.clear(); + } } /** @@ -702,12 +801,6 @@ public final class Sdk { return mDeviceManager; } - /** Returns the devices provided by the SDK, including user created devices */ - @NonNull - public List<Device> getDevices() { - return mDeviceManager.getDevices(getSdkLocation()); - } - /** * Returns a list of {@link ProjectState} representing projects depending, directly or * indirectly on a given library project. @@ -769,9 +862,8 @@ public final class Sdk { } } - private Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager) { + private Sdk(SdkManager manager, AvdManager avdManager) { mManager = manager; - mDexWrapper = dexWrapper; mAvdManager = avdManager; // listen to projects closing @@ -784,16 +876,16 @@ public final class Sdk { IResourceDelta.CHANGED | IResourceDelta.ADDED | IResourceDelta.REMOVED); // pre-compute some paths - mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() + + mDocBaseUrl = getDocumentationBaseUrl(manager.getLocation() + SdkConstants.OS_SDK_DOCS_FOLDER); - mDeviceManager = new DeviceManager(AdtPlugin.getDefault()); + mDeviceManager = DeviceManager.createInstance(manager.getLocation(), + AdtPlugin.getDefault()); // update whatever ProjectState is already present with new IAndroidTarget objects. synchronized (LOCK) { for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) { - entry.getValue().setTarget( - getTargetFromHashString(entry.getValue().getTargetHashString())); + loadTargetAndBuildTools(entry.getValue()); } } } @@ -880,16 +972,6 @@ public final class Sdk { } private void onProjectRemoved(IProject removedProject, boolean deleted) { - try { - if (removedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { - return; - } - } catch (CoreException e) { - // this can only happen if the project does not exist or is not open, neither - // of which can happen here since we're processing a Project removed/deleted event - // which is processed before the project is actually removed/closed. - } - if (DEBUG) { System.out.println(">>> CLOSED: " + removedProject.getName()); } @@ -963,15 +1045,6 @@ public final class Sdk { } private void onProjectOpened(final IProject openedProject) { - try { - if (openedProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { - return; - } - } catch (CoreException e) { - // this can only happen if the project does not exist or is not open, neither - // of which can happen here since we're processing a Project opened event. - } - ProjectState openedState = getProjectState(openedProject); if (openedState != null) { @@ -1052,7 +1125,11 @@ public final class Sdk { private IFileListener mFileListener = new IFileListener() { @Override public void fileChanged(final @NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas, - int kind, @Nullable String extension, int flags) { + int kind, @Nullable String extension, int flags, boolean isAndroidPRoject) { + if (!isAndroidPRoject) { + return; + } + if (SdkConstants.FN_PROJECT_PROPERTIES.equals(file.getName()) && file.getParent() == file.getProject()) { try { @@ -1060,13 +1137,9 @@ public final class Sdk { // the target. IProject iProject = file.getProject(); - if (iProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { - return; - } - ProjectState state = Sdk.getProjectState(iProject); - // get the current target + // get the current target and build tools IAndroidTarget oldTarget = state.getTarget(); // get the current library flag @@ -1075,7 +1148,7 @@ public final class Sdk { LibraryDifference diff = state.reloadProperties(); // load the (possibly new) target. - IAndroidTarget newTarget = loadTarget(state); + IAndroidTarget newTarget = loadTargetAndBuildTools(state); // reload the libraries if needed if (diff.hasDiff()) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ConfigurationSelector.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ConfigurationSelector.java index ce36457..15453fb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ConfigurationSelector.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ConfigurationSelector.java @@ -22,6 +22,7 @@ import com.android.ide.common.resources.configuration.DensityQualifier; import com.android.ide.common.resources.configuration.FolderConfiguration; import com.android.ide.common.resources.configuration.KeyboardStateQualifier; import com.android.ide.common.resources.configuration.LanguageQualifier; +import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; import com.android.ide.common.resources.configuration.NavigationMethodQualifier; import com.android.ide.common.resources.configuration.NavigationStateQualifier; import com.android.ide.common.resources.configuration.NetworkCodeQualifier; @@ -44,6 +45,7 @@ import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.resources.Density; import com.android.resources.Keyboard; import com.android.resources.KeyboardState; +import com.android.resources.LayoutDirection; import com.android.resources.Navigation; import com.android.resources.NavigationState; import com.android.resources.NightMode; @@ -433,6 +435,8 @@ public class ConfigurationSelector extends Composite { mUiMap.put(NetworkCodeQualifier.class, new MNCEdit(mQualifierEditParent)); mUiMap.put(LanguageQualifier.class, new LanguageEdit(mQualifierEditParent)); mUiMap.put(RegionQualifier.class, new RegionEdit(mQualifierEditParent)); + mUiMap.put(LayoutDirectionQualifier.class, + new LayoutDirectionEdit(mQualifierEditParent)); mUiMap.put(SmallestScreenWidthQualifier.class, new SmallestScreenWidthEdit(mQualifierEditParent)); mUiMap.put(ScreenWidthQualifier.class, new ScreenWidthEdit(mQualifierEditParent)); @@ -1017,6 +1021,65 @@ public class ConfigurationSelector extends Composite { } /** + * Edit widget for {@link LayoutDirectionQualifier}. + */ + private class LayoutDirectionEdit extends QualifierEditBase { + + private Combo mDirection; + + public LayoutDirectionEdit(Composite parent) { + super(parent, LayoutDirectionQualifier.NAME); + + mDirection = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY); + fillCombo(mDirection, LayoutDirection.values()); + + mDirection.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDirection.addSelectionListener(new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onDirectionChange(); + } + @Override + public void widgetSelected(SelectionEvent e) { + onDirectionChange(); + } + }); + } + + protected void onDirectionChange() { + // update the current config + int index = mDirection.getSelectionIndex(); + + if (index != -1) { + mSelectedConfiguration.setLayoutDirectionQualifier(new LayoutDirectionQualifier( + LayoutDirection.getByIndex(index))); + } else { + // empty selection, means no qualifier. + // Since the qualifier classes are immutable, and we don't want to + // remove the qualifier from the configuration, we create a new default one. + mSelectedConfiguration.setLayoutDirectionQualifier( + new LayoutDirectionQualifier()); + } + + // notify of change + onChange(true /* keepSelection */); + } + + @Override + public void setQualifier(ResourceQualifier qualifier) { + LayoutDirectionQualifier q = (LayoutDirectionQualifier)qualifier; + + LayoutDirection value = q.getValue(); + if (value == null) { + mDirection.clearSelection(); + } else { + mDirection.select(LayoutDirection.getIndex(value)); + } + } + } + + + /** * Edit widget for {@link SmallestScreenWidthQualifier}. */ private class SmallestScreenWidthEdit extends QualifierEditBase { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java index 066f4a1..c2a5f71 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/MarginChooser.java @@ -15,14 +15,10 @@ */ package com.android.ide.eclipse.adt.internal.ui; -import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; -import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; -import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.resources.ResourceType; -import org.eclipse.core.resources.IProject; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; @@ -219,19 +215,13 @@ public class MarginChooser extends SelectionStatusDialog implements Listener { // Button pressed - open resource chooser if (event.widget instanceof Button) { Button button = (Button) event.widget; + Text text = (Text) button.getData(PROP_TEXTFIELD); // Open a resource chooser dialog for specified resource type. - IProject project = mEditor.getProject(); - ProjectResources projectRepository = ResourceManager.getInstance() - .getProjectResources(project); - ResourceRepository frameworkRepository = mTargetData.getFrameworkResources(); - ResourceChooser dlg = new ResourceChooser(project, ResourceType.DIMEN, - projectRepository, frameworkRepository, getShell()); - dlg.setResourceResolver(mEditor.getResourceResolver()); - Text text = (Text) button.getData(PROP_TEXTFIELD); - dlg.setCurrentResource(text.getText().trim()); - if (dlg.open() == Window.OK) { - text.setText(dlg.getCurrentResource()); + ResourceChooser chooser = ResourceChooser.create(mEditor, ResourceType.DIMEN) + .setCurrentResource(text.getText().trim()); + if (chooser.open() == Window.OK) { + text.setText(chooser.getCurrentResource()); } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ReferenceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ReferenceChooserDialog.java index 4906730..6c62865 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ReferenceChooserDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ReferenceChooserDialog.java @@ -19,6 +19,7 @@ package com.android.ide.eclipse.adt.internal.ui; import com.android.ide.common.resources.ResourceItem; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; import com.android.resources.ResourceType; @@ -142,6 +143,11 @@ public class ReferenceChooserDialog extends SelectionStatusDialog { // create the "New Resource" button createNewResButtons(top); + Composite workaround = PropertyFactory.addWorkaround(top); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } + return top; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java index 1291af8..ce828cf 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourceChooser.java @@ -28,16 +28,20 @@ import com.android.ide.common.resources.ResourceResolver; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.assetstudio.OpenCreateAssetSetWizardAction; -import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; +import com.android.ide.eclipse.adt.internal.editors.layout.properties.PropertyFactory; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; +import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.resources.ResourceType; import com.android.utils.Pair; +import com.google.common.collect.Maps; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; @@ -71,11 +75,13 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog; import org.eclipse.ui.dialogs.SelectionStatusDialog; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -90,7 +96,7 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen private Pattern mProjectResourcePattern; private ResourceType mResourceType; - private final ResourceRepository mProjectResources; + private final List<ResourceRepository> mProjectResources; private final ResourceRepository mFrameworkResources; private Pattern mSystemResourcePattern; private Button mProjectButton; @@ -140,10 +146,12 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen * @param frameworkResources The Framework resource repository * @param parent the parent shell */ - public ResourceChooser(IProject project, ResourceType type, - ResourceRepository projectResources, - ResourceRepository frameworkResources, - Shell parent) { + private ResourceChooser( + @NonNull IProject project, + @NonNull ResourceType type, + @NonNull List<ResourceRepository> projectResources, + @Nullable ResourceRepository frameworkResources, + @NonNull Shell parent) { super(parent, new ResourceLabelProvider()); mProject = project; @@ -163,14 +171,85 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen } /** + * Creates a new {@link ResourceChooser} + * + * @param editor the associated layout editor + * @param type the resource type to choose + * @return a new {@link ResourceChooser} + */ + @NonNull + public static ResourceChooser create( + @NonNull GraphicalEditorPart editor, + @NonNull ResourceType type) { + IProject project = editor.getProject(); + Shell parent = editor.getCanvasControl().getShell(); + AndroidTargetData targetData = editor.getEditorDelegate().getEditor().getTargetData(); + ResourceChooser chooser = create(project, type, targetData, parent); + + // When editing Strings, allow editing the value text directly. When we + // get inline editing support (where values entered directly into the + // textual widget are translated automatically into a resource) this can + // go away. + if (type == ResourceType.STRING) { + chooser.setResourceResolver(editor.getResourceResolver()); + chooser.setShowValueText(true); + } else if (type == ResourceType.DIMEN || type == ResourceType.INTEGER) { + chooser.setResourceResolver(editor.getResourceResolver()); + } + + chooser.setPreviewHelper(new ResourcePreviewHelper(chooser, editor)); + return chooser; + } + + /** + * Creates a new {@link ResourceChooser} + * + * @param project the associated project + * @param type the resource type to choose + * @param targetData the associated framework target data + * @param parent the target shell + * @return a new {@link ResourceChooser} + */ + @NonNull + public static ResourceChooser create( + @NonNull IProject project, + @NonNull ResourceType type, + @Nullable AndroidTargetData targetData, + @NonNull Shell parent) { + ResourceManager manager = ResourceManager.getInstance(); + + List<ResourceRepository> projectResources = new ArrayList<ResourceRepository>(); + ProjectResources resources = manager.getProjectResources(project); + projectResources.add(resources); + + // Add in library project resources + ProjectState projectState = Sdk.getProjectState(project); + if (projectState != null) { + List<IProject> libraries = projectState.getFullLibraryProjects(); + if (libraries != null && !libraries.isEmpty()) { + for (IProject library : libraries) { + projectResources.add(manager.getProjectResources(library)); + } + } + } + + ResourceRepository frameworkResources = + targetData != null ? targetData.getFrameworkResources() : null; + return new ResourceChooser(project, type, projectResources, frameworkResources, parent); + } + + /** * Sets whether this dialog should show the value field as a separate text * value (and take the resulting value of the dialog from this text field * rather than from the selection) * * @param showValueText if true, show the value text field + * @return this, for constructor chaining */ - public void setShowValueText(boolean showValueText) { + public ResourceChooser setShowValueText(boolean showValueText) { mShowValueText = showValueText; + + return this; } /** @@ -178,9 +257,12 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen * selection * * @param resourceResolver the resource resolver to use + * @return this, for constructor chaining */ - public void setResourceResolver(ResourceResolver resourceResolver) { + public ResourceChooser setResourceResolver(ResourceResolver resourceResolver) { mResourceResolver = resourceResolver; + + return this; } /** @@ -188,9 +270,25 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen * resources, if any * * @param previewHelper the helper to use + * @return this, for constructor chaining */ - public void setPreviewHelper(ResourcePreviewHelper previewHelper) { + public ResourceChooser setPreviewHelper(ResourcePreviewHelper previewHelper) { mPreviewHelper = previewHelper; + + return this; + } + + /** + * Sets the initial dialog size + * + * @param width the initial width + * @param height the initial height + * @return this, for constructor chaining + */ + public ResourceChooser setInitialSize(int width, int height) { + setSize(width, height); + + return this; } @Override @@ -220,20 +318,42 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen } } - public void setCurrentResource(String resource) { + /** + * Sets the currently selected item + * + * @param resource the resource url for the currently selected item + * @return this, for constructor chaining + */ + public ResourceChooser setCurrentResource(@Nullable String resource) { mCurrentResource = resource; if (mShowValueText && mEditValueText != null) { mEditValueText.setText(resource); } + + return this; } + /** + * Returns the currently selected url + * + * @return the currently selected url + */ + @Nullable public String getCurrentResource() { return mCurrentResource; } - public void setInputValidator(IInputValidator inputValidator) { + /** + * Sets the input validator to use, if any + * + * @param inputValidator the validator + * @return this, for constructor chaining + */ + public ResourceChooser setInputValidator(@Nullable IInputValidator inputValidator) { mInputValidator = inputValidator; + + return this; } @Override @@ -328,6 +448,9 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen } } }); + if (mFrameworkResources == null) { + mSystemButton.setVisible(false); + } } /** @@ -439,6 +562,11 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen data.verticalAlignment = GridData.BEGINNING; mResolvedLabel.setLayoutData(data); } + + Composite workaround = PropertyFactory.addWorkaround(top); + if (workaround != null) { + workaround.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1)); + } } private void updateResolvedLabel() { @@ -513,7 +641,7 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen @Nullable private String createNewFile(ResourceType type) { // Show a name/value dialog entering the key name and the value - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); if (shell == null) { return null; } @@ -521,7 +649,7 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen ResourceNameValidator validator = ResourceNameValidator.create(true /*allowXmlExtension*/, mProject, mResourceType); InputDialog d = new InputDialog( - AdtPlugin.getDisplay().getActiveShell(), + AdtPlugin.getShell(), "Enter name", // title "Enter name", "", //$NON-NLS-1$ @@ -546,7 +674,7 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen @Nullable private String createNewValue(ResourceType type) { // Show a name/value dialog entering the key name and the value - Shell shell = AdtPlugin.getDisplay().getActiveShell(); + Shell shell = AdtPlugin.getShell(); if (shell == null) { return null; } @@ -593,7 +721,19 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen private ResourceItem[] setupResourceList() { Collection<ResourceItem> items = null; if (mProjectButton.getSelection()) { - items = mProjectResources.getResourceItemsOfType(mResourceType); + if (mProjectResources.size() == 1) { + items = mProjectResources.get(0).getResourceItemsOfType(mResourceType); + } else { + Map<String, ResourceItem> merged = Maps.newHashMapWithExpectedSize(200); + for (ResourceRepository repository : mProjectResources) { + for (ResourceItem item : repository.getResourceItemsOfType(mResourceType)) { + if (!merged.containsKey(item.getName())) { + merged.put(item.getName(), item); + } + } + } + items = merged.values(); + } } else if (mSystemButton.getSelection()) { items = mFrameworkResources.getResourceItemsOfType(mResourceType); } @@ -799,50 +939,18 @@ public class ResourceChooser extends AbstractElementListSelectionDialog implemen @NonNull GraphicalEditorPart graphicalEditor, @NonNull ResourceType type, String currentValue, IInputValidator validator) { - AndroidXmlEditor editor = graphicalEditor.getEditorDelegate().getEditor(); - IProject project = editor.getProject(); - if (project != null) { - // get the resource repository for this project and the system resources. - ResourceRepository projectRepository = ResourceManager.getInstance() - .getProjectResources(project); - Shell shell = AdtPlugin.getDisplay().getActiveShell(); - if (shell == null) { - return null; - } - - AndroidTargetData data = editor.getTargetData(); - ResourceRepository systemRepository = data.getFrameworkResources(); - - // open a resource chooser dialog for specified resource type. - ResourceChooser dlg = new ResourceChooser(project, type, projectRepository, - systemRepository, shell); - dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); - - // When editing Strings, allow editing the value text directly. When we - // get inline editing support (where values entered directly into the - // textual widget are translated automatically into a resource) this can - // go away. - if (type == ResourceType.STRING) { - dlg.setResourceResolver(graphicalEditor.getResourceResolver()); - dlg.setShowValueText(true); - } else if (type == ResourceType.DIMEN || type == ResourceType.INTEGER) { - dlg.setResourceResolver(graphicalEditor.getResourceResolver()); - } - - if (validator != null) { - // Ensure wide enough to accommodate validator error message - dlg.setSize(85, 10); - dlg.setInputValidator(validator); - } - - dlg.setCurrentResource(currentValue); - - int result = dlg.open(); - if (result == ResourceChooser.CLEAR_RETURN_CODE) { - return ""; //$NON-NLS-1$ - } else if (result == Window.OK) { - return dlg.getCurrentResource(); - } + ResourceChooser chooser = create(graphicalEditor, type). + setCurrentResource(currentValue); + if (validator != null) { + // Ensure wide enough to accommodate validator error message + chooser.setSize(85, 10); + chooser.setInputValidator(validator); + } + int result = chooser.open(); + if (result == ResourceChooser.CLEAR_RETURN_CODE) { + return ""; //$NON-NLS-1$ + } else if (result == Window.OK) { + return chooser.getCurrentResource(); } return null; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourcePreviewHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourcePreviewHelper.java index eeaca0c..afd1df9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourcePreviewHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/ui/ResourcePreviewHelper.java @@ -166,7 +166,7 @@ public class ResourcePreviewHelper { if (image == null) { RenderService renderService = RenderService.create(mEditor); - renderService.setSize(WIDTH, HEIGHT); + renderService.setOverrideRenderSize(WIDTH, HEIGHT); image = renderService.renderDrawable(drawable); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/utils/FingerprintUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/utils/FingerprintUtils.java new file mode 100644 index 0000000..bfe301e --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/utils/FingerprintUtils.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2008 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.utils; + +import java.util.Locale; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; + +public class FingerprintUtils { + + /** + * Returns the {@link Certificate} fingerprint as returned by <code>keytool</code>. + * + * @param certificate + * @param hashAlgorithm + */ + public static String getFingerprint(Certificate cert, String hashAlgorithm) { + if (cert == null) { + return null; + } + try { + MessageDigest digest = MessageDigest.getInstance(hashAlgorithm); + return toHexadecimalString(digest.digest(cert.getEncoded())); + } catch(NoSuchAlgorithmException e) { + // ignore + } catch(CertificateEncodingException e) { + // ignore + } + return null; + } + + private static String toHexadecimalString(byte[] value) { + StringBuffer sb = new StringBuffer(); + int len = value.length; + for (int i = 0; i < len; i++) { + int num = ((int) value[i]) & 0xff; + if (num < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString(num)); + if (i < len - 1) { + sb.append(':'); + } + } + return sb.toString().toUpperCase(Locale.US); + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java index 8d8b688..65eee94 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/AdtStartup.java @@ -71,15 +71,21 @@ public class AdtStartup implements IStartup, IWindowListener { @Override public void earlyStartup() { - if (InstallDetails.isAndroidIdePackage()) { - useBundledSdk(); + if (!isSdkSpecified()) { + File bundledSdk = getBundledSdk(); + if (bundledSdk != null) { + AdtPrefs.getPrefs().setSdkLocation(bundledSdk); + } + } + + boolean showSdkInstallationPage = !isSdkSpecified() && isFirstTime(); + boolean showOptInDialogPage = !mStore.hasPingId(); + + if (showSdkInstallationPage || showOptInDialogPage) { + showWelcomeWizard(showSdkInstallationPage, showOptInDialogPage); } - if (isFirstTime()) { - showWelcomeWizard(); - // Usage statistics are sent after the wizard has run asynchronously (provided the - // user opted in) - } else if (mStore.isPingOptIn()) { + if (mStore.isPingOptIn()) { sendUsageStats(); } @@ -88,40 +94,37 @@ public class AdtStartup implements IStartup, IWindowListener { AdtPlugin.getDefault().workbenchStarted(); } - private void useBundledSdk() { + private boolean isSdkSpecified() { String osSdkFolder = AdtPrefs.getPrefs().getOsSdkFolder(); + return (osSdkFolder != null && !osSdkFolder.isEmpty()); + } - // sdk path is already set - if (osSdkFolder != null && osSdkFolder.length() > 0) { - return; - } - - // The Android IDE bundle is structured as follows: - // root - // |--eclipse - // |--sdk - // So use the SDK folder that is + /** + * Returns the path to the bundled SDK if this is part of the ADT package. + * The ADT package has the following structure: + * root + * |--eclipse + * |--sdk + * @return path to bundled SDK, null if no valid bundled SDK detected. + */ + private File getBundledSdk() { Location install = Platform.getInstallLocation(); if (install != null && install.getURL() != null) { - String toolsFolder = new File(install.getURL().getFile()).getParent(); + File toolsFolder = new File(install.getURL().getFile()).getParentFile(); if (toolsFolder != null) { - String osSdkPath = toolsFolder + File.separator + "sdk"; - if (AdtPlugin.getDefault().checkSdkLocationAndId(osSdkPath, - new SdkValidator())) { - AdtPrefs.getPrefs().setSdkLocation(new File(osSdkPath)); + File sdkFolder = new File(toolsFolder, "sdk"); + if (sdkFolder.exists() && AdtPlugin.getDefault().checkSdkLocationAndId( + sdkFolder.getAbsolutePath(), + new SdkValidator())) { + return sdkFolder; } } } + + return null; } private boolean isFirstTime() { - // If we already have a known SDK location in our workspace then we know this - // is not the first time this user is running ADT. - String osSdkFolder = AdtPrefs.getPrefs().getOsSdkFolder(); - if (osSdkFolder != null && osSdkFolder.length() > 0) { - return false; - } - for (int i = 0; i < 2; i++) { String osSdkPath = null; @@ -251,14 +254,16 @@ public class AdtStartup implements IStartup, IWindowListener { }); } - private void showWelcomeWizard() { + private void showWelcomeWizard(final boolean showSdkInstallPage, + final boolean showUsageOptInPage) { final IWorkbench workbench = PlatformUI.getWorkbench(); workbench.getDisplay().asyncExec(new Runnable() { @Override public void run() { IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); if (window != null) { - WelcomeWizard wizard = new WelcomeWizard(mStore); + WelcomeWizard wizard = new WelcomeWizard(mStore, showSdkInstallPage, + showUsageOptInPage); WizardDialog dialog = new WizardDialog(window.getShell(), wizard); dialog.open(); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java index a81082b..916924e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizard.java @@ -41,16 +41,25 @@ import java.util.Set; */ public class WelcomeWizard extends Wizard { private final DdmsPreferenceStore mStore; + private WelcomeWizardPage mWelcomePage; private UsagePermissionPage mUsagePage; + private final boolean mShowWelcomePage; + private final boolean mShowUsagePage; + /** * Creates a new {@link WelcomeWizard} * * @param store preferences for usage statistics collection etc + * @param showInstallSdkPage show page to install SDK's + * @param showUsageOptinPage show page to get user consent for usage data collection */ - public WelcomeWizard(DdmsPreferenceStore store) { + public WelcomeWizard(DdmsPreferenceStore store, boolean showInstallSdkPage, + boolean showUsageOptinPage) { mStore = store; + mShowWelcomePage = showInstallSdkPage; + mShowUsagePage = showUsageOptinPage; setWindowTitle("Welcome to Android Development"); ImageDescriptor image = AdtPlugin.getImageDescriptor("icons/android-64.png"); //$NON-NLS-1$ @@ -59,13 +68,15 @@ public class WelcomeWizard extends Wizard { @Override public void addPages() { - mWelcomePage = new WelcomeWizardPage(); - addPage(mWelcomePage); + if (mShowWelcomePage) { + mWelcomePage = new WelcomeWizardPage(); + addPage(mWelcomePage); + } // It's possible that the user has already run the command line tools // such as ddms and has agreed to usage statistics collection, but has never // run ADT which is why the wizard was opened. No need to ask again. - if (!mStore.isPingOptIn()) { + if (mShowUsagePage && !mStore.hasPingId()) { mUsagePage = new UsagePermissionPage(); addPage(mUsagePage); } @@ -96,37 +107,40 @@ public class WelcomeWizard extends Wizard { store.setPingOptIn(isUsageCollectionApproved); } - // Read out wizard settings immediately; we will perform the actual work - // after the wizard window has been taken down and it's too late to read the - // settings then - final File path = mWelcomePage.getPath(); - final boolean installCommon = mWelcomePage.isInstallCommon(); - final boolean installLatest = mWelcomePage.isInstallLatest(); - final boolean createNew = mWelcomePage.isCreateNew(); - - // Perform installation asynchronously since it takes a while. - getShell().getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - if (createNew) { - try { - Set<Integer> apiLevels = new HashSet<Integer>(); - if (installCommon) { - apiLevels.add(8); - } - if (installLatest) { - apiLevels.add(AdtUpdateDialog.USE_MAX_REMOTE_API_LEVEL); + if (mWelcomePage != null) { + // Read out wizard settings immediately; we will perform the actual work + // after the wizard window has been taken down and it's too late to read the + // settings then + final File path = mWelcomePage.getPath(); + final boolean installCommon = mWelcomePage.isInstallCommon(); + final boolean installLatest = mWelcomePage.isInstallLatest(); + final boolean createNew = mWelcomePage.isCreateNew(); + + // Perform installation asynchronously since it takes a while. + getShell().getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + if (createNew) { + try { + Set<Integer> apiLevels = new HashSet<Integer>(); + if (installCommon) { + apiLevels.add(8); + } + if (installLatest) { + apiLevels.add(AdtUpdateDialog.USE_MAX_REMOTE_API_LEVEL); + } + installSdk(path, apiLevels); + } catch (Exception e) { + AdtPlugin.logAndPrintError(e, "ADT Welcome Wizard", + "Installation failed"); } - installSdk(path, apiLevels); - } catch (Exception e) { - AdtPlugin.logAndPrintError(e, "ADT Welcome Wizard", "Installation failed"); } - } - // Set SDK path after installation since this will trigger a SDK refresh. - AdtPrefs.getPrefs().setSdkLocation(path); - } - }); + // Set SDK path after installation since this will trigger a SDK refresh. + AdtPrefs.getPrefs().setSdkLocation(path); + } + }); + } // The wizard always succeeds, even if installation fails or is aborted return true; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java index 35ef1c7..bcee887 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/welcome/WelcomeWizardPage.java @@ -99,7 +99,7 @@ public class WelcomeWizardPage extends WizardPage implements ModifyListener, Sel mInstallCommonCheckbox = new Button(container, SWT.CHECK); mInstallCommonCheckbox.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); - mInstallCommonCheckbox.setText("Install Android 2.2, a version which is supported by ~93% phones and tablets"); + mInstallCommonCheckbox.setText("Install Android 2.2, a version which is supported by ~96% phones and tablets"); mInstallCommonCheckbox.addSelectionListener(this); new Label(container, SWT.NONE); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java index d7ace9a..62090d4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java @@ -18,6 +18,7 @@ package com.android.ide.eclipse.adt.internal.wizards.export; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; import com.android.ide.eclipse.adt.internal.project.ExportHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; @@ -271,6 +272,14 @@ public final class ExportWizard extends Wizard implements IExportWizard { if (entry != null) { mPrivateKey = entry.getPrivateKey(); mCertificate = (X509Certificate)entry.getCertificate(); + + AdtPlugin.printToConsole(mProject, + String.format("New keystore %s has been created.", + mDestinationFile.getAbsolutePath()), + "Certificate fingerprints:", + String.format(" MD5 : %s", getCertMd5Fingerprint()), + String.format(" SHA1: %s", getCertSha1Fingerprint())); + } else { // this really shouldn't happen since we now let the user choose the key // from a list read from the store. @@ -334,7 +343,8 @@ public final class ExportWizard extends Wizard implements IExportWizard { /* * (non-Javadoc) - * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection) + * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, + * org.eclipse.jface.viewers.IStructuredSelection) */ @Override public void init(IWorkbench workbench, IStructuredSelection selection) { @@ -478,6 +488,14 @@ public final class ExportWizard extends Wizard implements IExportWizard { return mDName; } + String getCertSha1Fingerprint() { + return FingerprintUtils.getFingerprint(mCertificate, "SHA1"); + } + + String getCertMd5Fingerprint() { + return FingerprintUtils.getFingerprint(mCertificate, "MD5"); + } + void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { mPrivateKey = privateKey; mCertificate = certificate; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java index 39ab258..c17f43e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java @@ -57,6 +57,18 @@ import java.util.Calendar; */ final class KeyCheckPage extends ExportWizardPage { + private static final int REQUIRED_YEARS = 25; + + private static final String VALIDITY_WARNING = + "<p>Make sure the certificate is valid for the planned lifetime of the product.</p>" + + "<p>If the certificate expires, you will be forced to sign your application with " + + "a different one.</p>" + + "<p>Applications cannot be upgraded if their certificate changes from " + + "one version to another, forcing a full uninstall/install, which will make " + + "the user lose his/her data.</p>" + + "<p>Google Play(Android Market) currently requires certificates to be valid " + + "until 2033.</p>"; + private final ExportWizard mWizard; private PrivateKey mPrivateKey; private X509Certificate mCertificate; @@ -169,12 +181,8 @@ final class KeyCheckPage extends ExportWizardPage { String.format("<p>Certificate expires in %d years.</p>", validity)); - if (validity < 25) { - sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"); - sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>"); - sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, "); - sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>"); - sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>"); + if (validity < REQUIRED_YEARS) { + sb.append(VALIDITY_WARNING); } mKeyDetails = sb.toString(); @@ -239,7 +247,7 @@ final class KeyCheckPage extends ExportWizardPage { int expirationYear = expirationCalendar.get(Calendar.YEAR); int thisYear = today.get(Calendar.YEAR); - if (thisYear + 25 < expirationYear) { + if (thisYear + REQUIRED_YEARS < expirationYear) { // do nothing } else { if (expirationYear == thisYear) { @@ -250,14 +258,19 @@ final class KeyCheckPage extends ExportWizardPage { "<p>The Certificate expires in %1$s %2$s.</p>", count, count == 1 ? "year" : "years")); } - - sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"); - sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>"); - sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, "); - sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>"); - sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>"); + sb.append(VALIDITY_WARNING); } + // show certificate fingerprints + String sha1 = mWizard.getCertSha1Fingerprint(); + String md5 = mWizard.getCertMd5Fingerprint(); + + sb.append("<p></p>" /*blank line*/); + sb.append("<p>Certificate fingerprints:</p>"); + sb.append(String.format("<li>MD5 : %s</li>", md5)); + sb.append(String.format("<li>SHA1: %s</li>", sha1)); + sb.append("<p></p>" /*blank line*/); + mKeyDetails = sb.toString(); } } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java index f503c1e..6e63107 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportPage.java @@ -24,18 +24,24 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.IMessageProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.ICheckStateListener; -import org.eclipse.jface.viewers.IColorProvider; -import org.eclipse.jface.viewers.ILabelProvider; -import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.wizard.IWizardPage; import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.SelectionEvent; @@ -43,7 +49,7 @@ import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; @@ -52,6 +58,7 @@ import org.eclipse.swt.widgets.DirectoryDialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkingSet; @@ -63,7 +70,10 @@ import java.util.List; /** WizardPage for importing Android projects */ class ImportPage extends WizardPage implements SelectionListener, IStructuredContentProvider, - ICheckStateListener, ILabelProvider, IColorProvider, KeyListener, TraverseListener { + ICheckStateListener, KeyListener, TraverseListener, ControlListener { + private static final int DIR_COLUMN = 0; + private static final int NAME_COLUMN = 1; + private final NewProjectWizardState mValues; private List<ImportedProject> mProjectPaths; private final IProject[] mExistingProjects; @@ -120,15 +130,29 @@ class ImportPage extends WizardPage implements SelectionListener, IStructuredCon projectsLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 3, 1)); projectsLabel.setText("Projects:"); - mCheckboxTableViewer = CheckboxTableViewer.newCheckList(container, - SWT.BORDER | SWT.FULL_SELECTION); - mTable = mCheckboxTableViewer.getTable(); + mTable = new Table(container, SWT.CHECK); + mTable.setHeaderVisible(true); + mCheckboxTableViewer = new CheckboxTableViewer(mTable); + + TableViewerColumn dirViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn dirColumn = dirViewerColumn.getColumn(); + dirColumn.setWidth(200); + dirColumn.setText("Project to Import"); + TableViewerColumn nameViewerColumn = new TableViewerColumn(mCheckboxTableViewer, SWT.NONE); + TableColumn nameColumn = nameViewerColumn.getColumn(); + nameColumn.setWidth(200); + nameColumn.setText("New Project Name"); + nameViewerColumn.setEditingSupport(new ProjectNameEditingSupport(mCheckboxTableViewer)); + mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 4)); + mTable.setLinesVisible(true); + mTable.setHeaderVisible(true); mTable.addSelectionListener(this); - mCheckboxTableViewer.setLabelProvider(this); + mTable.addControlListener(this); mCheckboxTableViewer.setContentProvider(this); mCheckboxTableViewer.setInput(this); mCheckboxTableViewer.addCheckStateListener(this); + mCheckboxTableViewer.setLabelProvider(new ProjectCellLabelProvider()); mSelectAllButton = new Button(container, SWT.NONE); mSelectAllButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 1, 1)); @@ -153,6 +177,21 @@ class ImportPage extends WizardPage implements SelectionListener, IStructuredCon Composite group = mWorkingSetGroup.createControl(container); group.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1)); + + updateColumnWidths(); + } + + private void updateColumnWidths() { + Rectangle r = mTable.getClientArea(); + int availableWidth = r.width; + // Add all available size to the first column + for (int i = 1; i < mTable.getColumnCount(); i++) { + TableColumn column = mTable.getColumn(i); + availableWidth -= column.getWidth(); + } + if (availableWidth > 100) { + mTable.getColumn(0).setWidth(availableWidth); + } } @Override @@ -196,21 +235,25 @@ class ImportPage extends WizardPage implements SelectionListener, IStructuredCon private List<ImportedProject> searchForProjects(File dir) { List<ImportedProject> projects = new ArrayList<ImportedProject>(); - addProjects(dir, projects); + addProjects(dir, projects, dir.getPath().length() + 1); return projects; } /** Finds all project directories under the given directory */ - private void addProjects(File dir, List<ImportedProject> projects) { + private void addProjects(File dir, List<ImportedProject> projects, int prefixLength) { if (dir.isDirectory()) { if (LintUtils.isProjectDir(dir)) { - projects.add(new ImportedProject(dir)); + String relative = dir.getPath(); + if (relative.length() > prefixLength) { + relative = relative.substring(prefixLength); + } + projects.add(new ImportedProject(dir, relative)); } File[] children = dir.listFiles(); if (children != null) { for (File child : children) { - addProjects(child, projects); + addProjects(child, projects, prefixLength); } } } @@ -234,6 +277,21 @@ class ImportPage extends WizardPage implements SelectionListener, IStructuredCon String.format("Cannot import %1$s because the project name is in use", project.getProjectName())); break; + } else { + status = ProjectNamePage.validateProjectName(project.getProjectName()); + if (status != null && !status.isOK()) { + // Need to insert project name to make it clear which project name + // is in violation + if (mValues.importProjects.size() > 1) { + String message = String.format("%1$s: %2$s", + project.getProjectName(), status.getMessage()); + status = new Status(status.getSeverity(), AdtPlugin.PLUGIN_ID, + message); + } + break; + } else { + status = null; // Don't leave non null status with isOK() == true + } } } } @@ -365,48 +423,85 @@ class ImportPage extends WizardPage implements SelectionListener, IStructuredCon } mValues.importProjects = selected; validatePage(); + + mCheckboxTableViewer.update(event.getElement(), null); } - // ---- Implements ILabelProvider ---- + // ---- Implements ControlListener ---- @Override - public void addListener(ILabelProviderListener listener) { + public void controlMoved(ControlEvent e) { } @Override - public void removeListener(ILabelProviderListener listener) { + public void controlResized(ControlEvent e) { + updateColumnWidths(); } - @Override - public boolean isLabelProperty(Object element, String property) { - return false; - } + private final class ProjectCellLabelProvider extends CellLabelProvider { + @Override + public void update(ViewerCell cell) { + Object element = cell.getElement(); + int index = cell.getColumnIndex(); + ImportedProject project = (ImportedProject) element; + + Display display = mTable.getDisplay(); + Color fg; + if (mCheckboxTableViewer.getGrayed(element)) { + fg = display.getSystemColor(SWT.COLOR_DARK_GRAY); + } else { + fg = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); + } + cell.setForeground(fg); + cell.setBackground(display.getSystemColor(SWT.COLOR_LIST_BACKGROUND)); + + switch (index) { + case DIR_COLUMN: { + // Directory name + cell.setText(project.getRelativePath()); + return; + } - @Override - public Image getImage(Object element) { - return null; + case NAME_COLUMN: { + // New name + cell.setText(project.getProjectName()); + return; + } + default: + assert false : index; + } + cell.setText(""); + } } - @Override - public String getText(Object element) { - ImportedProject file = (ImportedProject) element; - return String.format("%1$s (%2$s)", file.getProjectName(), file.getLocation().getPath()); - } + /** Editing support for the project name column */ + private class ProjectNameEditingSupport extends EditingSupport { + private ProjectNameEditingSupport(ColumnViewer viewer) { + super(viewer); + } - // ---- IColorProvider ---- + @Override + protected void setValue(Object element, Object value) { + ImportedProject project = (ImportedProject) element; + project.setProjectName(value.toString()); + mCheckboxTableViewer.update(element, null); + validatePage(); + } - @Override - public Color getForeground(Object element) { - Display display = mTable.getDisplay(); - if (mCheckboxTableViewer.getGrayed(element)) { - return display.getSystemColor(SWT.COLOR_DARK_GRAY); + @Override + protected Object getValue(Object element) { + ImportedProject project = (ImportedProject) element; + return project.getProjectName(); } - return display.getSystemColor(SWT.COLOR_LIST_FOREGROUND); - } + @Override + protected CellEditor getCellEditor(Object element) { + return new TextCellEditor(mTable); + } - @Override - public Color getBackground(Object element) { - return mTable.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND); + @Override + protected boolean canEdit(Object element) { + return true; + } } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java index 064aa34..74af651 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/ImportedProject.java @@ -15,27 +15,36 @@ */ package com.android.ide.eclipse.adt.internal.wizards.newproject; +import static com.android.SdkConstants.ATTR_NAME; + import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.xml.AndroidManifestParser; import com.android.ide.common.xml.ManifestData; import com.android.ide.common.xml.ManifestData.Activity; import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.io.FolderWrapper; import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.google.common.base.Charsets; +import com.google.common.io.Files; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import java.io.File; +import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -45,16 +54,22 @@ class ImportedProject { private String mActivityName; private ManifestData mManifest; private String mProjectName; + private String mRelativePath; - ImportedProject(File location) { + ImportedProject(File location, String relativePath) { super(); mLocation = location; + mRelativePath = relativePath; } File getLocation() { return mLocation; } + String getRelativePath() { + return mRelativePath; + } + @Nullable ManifestData getManifest() { if (mManifest == null) { @@ -104,6 +119,12 @@ class ImportedProject { @NonNull public String getProjectName() { if (mProjectName == null) { + // Are we importing an Eclipse project? If so just use the existing project name + mProjectName = findEclipseProjectName(); + if (mProjectName != null) { + return mProjectName; + } + String activityName = getActivityName(); if (activityName == null || activityName.isEmpty()) { // I could also look at the build files, say build.xml from ant, and @@ -136,6 +157,37 @@ class ImportedProject { return mProjectName; } + @Nullable + private String findEclipseProjectName() { + File projectFile = new File(mLocation, ".project"); //$NON-NLS-1$ + if (projectFile.exists()) { + String xml; + try { + xml = Files.toString(projectFile, Charsets.UTF_8); + Document doc = DomUtilities.parseDocument(xml, false); + if (doc != null) { + NodeList names = doc.getElementsByTagName(ATTR_NAME); + if (names.getLength() >= 1) { + Node nameElement = names.item(0); + String name = nameElement.getTextContent().trim(); + if (!name.isEmpty()) { + return name; + } + } + } + } catch (IOException e) { + // pass: don't attempt to read project name; must be some sort of unrelated + // file with the same name, perhaps from a different editor or IDE + } + } + + return null; + } + + public void setProjectName(@NonNull String newName) { + mProjectName = newName; + } + public IAndroidTarget getTarget() { // Pick a target: // First try to find the one requested by project.properties diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java index 2f7ad7a..eea9d36 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java @@ -23,18 +23,18 @@ import static org.eclipse.core.resources.IResource.DEPTH_ZERO; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.ide.common.resources.ValueResourceParser; import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.project.AndroidNature; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; -import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; @@ -522,10 +522,10 @@ public class NewProjectCreator { if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) { // The error indicates the file system is not case sensitive // and there's a resource with a similar name. - MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(), + MessageDialog.openError(AdtPlugin.getShell(), "Error", "Error: Case Variant Exists"); } else { - ErrorDialog.openError(AdtPlugin.getDisplay().getActiveShell(), + ErrorDialog.openError(AdtPlugin.getShell(), "Error", core.getMessage(), core.getStatus()); } } else { @@ -539,7 +539,7 @@ public class NewProjectCreator { if (msg == null) { msg = t.toString(); } - MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(), "Error", msg); + MessageDialog.openError(AdtPlugin.getShell(), "Error", msg); } e.printStackTrace(); } catch (InterruptedException e) { @@ -1058,7 +1058,7 @@ public class NewProjectCreator { String value = strings.get(key); // Escape values if necessary - value = ExtractStringRefactoring.escapeString(value); + value = ValueResourceParser.escapeResourceString(value); // place them in the template String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key); @@ -1088,8 +1088,8 @@ public class NewProjectCreator { /** Reformats the given contents with the current formatting settings */ private String reformat(XmlFormatStyle style, String contents) { if (AdtPrefs.getPrefs().getUseCustomXmlFormatter()) { - XmlFormatPreferences formatPrefs = XmlFormatPreferences.create(); - return XmlPrettyPrinter.prettyPrint(contents, formatPrefs, style, + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); + return EclipseXmlPrettyPrinter.prettyPrint(contents, formatPrefs, style, null /*lineSeparator*/); } else { return contents; @@ -1404,7 +1404,8 @@ public class NewProjectCreator { if (reformat) { // Guess the formatting style based on the file location - XmlFormatStyle style = XmlFormatStyle.getForFile(destFile.getProjectRelativePath()); + XmlFormatStyle style = EclipseXmlPrettyPrinter + .getForFile(destFile.getProjectRelativePath()); if (style != null) { template = reformat(style, template); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java index b1a0299..ce7e936 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/AddTranslationDialog.java @@ -26,6 +26,7 @@ import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.LocaleManager; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager; import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.resources.ResourceType; @@ -403,6 +404,7 @@ public class AddTranslationDialog extends Dialog implements ControlListener, Sel IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFolder folder = root.getFolder(parent.getFullPath()); manager.getResourceFolder(folder); + RenderPreviewManager.bumpRevision(); } catch (CoreException e) { AdtPlugin.log(e, null); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java index aec6b92..1d6467e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/ChooseConfigurationPage.java @@ -121,6 +121,8 @@ public class ChooseConfigurationPage extends WizardPage { }); setControl(composite); + + mConfigSelector.setConfiguration(mValues.configuration); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java index 68c7f4c..28fb8c0 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileCreationPage.java @@ -304,6 +304,7 @@ class NewXmlFileCreationPage extends WizardPage { if (SCROLL_VIEW.equals(root) || HORIZONTAL_SCROLL_VIEW.equals(root)) { return " <LinearLayout " //$NON-NLS-1$ + getDefaultAttrs(project, root).replace('\n', ' ') + + " android:orientation=\"vertical\"" //$NON-NLS-1$ + "></LinearLayout>\n"; //$NON-NLS-1$ } return null; @@ -733,6 +734,7 @@ class NewXmlFileCreationPage extends WizardPage { } String[] folderSegments = targetWsFolderPath.split(RES_QUALIFIER_SEP); if (folderSegments.length > 0) { + mValues.configuration = FolderConfiguration.getConfig(folderSegments); String folderName = folderSegments[0]; selectTypeFromFolder(folderName); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java index 22e2325..16cd7b3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newxmlfile/NewXmlFileWizard.java @@ -21,13 +21,14 @@ import static com.android.SdkConstants.GRID_LAYOUT; import com.android.SdkConstants; import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.IconFactory; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderPreviewManager; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.project.SupportLibraryHelper; @@ -249,14 +250,14 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { sb.append("</").append(root).append(">\n"); //$NON-NLS-1$ //$NON-NLS-2$ - XmlFormatPreferences formatPrefs = XmlFormatPreferences.create(); + EclipseXmlFormatPreferences formatPrefs = EclipseXmlFormatPreferences.create(); String fileContents; if (!autoFormat) { fileContents = sb.toString(); } else { - XmlFormatStyle style = XmlFormatStyle.getForFolderType(folderType); - fileContents = XmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs, - style, null /*lineSeparator*/); + XmlFormatStyle style = EclipseXmlPrettyPrinter.getForFolderType(folderType); + fileContents = EclipseXmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs, + style, null /*lineSeparator*/); } // Remove marker tokens and replace them with whitespace @@ -275,6 +276,15 @@ public class NewXmlFileWizard extends Wizard implements INewWizard { } file.create(stream, true /*force*/, null /*progress*/); IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null; + + // If you introduced a new locale, or new screen variations etc, ensure that + // the list of render previews is updated if necessary + if (file.getParent().getName().indexOf('-') != -1 + && (folderType == ResourceFolderType.LAYOUT + || folderType == ResourceFolderType.VALUES)) { + RenderPreviewManager.bumpRevision(); + } + return Pair.of(file, region); } catch (UnsupportedEncodingException e) { error = e.getMessage(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java index 9d2dd9a..ba4aedc 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/ActivityPage.java @@ -17,11 +17,13 @@ package com.android.ide.eclipse.adt.internal.wizards.templates; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_ACTIVITIES; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.CATEGORY_OTHER; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LAUNCHER; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_PADDING; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.PREVIEW_WIDTH; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageControl; +import com.google.common.collect.Lists; import com.google.common.io.Files; import org.eclipse.core.runtime.IStatus; @@ -61,6 +63,7 @@ class ActivityPage extends WizardPage implements SelectionListener { private Label mDescription; private boolean mOnlyActivities; private boolean mAskCreate; + private boolean mLauncherActivitiesOnly; /** * Create the wizard. @@ -84,6 +87,11 @@ class ActivityPage extends WizardPage implements SelectionListener { } } + /** Sets whether the activity page should only offer launcher activities */ + void setLauncherActivitiesOnly(boolean launcherActivitiesOnly) { + mLauncherActivitiesOnly = launcherActivitiesOnly; + } + @Override public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); @@ -108,18 +116,31 @@ class ActivityPage extends WizardPage implements SelectionListener { TemplateManager manager = mValues.template.getManager(); - mTemplates = manager.getTemplates(CATEGORY_ACTIVITIES); + java.util.List<File> templates = manager.getTemplates(CATEGORY_ACTIVITIES); + if (!mOnlyActivities) { - mTemplates.addAll(manager.getTemplates(CATEGORY_OTHER)); + templates.addAll(manager.getTemplates(CATEGORY_OTHER)); } - java.util.List<String> names = new ArrayList<String>(mTemplates.size()); + java.util.List<String> names = new ArrayList<String>(templates.size()); File current = mValues.activityValues.getTemplateLocation(); + mTemplates = Lists.newArrayListWithExpectedSize(templates.size()); int index = -1; - for (int i = 0, n = mTemplates.size(); i < n; i++) { - File template = mTemplates.get(i); - names.add(template.getName()); + for (int i = 0, n = templates.size(); i < n; i++) { + File template = templates.get(i); + TemplateMetadata metadata = manager.getTemplate(template); + if (metadata == null) { + continue; + } + if (mLauncherActivitiesOnly) { + Parameter parameter = metadata.getParameter(IS_LAUNCHER); + if (parameter == null) { + continue; + } + } + mTemplates.add(template); + names.add(metadata.getTitle()); if (template.equals(current)) { - index = i; + index = names.size(); } } String[] items = names.toArray(new String[names.size()]); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java index 7e5866e..ffcfa3e 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/FmEscapeXmlStringMethod.java @@ -15,7 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.wizards.templates; -import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring; +import com.android.ide.common.resources.ValueResourceParser; import freemarker.template.SimpleScalar; import freemarker.template.TemplateMethodModel; @@ -38,6 +38,6 @@ public class FmEscapeXmlStringMethod implements TemplateMethodModel { throw new TemplateModelException("Wrong arguments"); } String string = args.get(0).toString(); - return new SimpleScalar(ExtractStringRefactoring.escapeString(string)); + return new SimpleScalar(ValueResourceParser.escapeResourceString(string)); } }
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java index e698ac1..0b003f3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectPage.java @@ -546,9 +546,6 @@ public class NewProjectPage extends WizardPage Object source = e.getSource(); if (source == mMinSdkCombo) { mValues.minSdk = getSelectedMinSdk(); - // If higher than build target, adjust build target - // TODO: implement - Integer minSdk = mMinNameToApi.get(mValues.minSdk); if (minSdk == null) { try { @@ -559,6 +556,49 @@ public class NewProjectPage extends WizardPage } mValues.iconState.minSdk = minSdk.intValue(); mValues.minSdkLevel = minSdk.intValue(); + + // If higher than build target, adjust build target + if (mValues.minSdkLevel > mValues.getBuildApi()) { + // Try to find a build target with an adequate build API + IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData(); + IAndroidTarget best = null; + int bestApi = Integer.MAX_VALUE; + int bestTargetIndex = -1; + for (int i = 0; i < targets.length; i++) { + IAndroidTarget target = targets[i]; + if (!target.isPlatform()) { + continue; + } + int api = target.getVersion().getApiLevel(); + if (api >= mValues.minSdkLevel && api < bestApi) { + best = target; + bestApi = api; + bestTargetIndex = i; + } + } + + if (best != null) { + assert bestTargetIndex != -1; + mValues.target = best; + try { + mIgnore = true; + mBuildSdkCombo.select(bestTargetIndex); + } finally { + mIgnore = false; + } + } + } + + // If higher than targetSdkVersion, adjust targetSdkVersion + if (mValues.minSdkLevel > mValues.targetSdkLevel) { + mValues.targetSdkLevel = mValues.minSdkLevel; + try { + mIgnore = true; + setSelectedTargetSdk(mValues.targetSdkLevel); + } finally { + mIgnore = false; + } + } } else if (source == mBuildSdkCombo) { mValues.target = getSelectedBuildTarget(); @@ -614,6 +654,10 @@ public class NewProjectPage extends WizardPage mMinSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1 } + private void setSelectedTargetSdk(int api) { + mTargetSdkCombo.select(api - 1); // -1: First API level (at index 0) is 1 + } + @Nullable private IAndroidTarget getSelectedBuildTarget() { IAndroidTarget[] targets = (IAndroidTarget[]) mBuildSdkCombo.getData(); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java index 72e9985..84de9a4 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewProjectWizard.java @@ -19,6 +19,7 @@ import static org.eclipse.core.resources.IResource.DEPTH_INFINITE; import com.android.SdkConstants; import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; import com.android.assetstudiolib.GraphicGenerator; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; @@ -58,8 +59,9 @@ import java.util.Set; public class NewProjectWizard extends TemplateWizard { private static final String PARENT_ACTIVITY_CLASS = "parentActivityClass"; //$NON-NLS-1$ private static final String ACTIVITY_TITLE = "activityTitle"; //$NON-NLS-1$ - private static final String IS_LAUNCHER = "isLauncher"; //$NON-NLS-1$ + static final String IS_LAUNCHER = "isLauncher"; //$NON-NLS-1$ static final String IS_NEW_PROJECT = "isNewProject"; //$NON-NLS-1$ + static final String IS_LIBRARY_PROJECT = "isLibraryProject"; //$NON-NLS-1$ static final String ATTR_COPY_ICONS = "copyIcons"; //$NON-NLS-1$ static final String ATTR_TARGET_API = "targetApi"; //$NON-NLS-1$ static final String ATTR_MIN_API = "minApi"; //$NON-NLS-1$ @@ -98,6 +100,7 @@ public class NewProjectWizard extends TemplateWizard { mContentsPage = new ProjectContentsPage(mValues); mContentsPage.init(selection, AdtUtils.getActivePart()); mActivityPage = new ActivityPage(mValues, true, true); + mActivityPage.setLauncherActivitiesOnly(true); } @Override @@ -240,6 +243,16 @@ public class NewProjectWizard extends TemplateWizard { return mValues.template.getFilesToOpen(); } + @VisibleForTesting + NewProjectWizardState getValues() { + return mValues; + } + + @VisibleForTesting + void setValues(NewProjectWizardState values) { + mValues = values; + } + @Override protected List<Change> computeChanges() { final TemplateHandler template = mValues.template; @@ -374,6 +387,7 @@ public class NewProjectWizard extends TemplateWizard { addProjectInfo(parameters); parameters.put(IS_NEW_PROJECT, true); + parameters.put(IS_LIBRARY_PROJECT, mValues.isLibrary); // Ensure that activities created as part of a new project are marked as // launcher activities parameters.put(IS_LAUNCHER, true); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java index 1cf5daf..57cf5c8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplatePage.java @@ -811,7 +811,7 @@ public class NewTemplatePage extends WizardPage scope = SearchEngine.createJavaSearchScope(classes, IJavaSearchScope.SOURCES); } - Shell parent = AdtPlugin.getDisplay().getActiveShell(); + Shell parent = AdtPlugin.getShell(); final SelectionDialog dialog = JavaUI.createTypeDialog( parent, new ProgressMonitorDialog(parent), @@ -870,7 +870,6 @@ public class NewTemplatePage extends WizardPage p.type == Parameter.Type.SEPARATOR) { continue; } - p.suggest.indexOf(id); if (!p.suggest.contains(id)) { continue; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java index 1d1eb1d..805399b 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/NewTemplateWizardState.java @@ -22,6 +22,7 @@ import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectW import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_MIN_API_LEVEL; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_TARGET_API; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_LIBRARY_PROJECT; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.IS_NEW_PROJECT; import static com.android.ide.eclipse.adt.internal.wizards.templates.NewTemplateWizard.BLANK_ACTIVITY; @@ -30,6 +31,7 @@ import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.internal.assetstudio.ConfigureAssetSetPage; import com.android.ide.eclipse.adt.internal.assetstudio.CreateAssetSetWizardState; import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.sdklib.IAndroidTarget; @@ -155,6 +157,9 @@ public class NewTemplateWizardState { parameters.put(ATTR_TARGET_API, manifest.getTargetSdkVersion()); parameters.put(ATTR_BUILD_API, getBuildApi()); parameters.put(ATTR_COPY_ICONS, mIconState == null); + ProjectState projectState = Sdk.getProjectState(project); + parameters.put(IS_LIBRARY_PROJECT, + projectState != null ? projectState.isLibrary() : false); List<Change> changes = getTemplateHandler().render(project, parameters); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java index a9a3f33..3139451 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/Parameter.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.wizards.templates; +import static com.android.ide.eclipse.adt.internal.wizards.templates.NewProjectWizard.ATTR_PACKAGE_NAME; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_CONSTRAINTS; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_DEFAULT; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateHandler.ATTR_HELP; @@ -26,6 +27,8 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; import com.android.ide.eclipse.adt.internal.wizards.newproject.ApplicationInfoPage; import com.android.resources.ResourceFolderType; @@ -33,7 +36,10 @@ import com.android.resources.ResourceType; import com.google.common.base.Splitter; import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.swt.widgets.Control; @@ -146,6 +152,9 @@ class Parameter { } } + /** The template defining the parameter */ + public final TemplateMetadata template; + /** The type of parameter */ @NonNull public final Type type; @@ -208,7 +217,8 @@ class Parameter { /** Project associated with this validator */ private IProject mValidatorProject; - Parameter(@NonNull Element parameter) { + Parameter(@NonNull TemplateMetadata template, @NonNull Element parameter) { + this.template = template; element = parameter; String typeName = parameter.getAttribute(TemplateHandler.ATTR_TYPE); @@ -244,7 +254,12 @@ class Parameter { } } - Parameter(@NonNull Type type, @NonNull String id, @NonNull String initialValue) { + Parameter( + @NonNull TemplateMetadata template, + @NonNull Type type, + @NonNull String id, + @NonNull String initialValue) { + this.template = template; this.type = type; this.id = id; this.value = initialValue; @@ -265,7 +280,7 @@ class Parameter { } @Nullable - public IInputValidator getValidator(@Nullable IProject project) { + public IInputValidator getValidator(@Nullable final IProject project) { if (mNoValidator) { return null; } @@ -341,6 +356,37 @@ class Parameter { return status.getMessage(); } + // Uniqueness + if (project != null && constraints.contains(Constraint.UNIQUE)) { + try { + // Determine the package. + // If there is a package info + + IJavaProject p = BaseProjectHelper.getJavaProject(project); + if (p != null) { + String fqcn = newText; + if (fqcn.indexOf('.') == -1) { + String pkg = null; + Parameter parameter = template.getParameter( + ATTR_PACKAGE_NAME); + if (parameter != null && parameter.value != null) { + pkg = parameter.value.toString(); + } else { + pkg = ManifestInfo.get(project).getPackage(); + } + fqcn = pkg.isEmpty() ? newText : pkg + '.' + newText; + } + + IType t = p.findType(fqcn); + if (t != null && t.exists()) { + return String.format("%1$s already exists", newText); + } + } + } catch (CoreException e) { + AdtPlugin.log(e, null); + } + } + return null; } }; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java index 53b6c3c..bd9c0fa 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateHandler.java @@ -15,6 +15,7 @@ */ package com.android.ide.eclipse.adt.internal.wizards.templates; +import static com.android.SdkConstants.ATTR_PACKAGE; import static com.android.SdkConstants.DOT_AIDL; import static com.android.SdkConstants.DOT_FTL; import static com.android.SdkConstants.DOT_JAVA; @@ -24,18 +25,20 @@ import static com.android.SdkConstants.DOT_TXT; import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.EXT_XML; import static com.android.SdkConstants.FD_NATIVE_LIBS; +import static com.android.SdkConstants.XMLNS_PREFIX; import static com.android.ide.eclipse.adt.internal.wizards.templates.InstallDependencyPage.SUPPORT_LIBRARY_NAME; import static com.android.ide.eclipse.adt.internal.wizards.templates.TemplateManager.getTemplateRootFolder; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.ide.common.xml.XmlFormatStyle; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.actions.AddSupportJarAction; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle; -import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences; +import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback; @@ -58,6 +61,7 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; @@ -66,6 +70,7 @@ import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.ToolFactory; import org.eclipse.jdt.core.formatter.CodeFormatter; import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.NullChange; @@ -77,8 +82,10 @@ import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.osgi.framework.Constants; import org.osgi.framework.Version; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.Attributes; @@ -95,6 +102,7 @@ import java.io.Writer; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -389,6 +397,14 @@ class TemplateHandler { } } + /** + * Most recent thrown exception during template instantiation. This should + * basically always be null. Used by unit tests to see if any template + * instantiation recorded a failure. + */ + @VisibleForTesting + public static Exception sMostRecentException; + /** Read the given FreeMarker file and process the variable definitions */ private void processVariables(final Configuration freemarker, String file, final Map<String, Object> paramMap) { @@ -469,6 +485,7 @@ class TemplateHandler { } }); } catch (Exception e) { + sMostRecentException = e; AdtPlugin.log(e, null); } } @@ -579,12 +596,14 @@ class TemplateHandler { System.err.println("WARNING: Unknown template directive " + name); } } catch (Exception e) { + sMostRecentException = e; AdtPlugin.log(e, null); } } }); } catch (Exception e) { + sMostRecentException = e; AdtPlugin.log(e, null); } } @@ -657,26 +676,28 @@ class TemplateHandler { } } - Document currentManifest = DomUtilities.parseStructuredDocument(currentXml); + Document currentDocument = DomUtilities.parseStructuredDocument(currentXml); + assert currentDocument != null : currentXml; Document fragment = DomUtilities.parseStructuredDocument(xml); + assert fragment != null : xml; XmlFormatStyle formatStyle = XmlFormatStyle.MANIFEST; boolean modified; boolean ok; String fileName = to.getName(); if (fileName.equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) { - modified = ok = mergeManifest(currentManifest, fragment); + modified = ok = mergeManifest(currentDocument, fragment); } else { // Merge plain XML files String parentFolderName = to.getParent().getName(); ResourceFolderType folderType = ResourceFolderType.getFolderType(parentFolderName); if (folderType != null) { - formatStyle = XmlFormatStyle.getForFile(toPath); + formatStyle = EclipseXmlPrettyPrinter.getForFile(toPath); } else { formatStyle = XmlFormatStyle.FILE; } - modified = mergeResourceFile(currentManifest, fragment, folderType, paramMap); + modified = mergeResourceFile(currentDocument, fragment, folderType, paramMap); ok = true; } @@ -684,11 +705,9 @@ class TemplateHandler { String contents = null; if (ok) { if (modified) { - XmlPrettyPrinter printer = new XmlPrettyPrinter( - XmlFormatPreferences.create(), formatStyle, null); - StringBuilder sb = new StringBuilder(2 ); - printer.prettyPrint(-1, currentManifest, null, null, sb, false /*openTagOnly*/); - contents = sb.toString(); + contents = EclipseXmlPrettyPrinter.prettyPrint(currentDocument, + EclipseXmlFormatPreferences.create(), formatStyle, null, + currentXml.endsWith("\n")); //$NON-NLS-1$ } } else { // Just insert into file along with comment, using the "standard" conflict @@ -714,10 +733,22 @@ class TemplateHandler { /** Merges the given resource file contents into the given resource file * @param paramMap */ - private boolean mergeResourceFile(Document currentManifest, Document fragment, + private static boolean mergeResourceFile(Document currentDocument, Document fragment, ResourceFolderType folderType, Map<String, Object> paramMap) { boolean modified = false; + // Copy namespace declarations + NamedNodeMap attributes = fragment.getDocumentElement().getAttributes(); + if (attributes != null) { + for (int i = 0, n = attributes.getLength(); i < n; i++) { + Attr attribute = (Attr) attributes.item(i); + if (attribute.getName().startsWith(XMLNS_PREFIX)) { + currentDocument.getDocumentElement().setAttribute(attribute.getName(), + attribute.getValue()); + } + } + } + // For layouts for example, I want to *append* inside the root all the // contents of the new file. // But for resources for example, I want to combine elements which specify @@ -733,8 +764,9 @@ class TemplateHandler { nodes.add(child); root.removeChild(child); } + Collections.reverse(nodes); - root = currentManifest.getDocumentElement(); + root = currentDocument.getDocumentElement(); if (folderType == ResourceFolderType.VALUES) { // Try to merge items of the same name @@ -794,15 +826,31 @@ class TemplateHandler { } /** Merges the given manifest fragment into the given manifest file */ - private boolean mergeManifest(Document currentManifest, Document fragment) { + private static boolean mergeManifest(Document currentManifest, Document fragment) { // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create // and maintain error markers. + + // Transfer package element from manifest to merged in root; required by + // manifest merger + Element fragmentRoot = fragment.getDocumentElement(); + Element manifestRoot = currentManifest.getDocumentElement(); + if (fragmentRoot == null || manifestRoot == null) { + return false; + } + String pkg = fragmentRoot.getAttribute(ATTR_PACKAGE); + if (pkg == null || pkg.isEmpty()) { + pkg = manifestRoot.getAttribute(ATTR_PACKAGE); + if (pkg != null && !pkg.isEmpty()) { + fragmentRoot.setAttribute(ATTR_PACKAGE, pkg); + } + } + ManifestMerger merger = new ManifestMerger( MergerLog.wrapSdkLog(AdtPlugin.getDefault()), - new AdtManifestMergeCallback()); + new AdtManifestMergeCallback()).setExtractPackagePrefix(true); return currentManifest != null && - fragment != null && - merger.process(currentManifest, fragment); + fragment != null && + merger.process(currentManifest, fragment); } /** @@ -856,7 +904,7 @@ class TemplateHandler { contents = format(mProject, contents, to); IFile targetFile = getTargetFile(to); - TextFileChange change = createTextChange(targetFile); + TextFileChange change = createNewFileChange(targetFile); MultiTextEdit rootEdit = new MultiTextEdit(); rootEdit.addChild(new InsertEdit(0, contents)); change.setEdit(rootEdit); @@ -867,9 +915,9 @@ class TemplateHandler { private static String format(IProject project, String contents, IPath to) { String name = to.lastSegment(); if (name.endsWith(DOT_XML)) { - XmlFormatStyle formatStyle = XmlFormatStyle.getForFile(to); - XmlFormatPreferences prefs = XmlFormatPreferences.create(); - return XmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null); + XmlFormatStyle formatStyle = EclipseXmlPrettyPrinter.getForFile(to); + EclipseXmlFormatPreferences prefs = EclipseXmlFormatPreferences.create(); + return EclipseXmlPrettyPrinter.prettyPrint(contents, prefs, formatStyle, null); } else if (name.endsWith(DOT_JAVA)) { Map<?, ?> options = null; if (project != null && project.isAccessible()) { @@ -908,7 +956,7 @@ class TemplateHandler { return contents; } - private static TextFileChange createTextChange(IFile targetFile) { + private static TextFileChange createNewFileChange(IFile targetFile) { String fileName = targetFile.getName(); String message; if (targetFile.exists()) { @@ -917,7 +965,29 @@ class TemplateHandler { message = String.format("Create %1$s", fileName); } - TextFileChange change = new TextFileChange(message, targetFile); + TextFileChange change = new TextFileChange(message, targetFile) { + @Override + protected IDocument acquireDocument(IProgressMonitor pm) throws CoreException { + IDocument document = super.acquireDocument(pm); + + // In our case, we know we *always* use this TextFileChange + // to *create* files, we're not appending to existing files. + // However, due to the following bug we can end up with cached + // contents of previously deleted files that happened to have the + // same file name: + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=390402 + // Therefore, as a workaround, wipe out the cached contents here + if (document.getLength() > 0) { + try { + document.replace(0, document.getLength(), ""); + } catch (BadLocationException e) { + // pass + } + } + + return document; + } + }; change.setTextType(fileName.substring(fileName.lastIndexOf('.') + 1)); return change; } @@ -989,7 +1059,7 @@ class TemplateHandler { String newFile = Files.toString(src, Charsets.UTF_8); newFile = format(mProject, newFile, path); - TextFileChange addFile = createTextChange(file); + TextFileChange addFile = createNewFileChange(file); addFile.setEdit(new InsertEdit(0, newFile)); mTextChanges.add(addFile); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java index 13c3f00..cb184b8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/templates/TemplateMetadata.java @@ -86,7 +86,7 @@ class TemplateMetadata { mParameterMap = new HashMap<String, Parameter>(parameters.getLength()); for (int index = 0, max = parameters.getLength(); index < max; index++) { Element element = (Element) parameters.item(index); - Parameter parameter = new Parameter(element); + Parameter parameter = new Parameter(this, element); mParameters.add(parameter); if (parameter.id != null) { mParameterMap.put(parameter.id, parameter); @@ -221,7 +221,7 @@ class TemplateMetadata { if (mIconState.outputName != null) { // Register parameter such that if it is referencing other values, it gets // updated when other values are edited - Parameter outputParameter = new Parameter( + Parameter outputParameter = new Parameter(this, Parameter.Type.STRING, "_iconname", mIconState.outputName); //$NON-NLS-1$ getParameters().add(outputParameter); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties index b37b809..80a2f45 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties @@ -1,7 +1,7 @@ Could_Not_Find_Folder=Could not find SDK folder '%1$s'. Could_Not_Find_Folder_In_SDK=Could not find folder '%1$s' inside SDK '%2$s'. Could_Not_Find=Could not find %1$s\! -VersionCheck_Tools_Too_Old=This version of ADT requires Android SDK Tools in revision %1$s or above.\n\nCurrent revision is %2$s.\n\nPlease update your SDK Tools to the latest version. +VersionCheck_Tools_Too_Old=This version of ADT requires Android SDK Tools revision %1$s or above.\n\nCurrent revision is %2$s.\n\nPlease update your SDK Tools to the latest version. VersionCheck_Plugin_Version_Failed=Failed to get the required ADT version number from the SDK.\n\nThe Android Developer Toolkit may not work properly. VersionCheck_Unable_To_Parse_Version_s=Unable to parse sdk build version: %1$s VersionCheck_Plugin_Too_Old=This Android SDK requires Android Developer Toolkit version %1$d.%2$d.%3$d or above.\n\nCurrent version is %4$s.\n\nPlease update ADT to the latest version. |