aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Norbye <tnorbye@google.com>2011-02-14 14:41:07 -0800
committerTor Norbye <tnorbye@google.com>2011-02-17 11:37:46 -0800
commit8dc4366bbaad39d56e1c2ded4046c86a95a17666 (patch)
tree1cf8cdd65f7060024695571f1a21824d3f1cabf5
parentfda24dc116696fe937142b3a5819977cdab50d03 (diff)
downloadsdk-8dc4366bbaad39d56e1c2ded4046c86a95a17666.zip
sdk-8dc4366bbaad39d56e1c2ded4046c86a95a17666.tar.gz
sdk-8dc4366bbaad39d56e1c2ded4046c86a95a17666.tar.bz2
Refactoring: Wrap in Container, Change Layout Type, Extract
This changeset adds refactoring support for 3 visual refactoring operations: (1) Wrap in Container. This can be invoked on one or more sibling views (or the root view) to add a new layout container into the hierarchy which "wraps" the views. The refactoring can also update the layout attributes (such as RelativeLayout attachments) such that they refer to the new container instead. If invoked on the root element, the namespace declarations are migrated from the old root to the new root. (2) Change Layout. This can be invoked on a layout view to change the type of layout. In addition to editing the XML type declaration, it also removes layout parameters that no longer apply, and depending on which layout you are converting from and converting to, it may attempt to perform some translation to preserve the layout characteristics. In particular, if you convert from a LinearLayout to a RelativeLayout, then it will use RelativeLayout params to emulate the old LinearLayout by attaching items below (for vertical layouts) or to the right of (for horizontal layout) the previous sibling, and if the baseline property was set on the LinearLayout it will also add baseline constraints on the RelativeLayout. (It also adds default ids on any elements that need it.) There is a LOT more we can do to support layout transformations; this is just a beginning. (3) Extract as Include. We already had this feature, but it performed its own XML document manipulation. This code has been rewritten to use the Eclipse refactoring support (which the other two refactorings are implemented to use as well), which among other things means that you get Preview support - you can press Preview from the refactoring dialog to see the edits before they are made. This rewrite is also necessary to support an upcoming feature: the ability to replace occurrences in other configuration-variations of this layout; for that we need to ability to do multi-file changes which the refactoring support is ideal for. Change-Id: I50b142645f14c29c798fc02df6df69bad5b9426c
-rw-r--r--eclipse/dictionary.txt1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java15
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidContentAssist.java3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/AndroidXmlEditor.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java288
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java20
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExtractIncludeAction.java839
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GraphicalEditorPart.java2
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionManager.java2
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gre/RulesEngine.java3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java410
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java104
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java507
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java135
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java919
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java185
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java387
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java226
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java32
31 files changed, 3551 insertions, 931 deletions
diff --git a/eclipse/dictionary.txt b/eclipse/dictionary.txt
index 235e9f2..8246992 100644
--- a/eclipse/dictionary.txt
+++ b/eclipse/dictionary.txt
@@ -204,6 +204,7 @@ translucency
ui
uncomment
undescribed
+undoable
unhide
unicode
uninstall
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
index 07c59b1..fe15099 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -652,6 +652,33 @@
style="push"
tooltip="Extracts a string into Android resource string">
</action>
+ <action
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction"
+ definitionId="com.android.ide.eclipse.adt.refactoring.extract.include"
+ id="com.android.ide.eclipse.adt.actions.ExtractInclude"
+ label="Extract as Include..."
+ menubarPath="org.eclipse.jdt.ui.refactoring.menu/com.android.ide.eclipse.adt.refactoring.menu/android"
+ style="push"
+ tooltip="Extracts Views as Included Layout">
+ </action>
+ <action
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction"
+ definitionId="com.android.ide.eclipse.adt.refactoring.wrapin"
+ id="com.android.ide.eclipse.adt.actions.WrapIn"
+ label="Wrap In Container..."
+ menubarPath="org.eclipse.jdt.ui.refactoring.menu/com.android.ide.eclipse.adt.refactoring.menu/android"
+ style="push"
+ tooltip="Wraps Views in a new container">
+ </action>
+ <action
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction"
+ definitionId="com.android.ide.eclipse.adt.refactoring.convert"
+ id="com.android.ide.eclipse.adt.actions.ChangeLayout"
+ label="Change Layout..."
+ menubarPath="org.eclipse.jdt.ui.refactoring.menu/com.android.ide.eclipse.adt.refactoring.menu/android"
+ style="push"
+ tooltip="Changes layouts from one type to another">
+ </action>
<menu
id="org.eclipse.jdt.ui.refactoring.menu"
label="Refactor">
@@ -761,6 +788,24 @@
id="com.android.ide.eclipse.adt.refactoring.extract.string"
name="Extract Android String">
</command>
+ <command
+ categoryId="com.android.ide.eclipse.adt.refactoring.category"
+ description="Extract Views as Included Layout"
+ id="com.android.ide.eclipse.adt.refactoring.extract.include"
+ name="Extract as Include">
+ </command>
+ <command
+ categoryId="com.android.ide.eclipse.adt.refactoring.category"
+ description="Wraps Views in a New Container"
+ id="com.android.ide.eclipse.adt.refactoring.wrapin"
+ name="Wrap in Container">
+ </command>
+ <command
+ categoryId="com.android.ide.eclipse.adt.refactoring.category"
+ description="Converts Layouts from One Type to Another"
+ id="com.android.ide.eclipse.adt.refactoring.convert"
+ name="Change Layout">
+ </command>
</extension>
<extension
point="org.eclipse.ltk.core.refactoring.refactoringContributions">
@@ -768,6 +813,18 @@
class="com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringContribution"
id="com.android.ide.eclipse.adt.refactoring.extract.string">
</contribution>
+ <contribution
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeContribution"
+ id="com.android.ide.eclipse.adt.refactoring.extract.include">
+ </contribution>
+ <contribution
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInContribution"
+ id="com.android.ide.eclipse.adt.refactoring.wrapin">
+ </contribution>
+ <contribution
+ class="com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutContribution"
+ id="com.android.ide.eclipse.adt.refactoring.convert">
+ </contribution>
</extension>
<extension
point="org.eclipse.core.expressions.propertyTesters">
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java
index ee5a8c9..2ba2104 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LayoutConstants.java
@@ -38,6 +38,8 @@ public class LayoutConstants {
public static final String RELATIVE_LAYOUT = "RelativeLayout"; //$NON-NLS-1$
public static final String LINEAR_LAYOUT = "LinearLayout"; //$NON-NLS-1$
public static final String ABSOLUTE_LAYOUT = "AbsoluteLayout"; //$NON-NLS-1$
+ public static final String TABLE_LAYOUT = "TableLayout"; //$NON-NLS-1$
+ public static final String TABLE_ROW = "TableRow"; //$NON-NLS-1$
public static final String LIST_VIEW = "ListView"; //$NON-NLS-1$
public static final String GALLERY = "Gallery"; //$NON-NLS-1$
public static final String GRID_VIEW = "GridView"; //$NON-NLS-1$
@@ -84,6 +86,7 @@ public class LayoutConstants {
public static final String VALUE_WRAP_CONTENT = "wrap_content"; //$NON-NLS-1$
public static final String VALUE_FILL_PARENT = "fill_parent"; //$NON-NLS-1$
public static final String VALUE_TRUE = "true"; //$NON-NLS-1$
+ public static final String VALUE_FALSE= "false"; //$NON-NLS-1$
public static final String VALUE_N_DIP = "%ddip"; //$NON-NLS-1$
public static final String VALUE_CENTER_VERTICAL = "centerVertical"; //$NON-NLS-1$
@@ -114,18 +117,30 @@ public class LayoutConstants {
*/
public static final String ANDROID_URI = SdkConstants.NS_RESOURCES;
+ /**
+ * The package name where the widgets live (the ones that require no prefix in layout
+ * files)
+ */
+ public static final String ANDROID_WIDGET_PREFIX = "android.widget."; //$NON-NLS-1$
+
/** The fully qualified class name of an EditText view */
public static final String FQCN_EDIT_TEXT = "android.widget.EditText"; //$NON-NLS-1$
/** The fully qualified class name of a LinearLayout view */
public static final String FQCN_LINEAR_LAYOUT = "android.widget.LinearLayout"; //$NON-NLS-1$
+ /** The fully qualified class name of a RelativeLayout view */
+ public static final String FQCN_RELATIVE_LAYOUT = "android.widget.RelativeLayout"; //$NON-NLS-1$
+
/** The fully qualified class name of a FrameLayout view */
public static final String FQCN_FRAME_LAYOUT = "android.widget.FrameLayout"; //$NON-NLS-1$
/** The fully qualified class name of a TableRow view */
public static final String FQCN_TABLE_ROW = "android.widget.TableRow"; //$NON-NLS-1$
+ /** The fully qualified class name of a TableLayout view */
+ public static final String FQCN_TABLE_LAYOUT = "android.widget.TableLayout"; //$NON-NLS-1$
+
/** The fully qualified class name of a TabWidget view */
public static final String FQCN_TAB_WIDGET = "android.widget.TabWidget"; //$NON-NLS-1$
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 93acb4f..e1d030f 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
@@ -25,6 +25,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.SeparatorAttribu
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.TextValueDescriptor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiAttributeNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiFlagAttributeNode;
@@ -762,7 +763,7 @@ public abstract class AndroidContentAssist implements IContentAssistProcessor {
* Returns the XML DOM node corresponding to the given offset of the given document.
*/
public static Node getNode(ITextViewer viewer, int offset) {
- return AndroidXmlEditor.getNode(viewer.getDocument(), offset);
+ return DomUtilities.getNode(viewer.getDocument(), offset);
}
/**
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 89edee4..6d42fdb 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
@@ -669,39 +669,6 @@ public abstract class AndroidXmlEditor extends FormEditor implements IResourceCh
}
/**
- * Returns the XML DOM node corresponding to the given offset of the given
- * document.
- *
- * @param document The document to look in
- * @param offset The offset to look up the node for
- * @return The node containing the offset, or null
- */
- @SuppressWarnings("restriction") // No replacement for restricted XML model yet
- public static Node getNode(IDocument document, int offset) {
- Node node = null;
- IModelManager modelManager = StructuredModelManager.getModelManager();
- if (modelManager == null) {
- return null;
- }
- try {
- IStructuredModel model = modelManager.getExistingModelForRead(document);
- if (model != null) {
- try {
- for (; offset >= 0 && node == null; --offset) {
- node = (Node) model.getIndexedRegion(offset);
- }
- } finally {
- model.releaseFromRead();
- }
- }
- } catch (Exception e) {
- // Ignore exceptions.
- }
-
- return node;
- }
-
- /**
* Returns a version of the model that has been shared for read.
* <p/>
* Callers <em>must</em> call model.releaseFromRead() when done, typically
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
index 817d4cb..970e93c 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtils.java
@@ -357,12 +357,12 @@ public final class DescriptorsUtils {
* @return The capitalized string
*/
public static String capitalize(String str) {
- if (str == null || str.length() < 1 || str.charAt(0) < 'a' || str.charAt(0) > 'z') {
+ if (str == null || str.length() < 1 || Character.isUpperCase(str.charAt(0))) {
return str;
}
StringBuilder sb = new StringBuilder();
- sb.append((char)(str.charAt(0) + 'A' - 'a'));
+ sb.append(Character.toUpperCase(str.charAt(0)));
sb.append(str.substring(1));
return sb.toString();
}
@@ -891,33 +891,4 @@ public final class DescriptorsUtils {
return false;
}
-
- /**
- * Converts the given attribute value to an XML-attribute-safe value, meaning that
- * single and double quotes are replaced with their corresponding XML entities.
- *
- * @param attrValue the value to be escaped
- * @return the escaped value
- */
- public static String toXmlAttributeValue(String attrValue) {
- // Must escape ' and "
- if (attrValue.indexOf('"') == -1 && attrValue.indexOf('\'') == -1) {
- return attrValue;
- }
-
- int n = attrValue.length();
- StringBuilder sb = new StringBuilder(2 * n);
- for (int i = 0; i < n; i++) {
- char c = attrValue.charAt(i);
- if (c == '"') {
- sb.append("&quot;"); //$NON-NLS-1$
- } else if (c == '\'') {
- sb.append("&apos;"); //$NON-NLS-1$
- } else {
- sb.append(c);
- }
- }
-
- return sb.toString();
- }
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java
index 70e062a..6be764e 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutEditor.java
@@ -25,6 +25,7 @@ import com.android.ide.eclipse.adt.internal.editors.descriptors.IUnknownDescript
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.CustomViewDescriptorService;
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.DomUtilities;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.OutlinePage;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.PropertySheetPage;
@@ -351,7 +352,7 @@ public class LayoutEditor extends AndroidXmlEditor implements IShowEditorInput,
ISourceViewer textViewer = getStructuredSourceViewer();
int caretOffset = textViewer.getTextWidget().getCaretOffset();
if (caretOffset >= 0) {
- Node node = AndroidXmlEditor.getNode(textViewer.getDocument(), caretOffset);
+ Node node = DomUtilities.getNode(textViewer.getDocument(), caretOffset);
if (node != null && mGraphicalEditor != null) {
mGraphicalEditor.select(node);
}
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
new file mode 100644
index 0000000..f056ea5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilities.java
@@ -0,0 +1,288 @@
+/*
+ * 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.layout.gle2;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.util.Pair;
+
+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.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@SuppressWarnings("restriction") // No replacement for restricted XML model yet
+public class DomUtilities {
+ /**
+ * Returns the XML DOM node corresponding to the given offset of the given
+ * document.
+ *
+ * @param document The document to look in
+ * @param offset The offset to look up the node for
+ * @return The node containing the offset, or null
+ */
+ public static Node getNode(IDocument document, int offset) {
+ Node node = null;
+ IModelManager modelManager = StructuredModelManager.getModelManager();
+ if (modelManager == null) {
+ return null;
+ }
+ try {
+ IStructuredModel model = modelManager.getExistingModelForRead(document);
+ if (model != null) {
+ try {
+ for (; offset >= 0 && node == null; --offset) {
+ node = (Node) model.getIndexedRegion(offset);
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return node;
+ }
+
+ /**
+ * Like {@link #getNode(IDocument, int)}, but has a bias parameter which lets you
+ * indicate whether you want the search to look forwards or backwards.
+ * This is vital when trying to compute a node range. Consider the following
+ * XML fragment:
+ * {@code
+ * <a/><b/>[<c/><d/><e/>]<f/><g/>
+ * }
+ * Suppose we want to locate the nodes in the range indicated by the brackets above.
+ * If we want to search for the node corresponding to the start position, should
+ * we pick the node on its left or the node on its right? Similarly for the end
+ * position. Clearly, we'll need to bias the search towards the right when looking
+ * for the start position, and towards the left when looking for the end position.
+ * The following method lets us do just that. When passed an offset which sits
+ * on the edge of the computed node, it will pick the neighbor based on whether
+ * "forward" is true or false, where forward means searching towards the right
+ * and not forward is obviously towards the left.
+ * @param document the document to search in
+ * @param offset the offset to search for
+ * @param forward if true, search forwards, otherwise search backwards when on node boundaries
+ * @return the node which surrounds the given offset, or the node adjacent to the offset
+ * where the side depends on the forward parameter
+ */
+ public static Node getNode(IDocument document, int offset, boolean forward) {
+ Node node = getNode(document, offset);
+
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+
+ if (!forward && offset <= region.getStartOffset()) {
+ Node left = node.getPreviousSibling();
+ if (left == null) {
+ left = node.getParentNode();
+ }
+
+ node = left;
+ } else if (forward && offset >= region.getEndOffset()) {
+ Node right = node.getNextSibling();
+ if (right == null) {
+ right = node.getParentNode();
+ }
+ node = right;
+ }
+ }
+
+ return node;
+ }
+
+ /**
+ * Returns a range of elements for the given caret range. Note that the two elements
+ * may not be at the same level so callers may want to perform additional input
+ * filtering.
+ *
+ * @param document the document to search in
+ * @param beginOffset the beginning offset of the range
+ * @param endOffset the ending offset of the range
+ * @return a pair of begin+end elements, or null
+ */
+ public static Pair<Element, Element> getElementRange(IDocument document, int beginOffset,
+ int endOffset) {
+ Element beginElement = null;
+ Element endElement = null;
+ Node beginNode = getNode(document, beginOffset, true);
+ Node endNode = beginNode;
+ if (endOffset > beginOffset) {
+ endNode = getNode(document, endOffset, false);
+ }
+
+ if (beginNode == null || endNode == null) {
+ return null;
+ }
+
+ // Adjust offsets if you're pointing at text
+ if (beginNode.getNodeType() != Node.ELEMENT_NODE) {
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar2/>
+ beginElement = getNextElement(beginNode);
+ if (beginElement == null) {
+ // Might be inside the end of a parent, e.g.
+ // <foo> <bar/> | </foo> => should pick <bar/>
+ beginElement = getPreviousElement(beginNode);
+ if (beginElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ beginElement = getParentElement(beginNode);
+ }
+ }
+ } else {
+ beginElement = (Element) beginNode;
+ }
+
+ if (endNode.getNodeType() != Node.ELEMENT_NODE) {
+ // In the following, | marks the caret position:
+ // <foo> <bar1/> | <bar2/> </foo> => should pick <bar1/>
+ endElement = getPreviousElement(endNode);
+ if (endElement == null) {
+ // Might be inside the beginning of a parent, e.g.
+ // <foo> | <bar/></foo> => should pick <bar/>
+ endElement = getNextElement(endNode);
+ if (endElement == null) {
+ // We must be inside an empty element,
+ // <foo> | </foo>
+ // In that case just pick the parent.
+ endElement = getParentElement(endNode);
+ }
+ }
+ } else {
+ endElement = (Element) endNode;
+ }
+
+ if (beginElement != null && endElement != null) {
+ return Pair.of(beginElement, endElement);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the next sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the next sibling element, or null
+ */
+ public static Element getNextElement(Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getNextSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the previous sibling element of the node, or null if there is no such element
+ *
+ * @param node the starting node
+ * @return the previous sibling element, or null
+ */
+ public static Element getPreviousElement(Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getPreviousSibling();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Returns the closest ancestor element, or null if none
+ *
+ * @param node the starting node
+ * @return the closest parent element, or null
+ */
+ public static Element getParentElement(Node node) {
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getParentNode();
+ }
+
+ return (Element) node; // may be null as well
+ }
+
+ /**
+ * Converts the given attribute value to an XML-attribute-safe value, meaning that
+ * single and double quotes are replaced with their corresponding XML entities.
+ *
+ * @param attrValue the value to be escaped
+ * @return the escaped value
+ */
+ public static String toXmlAttributeValue(String attrValue) {
+ // Must escape ' and "
+ if (attrValue.indexOf('"') == -1 && attrValue.indexOf('\'') == -1) {
+ return attrValue;
+ }
+
+ int n = attrValue.length();
+ StringBuilder sb = new StringBuilder(2 * n);
+ for (int i = 0; i < n; i++) {
+ char c = attrValue.charAt(i);
+ if (c == '"') {
+ sb.append("&quot;"); //$NON-NLS-1$
+ } else if (c == '\'') {
+ sb.append("&apos;"); //$NON-NLS-1$
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /** Utility used by {@link #getFreeWidgetId(Element)} */
+ private static void addLowercaseIds(Element root, Set<String> seen) {
+ if (root.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
+ String id = root.getAttributeNS(ANDROID_URI, ATTR_ID);
+ seen.add(id.toLowerCase());
+ }
+ }
+
+ /**
+ * Returns a suitable new widget id (not including the {@code @id/} prefix) for the
+ * given element, which is guaranteed to be unique in this document
+ *
+ * @param element the element to compute a new widget id for
+ * @return a unique id, never null, which does not include the {@code @id/} prefix
+ * @see DescriptorsUtils#getFreeWidgetId
+ */
+ public static String getFreeWidgetId(Element element) {
+ Set<String> ids = new HashSet<String>();
+ addLowercaseIds(element.getOwnerDocument().getDocumentElement(), ids);
+
+ String prefix = element.getTagName();
+ String generated;
+ int num = 1;
+ do {
+ num++;
+ generated = String.format("%1$s%2$d", prefix, num); //$NON-NLS-1$
+ } while (ids.contains(generated.toLowerCase()));
+
+ return generated;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
index 5464917..d1ab0ca 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java
@@ -23,6 +23,9 @@ import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ExtractIncludeAction;
+import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.WrapInAction;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
@@ -37,8 +40,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.TreeMap;
+import java.util.Map.Entry;
import java.util.regex.Pattern;
/**
@@ -200,19 +203,18 @@ import java.util.regex.Pattern;
}
}
- insertExtractAsInclude(endId);
+ insertVisualRefactorings(endId);
}
- private void insertExtractAsInclude(String endId) {
+ private void insertVisualRefactorings(String endId) {
// Extract As <include> refactoring.
// Only include the menu item if you are not right clicking on a root,
// or on an included view, or on a non-contiguous selection
- IAction extractIncludeAction = new ExtractIncludeAction(mCanvas);
- if (extractIncludeAction.isEnabled()) {
- mMenuManager.insertBefore(endId, new Separator());
- mMenuManager.insertBefore(endId, extractIncludeAction);
- mMenuManager.insertBefore(endId, new Separator());
- }
+ mMenuManager.insertBefore(endId, new Separator());
+ mMenuManager.insertBefore(endId, ExtractIncludeAction.create(mEditor));
+ mMenuManager.insertBefore(endId, WrapInAction.create(mEditor));
+ mMenuManager.insertBefore(endId, ChangeLayoutAction.create(mEditor));
+ mMenuManager.insertBefore(endId, new Separator());
}
/**
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExtractIncludeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExtractIncludeAction.java
deleted file mode 100644
index 9566aff..0000000
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ExtractIncludeAction.java
+++ /dev/null
@@ -1,839 +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.editors.layout.gle2;
-
-import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_PREFIX;
-import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
-import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
-import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
-import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
-import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
-import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
-import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
-import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
-import static com.android.ide.eclipse.adt.AndroidConstants.DOT_XML;
-import static com.android.ide.eclipse.adt.AndroidConstants.WS_SEP;
-import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
-import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
-import static com.android.resources.ResourceType.LAYOUT;
-
-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.layout.LayoutEditor;
-import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
-import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
-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.preferences.AdtPrefs;
-import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ResourceNameValidator;
-import com.android.util.Pair;
-
-import org.eclipse.core.resources.IContainer;
-import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IProject;
-import org.eclipse.core.runtime.CoreException;
-import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.Path;
-import org.eclipse.core.runtime.QualifiedName;
-import org.eclipse.jface.action.Action;
-import org.eclipse.jface.action.IAction;
-import org.eclipse.jface.dialogs.IInputValidator;
-import org.eclipse.jface.dialogs.InputDialog;
-import org.eclipse.jface.text.BadLocationException;
-import org.eclipse.jface.window.Window;
-import org.eclipse.ui.IEditorPart;
-import org.eclipse.ui.IWorkbenchPage;
-import org.eclipse.ui.PartInitException;
-import org.eclipse.ui.ide.IDE;
-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.sse.core.internal.provisional.text.IStructuredDocumentRegion;
-import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
-import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
-import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Extracts the selection and writes it out as a separate layout file, then adds an
- * include to that new layout file. Interactively asks the user for a new name for the
- * layout.
- */
-@SuppressWarnings("restriction") // For XML model
-public class ExtractIncludeAction extends Action {
- private LayoutCanvas mCanvas;
-
- public ExtractIncludeAction(LayoutCanvas canvas) {
- super("Extract as Include...", IAction.AS_PUSH_BUTTON);
- mCanvas = canvas;
- }
-
- @Override
- public boolean isEnabled() {
- List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
- if (selection.size() == 0) {
- return false;
- }
-
- // Can't extract the root -- wouldn't that be pointless? (or maybe not always)
- for (SelectionItem item : selection) {
- if (item.isRoot()) {
- return false;
- }
- }
-
- // Disable if you've selected a single include tag
- if (selection.size() == 1) {
- UiViewElementNode uiNode = selection.get(0).getViewInfo().getUiViewNode();
- if (uiNode != null) {
- Node xmlNode = uiNode.getXmlNode();
- if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
- return false;
- }
- }
- }
-
- // Enforce that the selection is -contiguous-
- if (selection.size() > 1) {
- // All elements must be siblings (e.g. same parent)
- List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(selection.size());
- for (SelectionItem item : selection) {
- UiViewElementNode node = item.getViewInfo().getUiViewNode();
- if (node != null) {
- nodes.add(node);
- }
- }
- if (nodes.size() == 0) {
- return false;
- }
-
- UiElementNode parent = nodes.get(0).getUiParent();
- for (UiViewElementNode node : nodes) {
- if (parent != node.getUiParent()) {
- return false;
- }
- }
- // Ensure that the siblings are contiguous; no gaps.
- // If we've selected all the children of the parent then we don't need to look.
- List<UiElementNode> siblings = parent.getUiChildren();
- if (siblings.size() != nodes.size()) {
- Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
- boolean inRange = false;
- int remaining = nodes.size();
- for (UiElementNode node : siblings) {
- boolean in = nodeSet.contains(node);
- if (in) {
- remaining--;
- if (remaining == 0) {
- break;
- }
- inRange = true;
- } else if (inRange) {
- return false;
- }
- }
- }
- }
-
- return true;
- }
-
- private String inputName() {
- IProject project = mCanvas.getLayoutEditor().getProject();
- IInputValidator validator = ResourceNameValidator.create(true, project, LAYOUT);
-
- String defaultName = ""; //$NON-NLS-1$
- Element primaryNode = getPrimaryNode();
- if (primaryNode != null) {
- String id = primaryNode.getAttributeNS(ANDROID_URI, ATTR_ID);
- // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
- if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) {
- // Use everything following the id/, and make it lowercase since that is
- // the convention for layouts
- defaultName = id.substring(id.indexOf('/') + 1).toLowerCase();
- if (validator.isValid(defaultName) != null) { // Already exists?
- defaultName = ""; //$NON-NLS-1$
- }
- }
- }
-
- InputDialog d = new InputDialog(AdtPlugin.getDisplay().getActiveShell(),
- "Extract As Include", // title
- "New Layout Name", defaultName, validator);
-
- if (d.open() != Window.OK) {
- return null;
- }
-
- return d.getValue().trim();
- }
-
- @Override
- public void run() {
- String newName = inputName();
- if (newName == null) {
- // User canceled
- return;
- }
-
- // Create extracted content
- // In order to ensure that we preserve as much of the user's original formatting
- // and attribute order as possible, we will just snip out the exact element ranges
- // from the current source editor and reindent them in the new file
- Pair<Integer, Integer> range = computeExtractRange();
- if (range == null) {
- return;
- }
- int start = range.getFirst();
- int end = range.getSecond();
- String extractedText = getExtractedText(start, end);
-
- Pair<String, String> namespace = computeNamespaces();
- String androidNsPrefix = namespace.getFirst();
- String namespaceDeclarations = namespace.getSecond();
-
- // Insert namespace:
- extractedText = insertNamespace(extractedText, namespaceDeclarations);
-
- StringBuilder sb = new StringBuilder();
- sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
- sb.append(extractedText);
- sb.append('\n');
-
- String newFileName = newName + DOT_XML;
- IProject project = mCanvas.getLayoutEditor().getProject();
- IContainer parent = mCanvas.getLayoutEditor().getInputFile().getParent();
- IPath parentPath = parent.getProjectRelativePath();
- IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
-
- writeFile(file, sb.toString());
-
- // Force refresh to pick up the newly available @layout/<newName>
- LayoutEditor editor = mCanvas.getLayoutEditor();
- editor.getGraphicalEditor().refreshProjectResources();
-
- String referenceId = getReferenceId();
- List<Edit> edits = new ArrayList<Edit>();
-
- // Replace existing elements in the source file and insert <include>
- String include = computeIncludeString(newName, androidNsPrefix, referenceId);
- edits.add(new Edit(start, end, include));
-
- // Update any layout references to the old id with the new id
- if (referenceId != null) {
- String rootId = getRootId();
- IStructuredModel model = editor.getModelForRead();
- try {
- IStructuredDocument doc = model.getStructuredDocument();
- if (doc != null) {
- List<Edit> replaceIds = replaceIds(doc, start, end, rootId, referenceId);
- edits.addAll(replaceIds);
- }
- } finally {
- model.releaseFromRead();
- }
- }
-
- // Open extracted file. This seems to trigger refreshing of ProjectResources
- // such that the @layout/<newName> reference from the new <include> we're adding
- // will work; without this we get file reference errors
- openFile(file);
-
- // Perform edits
- applyEdits("Extract As Include", edits);
- }
-
- /** Produce a list of edits to replace references to the given id with the given new id */
- private List<Edit> replaceIds(IStructuredDocument doc, int skipStart, int skipEnd,
- String rootId, String referenceId) {
-
- // We need to search for either @+id/ or @id/
- String match1 = rootId;
- String match2;
- if (match1.startsWith(ID_PREFIX)) {
- match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"';
- match1 = '"' + match1 + '"';
- } else if (match1.startsWith(NEW_ID_PREFIX)) {
- match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"';
- match1 = '"' + match1 + '"';
- } else {
- return Collections.emptyList();
- }
-
- String namePrefix = ANDROID_NS_PREFIX + ':' + ATTR_LAYOUT_PREFIX;
- List<Edit> edits = new ArrayList<Edit>();
-
- IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
- for (; region != null; region = region.getNext()) {
- ITextRegionList list = region.getRegions();
- int regionStart = region.getStart();
-
- // Look at all attribute values and look for an id reference match
- String attributeName = ""; //$NON-NLS-1$
- for (int j = 0; j < region.getNumberOfRegions(); j++) {
- ITextRegion subRegion = list.get(j);
- String type = subRegion.getType();
- if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
- attributeName = region.getText(subRegion);
- } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
- // Only replace references in layout attributes
- if (!attributeName.startsWith(namePrefix)) {
- continue;
- }
- // Skip occurrences in the given skip range
- int subRegionStart = regionStart + subRegion.getStart();
- if (subRegionStart >= skipStart && subRegionStart <= skipEnd) {
- continue;
- }
-
- String attributeValue = region.getText(subRegion);
- if (attributeValue.equals(match1) || attributeValue.equals(match2)) {
- int start = subRegionStart + 1; // skip quote
- int end = start + rootId.length();
- edits.add(new Edit(start, end, referenceId));
- }
- }
- }
- }
-
- return edits;
- }
-
- /** Returns the id to be used for the include tag itself (may be null) */
- private String getReferenceId() {
- String rootId = getRootId();
- if (rootId != null) {
- return rootId + "_ref";
- }
-
- return null;
- }
-
- /** Get the id of the root selected element, if any */
- private String getRootId() {
- Element primaryNode = getPrimaryNode();
- if (primaryNode != null) {
- String oldId = primaryNode.getAttributeNS(ANDROID_URI, ATTR_ID);
- // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
- if (oldId != null && oldId.length() > 0) {
- return oldId;
- }
- }
-
- return null;
- }
-
- private boolean writeFile(IFile file, String content) {
- // Write out the content into the new XML file
- try {
- byte[] buf = content.getBytes("UTF8"); //$NON-NLS-1$
- InputStream stream = new ByteArrayInputStream(buf);
- file.create(stream, true /* force */, null /* progress */);
- return true;
- } catch (Exception e) {
- String message = e.getMessage();
- String error = String.format("Failed to generate %1$s: %2$s", file.getName(), message);
- AdtPlugin.displayError("Extract As Include", error);
- return false;
- }
- }
-
- private Pair<String, String> computeNamespaces() {
- String androidNsPrefix = null;
- String namespaceDeclarations = null;
-
- Document document = getDocument();
- if (document != null) {
- StringBuilder sb = new StringBuilder();
- Element root = document.getDocumentElement();
- NamedNodeMap attributes = root.getAttributes();
- for (int i = 0, n = attributes.getLength(); i < n; i++) {
- Node attributeNode = attributes.item(i);
-
- String prefix = attributeNode.getPrefix();
- if (XMLNS.equals(prefix)) {
- sb.append(' ');
- String name = attributeNode.getNodeName();
- sb.append(name);
- sb.append('=').append('"');
-
- String value = attributeNode.getNodeValue();
- if (value.equals(ANDROID_URI)) {
- androidNsPrefix = name;
- if (androidNsPrefix.startsWith(XMLNS_COLON)) {
- androidNsPrefix = androidNsPrefix.substring(XMLNS_COLON.length());
- }
- }
- sb.append(DescriptorsUtils.toXmlAttributeValue(value));
- sb.append('"');
- }
- }
-
- namespaceDeclarations = sb.toString();
- }
-
- if (androidNsPrefix == null) {
- androidNsPrefix = ANDROID_NS_PREFIX;
- }
- if (namespaceDeclarations == null) {
- StringBuilder sb = new StringBuilder();
- sb.append(' ');
- sb.append(XMLNS_COLON);
- sb.append(ANDROID_NS_PREFIX);
- sb.append('=').append('"');
- sb.append(ANDROID_URI);
- sb.append('"');
- namespaceDeclarations = sb.toString();
- }
-
- return Pair.of(androidNsPrefix, namespaceDeclarations);
- }
-
- private String insertNamespace(String xmlText, String namespaceDeclarations) {
- // Insert namespace declarations into the extracted XML fragment
- int firstSpace = xmlText.indexOf(' ');
- int elementEnd = xmlText.indexOf('>');
- int insertAt;
- if (firstSpace != -1 && firstSpace < elementEnd) {
- insertAt = firstSpace;
- } else {
- insertAt = elementEnd;
- }
- xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations
- + xmlText.substring(insertAt);
-
- return xmlText;
- }
-
- private void openFile(IFile file) {
- LayoutEditor editor = mCanvas.getLayoutEditor();
- GraphicalEditorPart graphicalEditor = editor.getGraphicalEditor();
- IFile leavingFile = graphicalEditor.getEditedFile();
-
- try {
- // Duplicate the current state into the newly created file
- QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
- String state = AdtPlugin.getFileProperty(leavingFile, qname);
- file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
- } catch (CoreException e) {
- // pass
- }
-
- /* TBD: "Show Included In" if supported.
- * Not sure if this is a good idea.
- if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
- try {
- Reference include = Reference.create(graphicalEditor.getEditedFile());
- file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include);
- } catch (CoreException e) {
- // pass - worst that can happen is that we don't start with inclusion
- }
- }
- */
-
- try {
- IEditorPart part = IDE.openEditor(editor.getEditorSite().getPage(), file);
- if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatXml()) {
- AndroidXmlEditor newEditor = (AndroidXmlEditor) part;
- newEditor.reformatDocument();
- }
- } catch (PartInitException e) {
- AdtPlugin.log(e, "Can't open new included layout");
- }
- }
-
- /**
- * Compute the actual {@code <include>} string to be inserted in place of the old
- * selection
- */
- private String computeIncludeString(String newName, String androidNsPrefix,
- String referenceId) {
- StringBuilder sb = new StringBuilder();
- sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
- sb.append(newName);
- sb.append('"');
- sb.append(' ');
-
- // Create new id for the include itself
- if (referenceId != null) {
- sb.append(androidNsPrefix);
- sb.append(':');
- sb.append(ATTR_ID);
- sb.append('=').append('"');
- sb.append(referenceId);
- sb.append('"').append(' ');
- }
-
- // Add id string, unless it's a <merge>, since we may need to adjust any layout
- // references to apply to the <include> tag instead
- // TODO: Use refactoring infrastructure to handle this part
-
- // I should move all the layout_ attributes as well
- // I also need to duplicate and modify the id and then replace
- // everything else in the file with this new id...
-
- // HACK: see issue 13494: We must duplicate the width/height attributes on the
- // <include> statement for designtime rendering only
- Element primaryNode = getPrimaryNode();
- String width = null;
- String height = null;
- if (primaryNode == null) {
- // Multiple selection - in that case we will be creating an outer <merge>
- // so we need to set our own width/height on it
- width = height = VALUE_WRAP_CONTENT;
- } else {
- if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
- width = VALUE_WRAP_CONTENT;
- } else {
- width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
- }
- if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
- height = VALUE_WRAP_CONTENT;
- } else {
- height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
- }
- }
- if (width != null) {
- sb.append(' ');
- sb.append(androidNsPrefix);
- sb.append(':');
- sb.append(ATTR_LAYOUT_WIDTH);
- sb.append('=').append('"');
- sb.append(DescriptorsUtils.toXmlAttributeValue(width));
- sb.append('"');
- }
- if (height != null) {
- sb.append(' ');
- sb.append(androidNsPrefix);
- sb.append(':');
- sb.append(ATTR_LAYOUT_HEIGHT);
- sb.append('=').append('"');
- sb.append(DescriptorsUtils.toXmlAttributeValue(height));
- sb.append('"');
- }
-
- // Duplicate all the other layout attributes as well
- if (primaryNode != null) {
- NamedNodeMap attributes = primaryNode.getAttributes();
- for (int i = 0, n = attributes.getLength(); i < n; i++) {
- Node attr = attributes.item(i);
- String name = attr.getLocalName();
- if (name.startsWith(ATTR_LAYOUT_PREFIX)
- && ANDROID_URI.equals(attr.getNamespaceURI())) {
- if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
- // Already handled
- continue;
- }
-
- sb.append(' ');
- sb.append(androidNsPrefix);
- sb.append(':');
- sb.append(name);
- sb.append('=').append('"');
- sb.append(DescriptorsUtils.toXmlAttributeValue(attr.getNodeValue()));
- sb.append('"');
- }
- }
- }
-
- sb.append("/>");
- return sb.toString();
- }
-
- /** Return the text in the document in the range start to end */
- private String getExtractedText(int start, int end) {
- LayoutEditor editor = mCanvas.getLayoutEditor();
-
- IStructuredModel model = editor.getModelForRead();
- try {
- IStructuredDocument document = editor.getStructuredDocument();
- String xml = document.get(start, end - start);
- Element primaryNode = getPrimaryNode();
- xml = stripTopLayoutAttributes(primaryNode, start, xml);
- xml = dedent(xml);
-
- // Wrap siblings in <merge>?
- if (primaryNode == null) {
- StringBuilder sb = new StringBuilder();
- sb.append("<merge>\n"); //$NON-NLS-1$
- // indent an extra level
- for (String line : xml.split("\n")) { //$NON-NLS-1$
- sb.append(" "); //$NON-NLS-1$
- sb.append(line).append('\n');
- }
- sb.append("</merge>\n"); //$NON-NLS-1$
- xml = sb.toString();
- }
-
- return xml;
- } catch (BadLocationException e) {
- // the region offset was invalid. ignore.
- return null;
- } finally {
- model.releaseFromRead();
- }
- }
-
- /** Remove sections of the document that correspond to top level layout attributes;
- * these are placed on the include element instead */
- private String stripTopLayoutAttributes(Element primaryNode, int start, String xml) {
- if (primaryNode != null) {
- // List of attributes to remove
- //IndexedRegion attRegion = (IndexedRegion) attrs.item(i);
- List<IndexedRegion> skip = new ArrayList<IndexedRegion>();
- NamedNodeMap attributes = primaryNode.getAttributes();
- for (int i = 0, n = attributes.getLength(); i < n; i++) {
- Node attr = attributes.item(i);
- String name = attr.getLocalName();
- if (name.startsWith(ATTR_LAYOUT_PREFIX)
- && ANDROID_URI.equals(attr.getNamespaceURI())) {
- if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
- // These are special and are left in
- continue;
- }
-
- if (attr instanceof IndexedRegion) {
- skip.add((IndexedRegion) attr);
- }
- }
- }
- if (skip.size() > 0) {
- Collections.sort(skip, new Comparator<IndexedRegion>() {
- // Sort in start order
- public int compare(IndexedRegion r1, IndexedRegion r2) {
- return r1.getStartOffset() - r2.getStartOffset();
- }
- });
-
- // Successively cut out the various layout attributes
- // TODO remove adjacent whitespace too (but not newlines, unless they
- // are newly adjacent)
- StringBuilder sb = new StringBuilder(xml.length());
- int nextStart = 0;
-
- // Copy out all the sections except the skip sections
- for (IndexedRegion r : skip) {
- int regionStart = r.getStartOffset();
- // Adjust to string offsets since we've copied the string out of
- // the document
- regionStart -= start;
-
- sb.append(xml.substring(nextStart, regionStart));
-
- nextStart = regionStart + r.getLength();
- }
- if (nextStart < xml.length()) {
- sb.append(xml.substring(nextStart));
- }
-
- return sb.toString();
- }
- }
-
- return xml;
- }
-
- private static String getIndent(String line, int max) {
- int i = 0;
- int n = Math.min(max, line.length());
- for (; i < n; i++) {
- char c = line.charAt(i);
- if (!Character.isWhitespace(c)) {
- return line.substring(0, i);
- }
- }
-
- if (n < line.length()) {
- return line.substring(0, n);
- } else {
- return line;
- }
- }
-
- private static String dedent(String xml) {
- String[] lines = xml.split("\n"); //$NON-NLS-1$
- if (lines.length < 2) {
- // The first line never has any indentation since we copy it out from the
- // element start index
- return xml;
- }
-
- String indentPrefix = getIndent(lines[1], lines[1].length());
- for (int i = 2, n = lines.length; i < n; i++) {
- String line = lines[i];
-
- // Ignore blank lines
- if (line.trim().length() == 0) {
- continue;
- }
-
- indentPrefix = getIndent(line, indentPrefix.length());
-
- if (indentPrefix.length() == 0) {
- return xml;
- }
- }
-
- StringBuilder sb = new StringBuilder();
- for (String line : lines) {
- if (line.startsWith(indentPrefix)) {
- sb.append(line.substring(indentPrefix.length()));
- } else {
- sb.append(line);
- }
- sb.append('\n');
- }
- return sb.toString();
- }
-
- private Element getPrimaryNode() {
- List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
- if (selection.size() == 1) {
- UiViewElementNode node = selection.get(0).getViewInfo().getUiViewNode();
- if (node != null) {
- Node xmlNode = node.getXmlNode();
- if (xmlNode instanceof Element) {
- return (Element) xmlNode;
- }
- }
- }
-
- return null;
- }
-
- private Document getDocument() {
- List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
- for (SelectionItem item : selection) {
- UiViewElementNode node = item.getViewInfo().getUiViewNode();
- if (node != null) {
- Node xmlNode = node.getXmlNode();
- if (xmlNode != null) {
- return xmlNode.getOwnerDocument();
- }
- }
- }
-
- return null;
- }
-
- private Pair<Integer, Integer> computeExtractRange() {
- List<SelectionItem> selection = mCanvas.getSelectionManager().getSelections();
- if (selection.size() == 0) {
- return null;
- }
- int end = Integer.MIN_VALUE;
- int start = Integer.MAX_VALUE;
- for (SelectionItem item : selection) {
- CanvasViewInfo viewInfo = item.getViewInfo();
- UiViewElementNode uiNode = viewInfo.getUiViewNode();
- if (uiNode == null) {
- continue;
- }
- Node xmlNode = uiNode.getXmlNode();
- if (xmlNode instanceof IndexedRegion) {
- IndexedRegion region = (IndexedRegion) xmlNode;
-
- start = Math.min(start, region.getStartOffset());
- end = Math.max(end, region.getEndOffset());
- }
- }
- if (start < 0) {
- return null;
- }
-
- return Pair.of(start, end);
- }
-
- /** Apply edits into document under an undo lock */
- private void applyEdits(String label, final List<Edit> edits) {
- // Process the edits in reverse document position order to ensure
- // that the offsets aren't affected by other edits
- Collections.sort(edits);
- final LayoutEditor editor = mCanvas.getLayoutEditor();
- editor.wrapUndoEditXmlModel(label, new Runnable() {
- public void run() {
- IStructuredDocument document = editor.getStructuredDocument();
- if (document != null) {
- try {
- for (Edit edit : edits) {
- edit.apply(document);
- }
- } catch (BadLocationException e) {
- AdtPlugin.log(e, "Cannot insert <include> tag");
- return;
- }
- }
- }
- });
-
- // Save file to trigger include finder scanning (as well as making the
- // actual show-include feature work since it relies on reading files from
- // disk, not a live buffer)
- IEditorPart editorPart = editor;
- IWorkbenchPage page = editorPart.getEditorSite().getPage();
- page.saveEditor(editorPart, false);
- }
-
- /**
- * Edit operation (insert, delete, replace) at a given offset - a collection of these
- * can be sorted in reverse offset order such that they can be applied successively
- * without having to updated offsets
- * <p>
- * TODO: When rewriting this extract operation to the refactoring framework, use
- * refactoring framework's change list instead
- */
- private static class Edit implements Comparable<Edit> {
- private final int mStart;
- private final int mEnd;
- private final String mReplaceWith;
-
- public Edit(int start, int end, String replaceWith) {
- super();
- mStart = start;
- mEnd = end;
- mReplaceWith = replaceWith;
- }
-
- void apply(IStructuredDocument document) throws BadLocationException {
- document.replace(mStart, mEnd - mStart, mReplaceWith);
- }
-
- public int compareTo(Edit o) {
- // Sort in *reverse* offset order
- return o.mStart - mStart;
- }
-
- @Override
- public String toString() {
- return "Edit [start=" + mStart + ", end=" + mEnd + ", replaceWith=" + mReplaceWith
- + "]";
- }
- }
-}
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 ad505f1..22ed694 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
@@ -794,7 +794,7 @@ public class GraphicalEditorPart extends EditorPart
}
/** Refresh the configured project resources associated with this editor */
- /*package*/ void refreshProjectResources() {
+ public void refreshProjectResources() {
mConfiguredProjectRes = null;
mConfigListener.getConfiguredProjectResources();
}
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 37ce440..01f153b 100755
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutCanvas.java
@@ -432,7 +432,7 @@ public class LayoutCanvas extends Canvas {
/**
* Returns the {@link LayoutEditor} associated with this canvas.
*/
- /* package */ LayoutEditor getLayoutEditor() {
+ LayoutEditor getLayoutEditor() {
return mLayoutEditor;
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
index 983bcf5..84db9fb 100644
--- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/SelectionItem.java
@@ -31,7 +31,7 @@ import java.util.List;
/**
* Represents one selection in {@link LayoutCanvas}.
*/
-/* package */ class SelectionItem {
+class SelectionItem {
/** Current selected view info. Can be null. */
private final CanvasViewInfo mCanvasViewInfo;
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 4f9a277..e34ecbf 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
@@ -115,7 +115,7 @@ public class SelectionManager implements ISelectionProvider {
* @see #getSelection() {@link #getSelection()} to retrieve a {@link TreeViewer}
* compatible {@link ISelection}.
*/
- /* package */ List<SelectionItem> getSelections() {
+ List<SelectionItem> getSelections() {
return mUnmodifiableSelection;
}
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 cde4e28..944f7d4 100755
--- 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
@@ -16,6 +16,7 @@
package com.android.ide.eclipse.adt.internal.editors.layout.gre;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
import com.android.ide.common.api.DropFeedback;
@@ -600,7 +601,7 @@ public class RulesEngine {
// Deal with unknown descriptors; these lack the full qualified path and
// elements in the layout without a package are taken to be in the
// android.widget package.
- fqcn = "android.widget." + fqcn; //$NON-NLS-1$
+ fqcn = ANDROID_WIDGET_PREFIX + fqcn;
}
// Try to find a rule matching the "real" FQCN. If we find it, we're done.
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java
new file mode 100644
index 0000000..cb6eabf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Convert Layout" menu item is invoked.
+ */
+public class ChangeLayoutAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ChangeLayoutRefactoring ref = new ChangeLayoutRefactoring(mFile, mEditor,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ChangeLayoutWizard(ref, mFile.getProject());
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditor editor) {
+ return create("Change Layout...", editor, ChangeLayoutAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java
new file mode 100644
index 0000000..c508b7e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutContribution.java
@@ -0,0 +1,40 @@
+/*
+ * 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.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ChangeLayoutContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ChangeLayoutRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ChangeLayoutRefactoring.Descriptor) {
+ return ((ChangeLayoutRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
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
new file mode 100644
index 0000000..c49e13d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutRefactoring.java
@@ -0,0 +1,410 @@
+/*
+ * 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.layout.refactoring;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_BASELINE_ALIGNED;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_LINEAR_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_RELATIVE_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.FQCN_TABLE_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.LINEAR_LAYOUT;
+import static com.android.ide.common.layout.LayoutConstants.TABLE_ROW;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_FALSE;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
+import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML;
+
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+
+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.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Converts the selected layout into a layout of a different type.
+ */
+@SuppressWarnings("restriction") // XML model
+public class ChangeLayoutRefactoring extends VisualRefactoring {
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+
+ private String mTypeFqcn;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ChangeLayoutRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ }
+
+ public ChangeLayoutRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 2);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to convert");
+ return status;
+ }
+
+ mElements = getElements();
+ if (mElements.size() != 1) {
+ status.addFatalError("Select precisely one layout to convert");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_TYPE, mTypeFqcn);
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Change Layout";
+ }
+
+ void setType(String typeFqcn) {
+ mTypeFqcn = typeFqcn;
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ String name = getViewClass(mTypeFqcn);
+
+ IFile file = mEditor.getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setEdit(rootEdit);
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ String text = getText(mSelectionStart, mSelectionEnd);
+ Element layout = getPrimaryElement();
+ String oldName = layout.getNodeName();
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+
+ if (open != -1 && close != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength, name));
+ if (close != open) { // Gracefully handle <FooLayout/>
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + close, oldLength, name));
+ }
+ }
+
+ String oldType = getOldType();
+ String newType = mTypeFqcn;
+ if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_RELATIVE_LAYOUT)) {
+ // Hand-coded conversion
+ convertLinearToRelative(rootEdit);
+ } else if (oldType.equals(FQCN_RELATIVE_LAYOUT) && newType.equals(FQCN_LINEAR_LAYOUT)) {
+ convertRelativeToLinear(rootEdit);
+ } else if (oldType.equals(FQCN_LINEAR_LAYOUT) && newType.equals(FQCN_TABLE_LAYOUT)) {
+ convertLinearToTable(rootEdit);
+ } else {
+ convertGeneric(rootEdit, oldType, newType);
+ }
+
+ removeUndefinedLayoutAttrs(rootEdit, layout);
+
+ return changes;
+ }
+
+ /** Hand coded conversion from a LinearLayout to a TableLayout */
+ private void convertLinearToTable(MultiTextEdit rootEdit) {
+ // This is pretty easy; just switch the root tag (already done by the initial generic
+ // conversion) and then convert all the children into <TableRow> elements.
+ // Finally, get rid of the orientation attribute, if any.
+ Element layout = getPrimaryElement();
+ removeOrientationAttribute(rootEdit, layout);
+
+ NodeList children = layout.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ if (node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) node;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+ String text = getText(start, end);
+ String oldName = child.getNodeName();
+ if (oldName.equals(LINEAR_LAYOUT)) {
+ removeOrientationAttribute(rootEdit, child);
+ int open = text.indexOf(oldName);
+ int close = text.lastIndexOf(oldName);
+
+ if (open != -1 && close != -1) {
+ int oldLength = oldName.length();
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + open, oldLength,
+ TABLE_ROW));
+ if (close != open) { // Gracefully handle <FooLayout/>
+ rootEdit.addChild(new ReplaceEdit(mSelectionStart + close,
+ oldLength, TABLE_ROW));
+ }
+ }
+ } // else: WRAP in TableLayout!
+ }
+ }
+ }
+ }
+
+ /** Hand coded conversion from a LinearLayout to a RelativeLayout */
+ private void convertLinearToRelative(MultiTextEdit rootEdit) {
+ // This can be done accurately.
+ Element layout = getPrimaryElement();
+ // Horizontal is the default, so if no value is specified it is horizontal.
+ boolean isVertical = VALUE_VERTICAL.equals(layout.getAttributeNS(ANDROID_URI,
+ ATTR_ORIENTATION));
+
+ removeOrientationAttribute(rootEdit, layout);
+
+ String attributePrefix = getAndroidNamespacePrefix();
+
+ // TODO: Consider gravity of each element
+ // TODO: Consider weight of each element
+ // Right now it simply makes a single attachment to keep the order.
+
+ if (isVertical) {
+ // Align each child to the bottom and left of its parent
+ NodeList children = layout.getChildNodes();
+ String prevId = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ String id = ensureHasId(rootEdit, child);
+ if (prevId != null) {
+ addAttributeDeclaration(rootEdit, child, attributePrefix,
+ ATTR_LAYOUT_BELOW, prevId);
+ }
+ prevId = id;
+ }
+ }
+ } else {
+ // Align each child to the left
+ NodeList children = layout.getChildNodes();
+ boolean isBaselineAligned =
+ !VALUE_FALSE.equals(layout.getAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED));
+
+ String prevId = null;
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+ String id = ensureHasId(rootEdit, child);
+ if (prevId != null) {
+ addAttributeDeclaration(rootEdit, child, attributePrefix,
+ ATTR_LAYOUT_TO_RIGHT_OF, prevId);
+ if (isBaselineAligned) {
+ addAttributeDeclaration(rootEdit, child, attributePrefix,
+ ATTR_LAYOUT_ALIGN_BASELINE, prevId);
+ }
+ }
+ prevId = id;
+ }
+ }
+ }
+ }
+
+ /** Strips out the android:orientation attribute from the given linear layout element */
+ private void removeOrientationAttribute(MultiTextEdit rootEdit, Element layout) {
+ assert layout.getTagName().equals(LINEAR_LAYOUT);
+ removeAttribute(rootEdit, layout, ANDROID_URI, ATTR_ORIENTATION);
+ }
+
+ /**
+ * Hand coded conversion from a RelativeLayout to a LinearLayout
+ *
+ * @param rootEdit the root multi text edit to add edits to
+ */
+ private void convertRelativeToLinear(MultiTextEdit rootEdit) {
+ // This is going to be lossy...
+ // TODO: Attempt to "order" the items based on their visual positions
+ // and insert them in that order in the LinearLayout.
+ // TODO: Possibly use nesting if necessary, by spatial subdivision,
+ // to accomplish roughly the same layout as the relative layout specifies.
+ }
+
+ /**
+ * Hand coded -generic- conversion from one layout to another. This is not going to be
+ * an accurate layout transformation; instead it simply migrates the layout attributes
+ * that are supported, and adds defaults for any new required layout attributes. In
+ * addition, it attempts to order the children visually based on where they fit in a
+ * rendering. (Unsupported layout attributes will be removed by the caller at the
+ * end.)
+ * <ul>
+ * <li>Try to handle nesting. Converting a *hierarchy* of layouts into a flatter
+ * layout for powerful layouts that support it, like RelativeLayout.
+ * <li>Try to do automatic "inference" about the layout. I can render it and look at
+ * the ViewInfo positions and sizes. I can render it multiple times, at different
+ * sizes, to infer "stretchiness" and "weight" properties of the children.
+ * <li>Try to do indirect transformations. E.g. if I can go from A to B, and B to C,
+ * then an attempt to go from A to C should perform conversions A to B and then B to
+ * C.
+ * </ul>
+ *
+ * @param rootEdit the root multi text edit to add edits to
+ * @param oldType the fully qualified class name of the layout type we are converting
+ * from
+ * @param newType the fully qualified class name of the layout type we are converting
+ * to
+ */
+ private void convertGeneric(MultiTextEdit rootEdit, String oldType, String newType) {
+ // TODO: Add hooks for 3rd party conversions getting registered through the
+ // IViewRule interface.
+
+ // For now we simply go with the default behavior, which is to just strip the
+ // layout attributes that aren't supported.
+ }
+
+ /** Removes all the unused attributes after a conversion */
+ private void removeUndefinedLayoutAttrs(MultiTextEdit rootEdit, Element layout) {
+ ViewElementDescriptor descriptor = getLayoutDescriptor();
+ if (descriptor == null) {
+ return;
+ }
+
+ Set<String> defined = new HashSet<String>();
+ AttributeDescriptor[] layoutAttributes = descriptor.getLayoutAttributes();
+ for (AttributeDescriptor attribute : layoutAttributes) {
+ defined.add(attribute.getXmlLocalName());
+ }
+
+ NodeList children = layout.getChildNodes();
+ for (int i = 0, n = children.getLength(); i < n; i++) {
+ Node node = children.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element child = (Element) node;
+
+ List<Attr> attributes = findLayoutAttributes(child);
+ for (Attr attribute : attributes) {
+ String name = attribute.getLocalName();
+ if (!defined.contains(name)) {
+ // Remove it
+ removeAttribute(rootEdit, child, attribute.getNamespaceURI(), name);
+ }
+ }
+ }
+ }
+ }
+
+ private ViewElementDescriptor getLayoutDescriptor() {
+ Sdk current = Sdk.getCurrent();
+ if (current != null) {
+ IAndroidTarget target = current.getTarget(mProject);
+ if (target != null) {
+ AndroidTargetData targetData = current.getTargetData(target);
+ List<ViewElementDescriptor> layouts =
+ targetData.getLayoutDescriptors().getLayoutDescriptors();
+ for (ViewElementDescriptor descriptor : layouts) {
+ if (mTypeFqcn.equals(descriptor.getFullClassName())) {
+ return descriptor;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.convert", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ChangeLayoutRefactoring(args);
+ }
+ }
+
+ String getOldType() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldType = primary.getTagName();
+ if (oldType.indexOf('.') == -1) {
+ oldType = ANDROID_WIDGET_PREFIX + oldType;
+ }
+ return oldType;
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java
new file mode 100644
index 0000000..98066fd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ChangeLayoutWizard.java
@@ -0,0 +1,104 @@
+/*
+ * 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.layout.refactoring;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+class ChangeLayoutWizard extends RefactoringWizard {
+ private final IProject mProject;
+
+ public ChangeLayoutWizard(ChangeLayoutRefactoring ref, IProject project) {
+ super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mProject = project;
+ setDefaultPageTitle("Change Layout");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ChangeLayoutRefactoring ref = (ChangeLayoutRefactoring) getRefactoring();
+ String oldType = ref.getOldType();
+ addPage(new InputPage(mProject, oldType));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ChangeLayoutRefactoring} operation */
+ private static class InputPage extends UserInputWizardPage {
+ private final IProject mProject;
+ private final String mOldType;
+ private Combo mTypeCombo;
+
+ public InputPage(IProject project, String oldType) {
+ super("ChangeLayoutInputPage"); //$NON-NLS-1$
+ mProject = project;
+ mOldType = oldType;
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ typeLabel.setText("New Layout Type:");
+
+ mTypeCombo = new Combo(composite, SWT.READ_ONLY);
+ mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ SelectionAdapter selectionListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ }
+ };
+ mTypeCombo.addSelectionListener(selectionListener);
+
+ WrapInWizard.addLayouts(mProject, mTypeCombo, mOldType);
+
+ setControl(composite);
+ validatePage();
+ }
+
+ private boolean validatePage() {
+ boolean ok = true;
+
+ if (mTypeCombo.getText().equals(WrapInWizard.SEPARATOR_LABEL)) {
+ setErrorMessage("Select a layout type");
+ ok = false;
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ ChangeLayoutRefactoring refactoring =
+ (ChangeLayoutRefactoring) getRefactoring();
+ refactoring.setType(mTypeCombo.getText());
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java
new file mode 100644
index 0000000..5bd9bde
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Extract as Include" menu item is invoked.
+ */
+public class ExtractIncludeAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ ExtractIncludeRefactoring ref = new ExtractIncludeRefactoring(mFile, mEditor,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new ExtractIncludeWizard(ref, mFile.getProject());
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditor editor) {
+ return create("Extract Include...", editor, ExtractIncludeAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java
new file mode 100644
index 0000000..5903812
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeContribution.java
@@ -0,0 +1,40 @@
+/*
+ * 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.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class ExtractIncludeContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new ExtractIncludeRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof ExtractIncludeRefactoring.Descriptor) {
+ return ((ExtractIncludeRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
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
new file mode 100644
index 0000000..9cf307d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeRefactoring.java
@@ -0,0 +1,507 @@
+/*
+ * 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.layout.refactoring;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.eclipse.adt.AndroidConstants.DOT_XML;
+import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML;
+import static com.android.ide.eclipse.adt.AndroidConstants.WS_SEP;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
+import static com.android.resources.ResourceType.LAYOUT;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidConstants;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+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.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ResourceNameValidator;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+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.core.runtime.Path;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Extracts the selection and writes it out as a separate layout file, then adds an
+ * include to that new layout file. Interactively asks the user for a new name for the
+ * layout.
+ */
+@SuppressWarnings("restriction") // XML model
+public class ExtractIncludeRefactoring extends VisualRefactoring {
+ private static final String KEY_NAME = "name"; //$NON-NLS-1$
+ private static final String KEY_OCCURRENCES = "all-occurrences"; //$NON-NLS-1$
+ private static final String KEY_UPDATE_REFS = "update-refs"; //$NON-NLS-1$
+ private String mLayoutName;
+ private boolean mReplaceOccurrences;
+ private boolean mUpdateReferences;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ ExtractIncludeRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mLayoutName = arguments.get(KEY_NAME);
+ mUpdateReferences = Boolean.parseBoolean(arguments.get(KEY_UPDATE_REFS));
+ mReplaceOccurrences = Boolean.parseBoolean(arguments.get(KEY_OCCURRENCES));
+ }
+
+ public ExtractIncludeRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to extract");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ // TODO - don't do this if we based the selection on text. In this case,
+ // make sure we're -balanced-.
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+
+ if (!validateNotRoot(infos, status)) {
+ return status;
+ }
+
+ // Disable if you've selected a single include tag
+ if (infos.size() == 1) {
+ UiViewElementNode uiNode = infos.get(0).getUiViewNode();
+ if (uiNode != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
+ status.addWarning("No point in refactoring a single include tag");
+ }
+ }
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (!validateContiguous(infos, status)) {
+ return status;
+ }
+ }
+
+ // This also ensures that we have a valid DOM model:
+ mElements = getElements();
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to extract");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_NAME, mLayoutName);
+ args.put(KEY_UPDATE_REFS, Boolean.toString(mUpdateReferences));
+ args.put(KEY_OCCURRENCES, Boolean.toString(mReplaceOccurrences));
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Extract as Include";
+ }
+
+ void setLayoutName(String layoutName) {
+ mLayoutName = layoutName;
+ }
+
+ void setUpdateReferences(boolean selection) {
+ mUpdateReferences = selection;
+ }
+
+ void setReplaceOccurrences(boolean selection) {
+ mReplaceOccurrences = selection;
+ }
+
+ // ---- Actual implementation of Extract as Include modification computation ----
+
+ @Override
+ protected List<Change> computeChanges() {
+ String extractedText = getExtractedText();
+
+ Pair<String, String> namespace = computeNamespaces();
+ String androidNsPrefix = namespace.getFirst();
+ String namespaceDeclarations = namespace.getSecond();
+
+ // Insert namespace:
+ extractedText = insertNamespace(extractedText, namespaceDeclarations);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+ sb.append(extractedText);
+ sb.append('\n');
+
+ List<Change> changes = new ArrayList<Change>();
+
+ String newFileName = mLayoutName + DOT_XML;
+ IProject project = mEditor.getProject();
+ IFile sourceFile = mEditor.getInputFile();
+
+ TextFileChange change = new TextFileChange(sourceFile.getName(), sourceFile);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setEdit(rootEdit);
+ change.setTextType(EXT_XML);
+ changes.add(change);
+
+ String referenceId = getReferenceId();
+ // Replace existing elements in the source file and insert <include>
+ String include = computeIncludeString(mLayoutName, androidNsPrefix, referenceId);
+ int length = mSelectionEnd - mSelectionStart;
+ ReplaceEdit replace = new ReplaceEdit(mSelectionStart, length, include);
+ rootEdit.addChild(replace);
+
+ // Update any layout references to the old id with the new id
+ if (mUpdateReferences && referenceId != null) {
+ String rootId = getRootId();
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(doc, mSelectionStart,
+ mSelectionEnd, rootId, referenceId);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ // Add change to create the new file
+ IContainer parent = sourceFile.getParent();
+ IPath parentPath = parent.getProjectRelativePath();
+ final IFile file = project.getFile(new Path(parentPath + WS_SEP + newFileName));
+ TextFileChange addFile = new TextFileChange("Create new separate layout", file);
+ addFile.setTextType(AndroidConstants.EXT_XML);
+ changes.add(addFile);
+ addFile.setEdit(new InsertEdit(0, sb.toString()));
+
+ Change finishHook = createFinishHook(file);
+ changes.add(finishHook);
+
+ return changes;
+ }
+
+ String getInitialName() {
+ String defaultName = ""; //$NON-NLS-1$
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String id = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+ // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
+ if (id != null && (id.startsWith(ID_PREFIX) || id.startsWith(NEW_ID_PREFIX))) {
+ // Use everything following the id/, and make it lowercase since that is
+ // the convention for layouts
+ defaultName = id.substring(id.indexOf('/') + 1).toLowerCase();
+
+ IInputValidator validator = ResourceNameValidator.create(true, mProject, LAYOUT);
+
+ if (validator.isValid(defaultName) != null) { // Already exists?
+ defaultName = ""; //$NON-NLS-1$
+ }
+ }
+ }
+
+ return defaultName;
+ }
+
+ private Change createFinishHook(final IFile file) {
+ return new NullChange("Open extracted layout and refresh resources") {
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ Display display = AdtPlugin.getDisplay();
+ display.asyncExec(new Runnable() {
+ public void run() {
+ openFile(file);
+ mEditor.getGraphicalEditor().refreshProjectResources();
+ // Save file to trigger include finder scanning (as well as making
+ // the
+ // actual show-include feature work since it relies on reading
+ // files from
+ // disk, not a live buffer)
+ IWorkbenchPage page = mEditor.getEditorSite().getPage();
+ page.saveEditor(mEditor, false);
+ }
+ });
+
+ // Not undoable: just return null instead of an undo-change.
+ return null;
+ }
+ };
+ }
+
+ private Pair<String, String> computeNamespaces() {
+ String androidNsPrefix = null;
+ String namespaceDeclarations = null;
+
+ StringBuilder sb = new StringBuilder();
+ List<Attr> attributeNodes = findNamespaceAttributes();
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ sb.append(' ');
+ String name = attributeNode.getNodeName();
+ sb.append(name);
+ sb.append('=').append('"');
+
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ androidNsPrefix = name;
+ if (androidNsPrefix.startsWith(XMLNS_COLON)) {
+ androidNsPrefix = androidNsPrefix.substring(XMLNS_COLON.length());
+ }
+ }
+ sb.append(DomUtilities.toXmlAttributeValue(value));
+ sb.append('"');
+ }
+ }
+ namespaceDeclarations = sb.toString();
+
+ if (androidNsPrefix == null) {
+ androidNsPrefix = ANDROID_NS_PREFIX;
+ }
+
+ if (namespaceDeclarations.length() == 0) {
+ sb.setLength(0);
+ sb.append(' ');
+ sb.append(XMLNS_COLON);
+ sb.append(androidNsPrefix);
+ sb.append('=').append('"');
+ sb.append(ANDROID_URI);
+ sb.append('"');
+ namespaceDeclarations = sb.toString();
+ }
+
+ return Pair.of(androidNsPrefix, namespaceDeclarations);
+ }
+
+ /** Returns the id to be used for the include tag itself (may be null) */
+ private String getReferenceId() {
+ String rootId = getRootId();
+ if (rootId != null) {
+ return rootId + "_ref";
+ }
+
+ return null;
+ }
+
+ /**
+ * Compute the actual {@code <include>} string to be inserted in place of the old
+ * selection
+ */
+ private String computeIncludeString(String newName, String androidNsPrefix,
+ String referenceId) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<include layout=\"@layout/"); //$NON-NLS-1$
+ sb.append(newName);
+ sb.append('"');
+ sb.append(' ');
+
+ // Create new id for the include itself
+ if (referenceId != null) {
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_ID);
+ sb.append('=').append('"');
+ sb.append(referenceId);
+ sb.append('"').append(' ');
+ }
+
+ // Add id string, unless it's a <merge>, since we may need to adjust any layout
+ // references to apply to the <include> tag instead
+
+ // I should move all the layout_ attributes as well
+ // I also need to duplicate and modify the id and then replace
+ // everything else in the file with this new id...
+
+ // HACK: see issue 13494: We must duplicate the width/height attributes on the
+ // <include> statement for designtime rendering only
+ Element primaryNode = getPrimaryElement();
+ String width = null;
+ String height = null;
+ if (primaryNode == null) {
+ // Multiple selection - in that case we will be creating an outer <merge>
+ // so we need to set our own width/height on it
+ width = height = VALUE_WRAP_CONTENT;
+ } else {
+ if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)) {
+ width = VALUE_WRAP_CONTENT;
+ } else {
+ width = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ }
+ if (!primaryNode.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)) {
+ height = VALUE_WRAP_CONTENT;
+ } else {
+ height = primaryNode.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+ }
+ }
+ if (width != null) {
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_LAYOUT_WIDTH);
+ sb.append('=').append('"');
+ sb.append(DomUtilities.toXmlAttributeValue(width));
+ sb.append('"');
+ }
+ if (height != null) {
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(ATTR_LAYOUT_HEIGHT);
+ sb.append('=').append('"');
+ sb.append(DomUtilities.toXmlAttributeValue(height));
+ sb.append('"');
+ }
+
+ // Duplicate all the other layout attributes as well
+ if (primaryNode != null) {
+ NamedNodeMap attributes = primaryNode.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attr = attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_PREFIX)
+ && ANDROID_URI.equals(attr.getNamespaceURI())) {
+ if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Already handled
+ continue;
+ }
+
+ sb.append(' ');
+ sb.append(androidNsPrefix);
+ sb.append(':');
+ sb.append(name);
+ sb.append('=').append('"');
+ sb.append(DomUtilities.toXmlAttributeValue(attr.getNodeValue()));
+ sb.append('"');
+ }
+ }
+ }
+
+ sb.append("/>");
+ return sb.toString();
+ }
+
+ /** Return the text in the document in the range start to end */
+ private String getExtractedText() {
+ String xml = getText(mSelectionStart, mSelectionEnd);
+ Element primaryNode = getPrimaryElement();
+ xml = stripTopLayoutAttributes(primaryNode, mSelectionStart, xml);
+ xml = dedent(xml);
+
+ // Wrap siblings in <merge>?
+ if (primaryNode == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<merge>\n"); //$NON-NLS-1$
+ // indent an extra level
+ for (String line : xml.split("\n")) { //$NON-NLS-1$
+ sb.append(" "); //$NON-NLS-1$
+ sb.append(line).append('\n');
+ }
+ sb.append("</merge>\n"); //$NON-NLS-1$
+ xml = sb.toString();
+ }
+
+ return xml;
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.extract.include", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new ExtractIncludeRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java
new file mode 100644
index 0000000..0e3314e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/ExtractIncludeWizard.java
@@ -0,0 +1,135 @@
+/*
+ * 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.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.ResourceNameValidator;
+import com.android.resources.ResourceType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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;
+
+class ExtractIncludeWizard extends RefactoringWizard {
+ private final IProject mProject;
+
+ public ExtractIncludeWizard(ExtractIncludeRefactoring ref, IProject project) {
+ super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mProject = project;
+ setDefaultPageTitle(ref.getName());
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ ExtractIncludeRefactoring ref = (ExtractIncludeRefactoring) getRefactoring();
+ String initialName = ref.getInitialName();
+ addPage(new InputPage(mProject, initialName));
+ }
+
+ /** Wizard page which inputs parameters for the {@link ExtractIncludeRefactoring} operation */
+ private static class InputPage extends UserInputWizardPage {
+ private final IProject mProject;
+ private final String mSuggestedName;
+ private Text mNameText;
+ private Button mUpdateReferences;
+ private Button mReplaceAllOccurrences;
+
+ public InputPage(IProject project, String suggestedName) {
+ super("ExtractIncludeInputPage"); //$NON-NLS-1$
+ mProject = project;
+ mSuggestedName = suggestedName;
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label nameLabel = new Label(composite, SWT.NONE);
+ nameLabel.setText("New Layout Name:");
+ nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+
+ mNameText = new Text(composite, SWT.BORDER);
+ mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mNameText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ });
+
+ mUpdateReferences = new Button(composite, SWT.CHECK);
+ mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mUpdateReferences.setSelection(true);
+ mUpdateReferences.setText("Update layout references");
+
+ mReplaceAllOccurrences = new Button(composite, SWT.CHECK);
+ mReplaceAllOccurrences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mReplaceAllOccurrences.setText("Replace all occurrences with include to new layout");
+ mReplaceAllOccurrences.setEnabled(false);
+
+ // Initialize UI:
+ if (mSuggestedName != null) {
+ mNameText.setText(mSuggestedName);
+ }
+
+ setControl(composite);
+ validatePage();
+ }
+
+ private boolean validatePage() {
+ boolean ok = true;
+
+ String text = mNameText.getText().trim();
+
+ if (text.length() == 0) {
+ setErrorMessage("Provide a name for the new layout");
+ ok = false;
+ } else {
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
+ ResourceType.LAYOUT);
+ String message = validator.isValid(text);
+ if (message != null) {
+ setErrorMessage(message);
+ ok = false;
+ }
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ ExtractIncludeRefactoring refactoring =
+ (ExtractIncludeRefactoring) getRefactoring();
+ refactoring.setLayoutName(text);
+ refactoring.setReplaceOccurrences(mReplaceAllOccurrences.getSelection());
+ refactoring.setUpdateReferences(mUpdateReferences.getSelection());
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+}
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
new file mode 100644
index 0000000..cae49ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoring.java
@@ -0,0 +1,919 @@
+/*
+ * 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.layout.refactoring;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_NS_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX;
+import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS;
+import static com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor.XMLNS_COLON;
+
+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.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite;
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
+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.gle2.GraphicalEditorPart;
+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.preferences.AdtPrefs;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+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.core.runtime.Path;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
+import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
+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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Parent class for the various visual refactoring operations; contains shared
+ * implementations needed by most of them
+ */
+@SuppressWarnings("restriction") // XML model
+public abstract class VisualRefactoring extends Refactoring {
+ protected static final String KEY_FILE = "file"; //$NON-NLS-1$
+ protected static final String KEY_PROJECT = "proj"; //$NON-NLS-1$
+ protected static final String KEY_SEL_START = "sel-start"; //$NON-NLS-1$
+ protected static final String KEY_SEL_END = "sel-end"; //$NON-NLS-1$
+
+ protected IFile mFile;
+ protected LayoutEditor mEditor;
+ protected IProject mProject;
+ protected int mSelectionStart = -1;
+ protected int mSelectionEnd = -1;
+ protected List<Element> mElements = null;
+ protected ITreeSelection mTreeSelection;
+ protected ITextSelection mSelection;
+ protected List<Change> mChanges;
+ private String mAndroidNamespacePrefix;
+
+ /**
+ * This constructor is solely used by {@link VisualRefactoringDescriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ VisualRefactoring(Map<String, String> arguments) {
+ IPath path = Path.fromPortableString(arguments.get(KEY_PROJECT));
+ mProject = (IProject) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ path = Path.fromPortableString(arguments.get(KEY_FILE));
+ mFile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(path);
+ mSelectionStart = Integer.parseInt(arguments.get(KEY_SEL_START));
+ mSelectionEnd = Integer.parseInt(arguments.get(KEY_SEL_END));
+ mEditor = null;
+ }
+
+ public VisualRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ mFile = file;
+ mEditor = editor;
+ mProject = file.getProject();
+ mSelection = selection;
+ mTreeSelection = treeSelection;
+
+ // Initialize mSelectionStart and mSelectionEnd based on the selection context, which
+ // is either a treeSelection (when invoked from the layout editor or the outline), or
+ // a selection (when invoked from an XML editor)
+ if (treeSelection != null) {
+ int end = Integer.MIN_VALUE;
+ int start = Integer.MAX_VALUE;
+ for (TreePath path : treeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ CanvasViewInfo viewInfo = (CanvasViewInfo) lastSegment;
+ UiViewElementNode uiNode = viewInfo.getUiViewNode();
+ if (uiNode == null) {
+ continue;
+ }
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xmlNode;
+
+ start = Math.min(start, region.getStartOffset());
+ end = Math.max(end, region.getEndOffset());
+ }
+ }
+ }
+ if (start >= 0) {
+ mSelectionStart = start;
+ mSelectionEnd = end;
+ }
+ } else if (selection != null) {
+ // TODO: update selection to boundaries!
+ mSelectionStart = selection.getOffset();
+ mSelectionEnd = mSelectionStart + selection.getLength();
+ }
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to extract");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ // TODO - don't do this if we based the selection on text. In this case,
+ // make sure we're -balanced-.
+
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ infos.add((CanvasViewInfo) lastSegment);
+ }
+ }
+
+ if (infos.size() == 0) {
+ status.addFatalError("No selection to extract");
+ return status;
+ }
+
+ // Can't extract the root -- wouldn't that be pointless? (or maybe not
+ // always)
+ for (CanvasViewInfo info : infos) {
+ if (info.isRoot()) {
+ status.addFatalError("Cannot refactor the root");
+ return status;
+ }
+ }
+
+ // Disable if you've selected a single include tag
+ if (infos.size() == 1) {
+ UiViewElementNode uiNode = infos.get(0).getUiViewNode();
+ if (uiNode != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode.getLocalName().equals(LayoutDescriptors.VIEW_INCLUDE)) {
+ status.addWarning("No point in refactoring a single include tag");
+ }
+ }
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (infos.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
+ .size());
+ for (CanvasViewInfo info : infos) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ if (nodes.size() == 0) {
+ status.addFatalError("No selected views");
+ return status;
+ }
+
+ UiElementNode parent = nodes.get(0).getUiParent();
+ for (UiViewElementNode node : nodes) {
+ if (parent != node.getUiParent()) {
+ status.addFatalError("The selected elements must be adjacent");
+ return status;
+ }
+ }
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<UiElementNode> siblings = parent.getUiChildren();
+ if (siblings.size() != nodes.size()) {
+ Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
+ boolean inRange = false;
+ int remaining = nodes.size();
+ for (UiElementNode node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ status.addFatalError("The selected elements must be adjacent");
+ return status;
+ }
+ }
+ }
+ }
+ }
+
+ // This also ensures that we have a valid DOM model:
+ mElements = getElements();
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to extract");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ protected abstract List<Change> computeChanges();
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+ mChanges = new ArrayList<Change>();
+ try {
+ monitor.beginTask("Checking post-conditions...", 5);
+ List<Change> changes = computeChanges();
+ mChanges.addAll(changes);
+
+ monitor.worked(1);
+ } finally {
+ monitor.done();
+ }
+
+ return status;
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor monitor) throws CoreException,
+ OperationCanceledException {
+ try {
+ monitor.beginTask("Applying changes...", 1);
+
+ CompositeChange change = new CompositeChange(
+ getName(),
+ mChanges.toArray(new Change[mChanges.size()])) {
+ @Override
+ public ChangeDescriptor getDescriptor() {
+ VisualRefactoringDescriptor desc = createDescriptor();
+ return new RefactoringChangeDescriptor(desc);
+ }
+ };
+
+ monitor.worked(1);
+ return change;
+
+ } finally {
+ monitor.done();
+ }
+ }
+
+ protected abstract VisualRefactoringDescriptor createDescriptor();
+
+ protected Map<String, String> createArgumentMap() {
+ HashMap<String, String> args = new HashMap<String, String>();
+ args.put(KEY_PROJECT, mProject.getFullPath().toPortableString());
+ args.put(KEY_FILE, mFile.getFullPath().toPortableString());
+ args.put(KEY_SEL_START, Integer.toString(mSelectionStart));
+ args.put(KEY_SEL_END, Integer.toString(mSelectionEnd));
+
+ return args;
+ }
+
+ // ---- Shared functionality ----
+
+
+ protected void openFile(IFile file) {
+ GraphicalEditorPart graphicalEditor = mEditor.getGraphicalEditor();
+ IFile leavingFile = graphicalEditor.getEditedFile();
+
+ try {
+ // Duplicate the current state into the newly created file
+ QualifiedName qname = ConfigurationComposite.NAME_CONFIG_STATE;
+ String state = AdtPlugin.getFileProperty(leavingFile, qname);
+
+ // TODO: Look for a ".NoTitleBar.Fullscreen" theme version of the current
+ // theme to show.
+
+ file.setSessionProperty(GraphicalEditorPart.NAME_INITIAL_STATE, state);
+ } catch (CoreException e) {
+ // pass
+ }
+
+ /* TBD: "Show Included In" if supported.
+ * Not sure if this is a good idea.
+ if (graphicalEditor.renderingSupports(Capability.EMBEDDED_LAYOUT)) {
+ try {
+ Reference include = Reference.create(graphicalEditor.getEditedFile());
+ file.setSessionProperty(GraphicalEditorPart.NAME_INCLUDE, include);
+ } catch (CoreException e) {
+ // pass - worst that can happen is that we don't start with inclusion
+ }
+ }
+ */
+
+ try {
+ IEditorPart part = IDE.openEditor(mEditor.getEditorSite().getPage(), file);
+ if (part instanceof AndroidXmlEditor && AdtPrefs.getPrefs().getFormatXml()) {
+ AndroidXmlEditor newEditor = (AndroidXmlEditor) part;
+ newEditor.reformatDocument();
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open new included layout");
+ }
+ }
+
+
+ /** Produce a list of edits to replace references to the given id with the given new id */
+ protected List<TextEdit> replaceIds(IStructuredDocument doc, int skipStart, int skipEnd,
+ String rootId, String referenceId) {
+ if (rootId == null) {
+ return Collections.emptyList();
+ }
+
+ // We need to search for either @+id/ or @id/
+ String match1 = rootId;
+ String match2;
+ if (match1.startsWith(ID_PREFIX)) {
+ match2 = '"' + NEW_ID_PREFIX + match1.substring(ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else if (match1.startsWith(NEW_ID_PREFIX)) {
+ match2 = '"' + ID_PREFIX + match1.substring(NEW_ID_PREFIX.length()) + '"';
+ match1 = '"' + match1 + '"';
+ } else {
+ return Collections.emptyList();
+ }
+
+ String namePrefix = getAndroidNamespacePrefix() + ':' + ATTR_LAYOUT_PREFIX;
+ List<TextEdit> edits = new ArrayList<TextEdit>();
+
+ IStructuredDocumentRegion region = doc.getFirstStructuredDocumentRegion();
+ for (; region != null; region = region.getNext()) {
+ ITextRegionList list = region.getRegions();
+ int regionStart = region.getStart();
+
+ // Look at all attribute values and look for an id reference match
+ String attributeName = ""; //$NON-NLS-1$
+ for (int j = 0; j < region.getNumberOfRegions(); j++) {
+ ITextRegion subRegion = list.get(j);
+ String type = subRegion.getType();
+ if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
+ attributeName = region.getText(subRegion);
+ } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
+ // Only replace references in layout attributes
+ if (!attributeName.startsWith(namePrefix)) {
+ continue;
+ }
+ // Skip occurrences in the given skip range
+ int subRegionStart = regionStart + subRegion.getStart();
+ if (subRegionStart >= skipStart && subRegionStart <= skipEnd) {
+ continue;
+ }
+
+ String attributeValue = region.getText(subRegion);
+ if (attributeValue.equals(match1) || attributeValue.equals(match2)) {
+ int start = subRegionStart + 1; // skip quote
+ int end = start + rootId.length();
+
+ edits.add(new ReplaceEdit(start, end - start, referenceId));
+ }
+ }
+ }
+ }
+
+ return edits;
+ }
+
+ /** Get the id of the root selected element, if any */
+ protected String getRootId() {
+ Element primary = getPrimaryElement();
+ if (primary != null) {
+ String oldId = primary.getAttributeNS(ANDROID_URI, ATTR_ID);
+ // id null check for https://bugs.eclipse.org/bugs/show_bug.cgi?id=272378
+ if (oldId != null && oldId.length() > 0) {
+ return oldId;
+ }
+ }
+
+ return null;
+ }
+
+ protected String getAndroidNamespacePrefix() {
+ if (mAndroidNamespacePrefix == null) {
+ List<Attr> attributeNodes = findNamespaceAttributes();
+ for (Node attributeNode : attributeNodes) {
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ String name = attributeNode.getNodeName();
+ String value = attributeNode.getNodeValue();
+ if (value.equals(ANDROID_URI)) {
+ mAndroidNamespacePrefix = name;
+ if (mAndroidNamespacePrefix.startsWith(XMLNS_COLON)) {
+ mAndroidNamespacePrefix =
+ mAndroidNamespacePrefix.substring(XMLNS_COLON.length());
+ }
+ }
+ }
+ }
+
+ if (mAndroidNamespacePrefix == null) {
+ mAndroidNamespacePrefix = ANDROID_NS_PREFIX;
+ }
+ }
+
+ return mAndroidNamespacePrefix;
+ }
+
+ protected List<Attr> findNamespaceAttributes() {
+ Document document = getDomDocument();
+ if (document != null) {
+ Element root = document.getDocumentElement();
+ return findNamespaceAttributes(root);
+ }
+
+ return Collections.emptyList();
+ }
+
+ protected List<Attr> findNamespaceAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String prefix = attributeNode.getPrefix();
+ if (XMLNS.equals(prefix)) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected List<Attr> findLayoutAttributes(Node root) {
+ List<Attr> result = new ArrayList<Attr>();
+ NamedNodeMap attributes = root.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attributeNode = attributes.item(i);
+
+ String name = attributeNode.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_PREFIX)
+ && ANDROID_URI.equals(attributeNode.getNamespaceURI())) {
+ result.add((Attr) attributeNode);
+ }
+ }
+
+ return result;
+ }
+
+ protected String insertNamespace(String xmlText, String namespaceDeclarations) {
+ // Insert namespace declarations into the extracted XML fragment
+ int firstSpace = xmlText.indexOf(' ');
+ int elementEnd = xmlText.indexOf('>');
+ int insertAt;
+ if (firstSpace != -1 && firstSpace < elementEnd) {
+ insertAt = firstSpace;
+ } else {
+ insertAt = elementEnd;
+ }
+ xmlText = xmlText.substring(0, insertAt) + namespaceDeclarations
+ + xmlText.substring(insertAt);
+
+ return xmlText;
+ }
+
+ /** Remove sections of the document that correspond to top level layout attributes;
+ * these are placed on the include element instead */
+ protected String stripTopLayoutAttributes(Element primary, int start, String xml) {
+ if (primary != null) {
+ // List of attributes to remove
+ List<IndexedRegion> skip = new ArrayList<IndexedRegion>();
+ NamedNodeMap attributes = primary.getAttributes();
+ for (int i = 0, n = attributes.getLength(); i < n; i++) {
+ Node attr = attributes.item(i);
+ String name = attr.getLocalName();
+ if (name.startsWith(ATTR_LAYOUT_PREFIX)
+ && ANDROID_URI.equals(attr.getNamespaceURI())) {
+ if (name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT)) {
+ // These are special and are left in
+ continue;
+ }
+
+ if (attr instanceof IndexedRegion) {
+ skip.add((IndexedRegion) attr);
+ }
+ }
+ }
+ if (skip.size() > 0) {
+ Collections.sort(skip, new Comparator<IndexedRegion>() {
+ // Sort in start order
+ public int compare(IndexedRegion r1, IndexedRegion r2) {
+ return r1.getStartOffset() - r2.getStartOffset();
+ }
+ });
+
+ // Successively cut out the various layout attributes
+ // TODO remove adjacent whitespace too (but not newlines, unless they
+ // are newly adjacent)
+ StringBuilder sb = new StringBuilder(xml.length());
+ int nextStart = 0;
+
+ // Copy out all the sections except the skip sections
+ for (IndexedRegion r : skip) {
+ int regionStart = r.getStartOffset();
+ // Adjust to string offsets since we've copied the string out of
+ // the document
+ regionStart -= start;
+
+ sb.append(xml.substring(nextStart, regionStart));
+
+ nextStart = regionStart + r.getLength();
+ }
+ if (nextStart < xml.length()) {
+ sb.append(xml.substring(nextStart));
+ }
+
+ return sb.toString();
+ }
+ }
+
+ return xml;
+ }
+
+ protected static String getIndent(String line, int max) {
+ int i = 0;
+ int n = Math.min(max, line.length());
+ for (; i < n; i++) {
+ char c = line.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ return line.substring(0, i);
+ }
+ }
+
+ if (n < line.length()) {
+ return line.substring(0, n);
+ } else {
+ return line;
+ }
+ }
+
+ protected static String dedent(String xml) {
+ String[] lines = xml.split("\n"); //$NON-NLS-1$
+ if (lines.length < 2) {
+ // The first line never has any indentation since we copy it out from the
+ // element start index
+ return xml;
+ }
+
+ String indentPrefix = getIndent(lines[1], lines[1].length());
+ for (int i = 2, n = lines.length; i < n; i++) {
+ String line = lines[i];
+
+ // Ignore blank lines
+ if (line.trim().length() == 0) {
+ continue;
+ }
+
+ indentPrefix = getIndent(line, indentPrefix.length());
+
+ if (indentPrefix.length() == 0) {
+ return xml;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (String line : lines) {
+ if (line.startsWith(indentPrefix)) {
+ sb.append(line.substring(indentPrefix.length()));
+ } else {
+ sb.append(line);
+ }
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ protected String getText(int start, int end) {
+ try {
+ IStructuredDocument document = mEditor.getStructuredDocument();
+ return document.get(start, end - start);
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ return null;
+ }
+ }
+
+ protected List<Element> getElements() {
+ if (mElements == null) {
+ List<Element> nodes = new ArrayList<Element>();
+
+ AndroidXmlEditor editor = mEditor;
+ IStructuredDocument doc = editor.getStructuredDocument();
+ Pair<Element, Element> range = DomUtilities.getElementRange(doc,
+ mSelectionStart, mSelectionEnd);
+ if (range != null) {
+ Element first = range.getFirst();
+ Element last = range.getSecond();
+
+ if (first == last) {
+ nodes.add(first);
+ } else if (first.getParentNode() == last.getParentNode()) {
+ // Add the range
+ Node node = first;
+ while (node != null) {
+ if (node instanceof Element) {
+ nodes.add((Element) node);
+ }
+ if (node == last) {
+ break;
+ }
+ node = node.getNextSibling();
+ }
+ } else {
+ // Different parents: this means we have an uneven selection, selecting
+ // elements from different levels. We can't extract ranges like that.
+ }
+ }
+ mElements = nodes;
+ }
+
+ return mElements;
+ }
+
+ protected Element getPrimaryElement() {
+ List<Element> elements = getElements();
+ if (elements != null && elements.size() == 1) {
+ return elements.get(0);
+ }
+
+ return null;
+ }
+
+ protected Document getDomDocument() {
+ return mEditor.getUiRootNode().getXmlDocument();
+ }
+
+ protected List<CanvasViewInfo> getSelectedViewInfos() {
+ List<CanvasViewInfo> infos = new ArrayList<CanvasViewInfo>();
+ if (mTreeSelection != null) {
+ for (TreePath path : mTreeSelection.getPaths()) {
+ Object lastSegment = path.getLastSegment();
+ if (lastSegment instanceof CanvasViewInfo) {
+ infos.add((CanvasViewInfo) lastSegment);
+ }
+ }
+ }
+ return infos;
+ }
+
+ protected boolean validateNotEmpty(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() == 0) {
+ status.addFatalError("No selection to extract");
+ return false;
+ }
+
+ return true;
+ }
+
+ protected boolean validateNotRoot(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ for (CanvasViewInfo info : infos) {
+ if (info.isRoot()) {
+ status.addFatalError("Cannot refactor the root");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected boolean validateContiguous(List<CanvasViewInfo> infos, RefactoringStatus status) {
+ if (infos.size() > 1) {
+ // All elements must be siblings (e.g. same parent)
+ List<UiViewElementNode> nodes = new ArrayList<UiViewElementNode>(infos
+ .size());
+ for (CanvasViewInfo info : infos) {
+ UiViewElementNode node = info.getUiViewNode();
+ if (node != null) {
+ nodes.add(node);
+ }
+ }
+ if (nodes.size() == 0) {
+ status.addFatalError("No selected views");
+ return false;
+ }
+
+ UiElementNode parent = nodes.get(0).getUiParent();
+ for (UiViewElementNode node : nodes) {
+ if (parent != node.getUiParent()) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ // Ensure that the siblings are contiguous; no gaps.
+ // If we've selected all the children of the parent then we don't need
+ // to look.
+ List<UiElementNode> siblings = parent.getUiChildren();
+ if (siblings.size() != nodes.size()) {
+ Set<UiViewElementNode> nodeSet = new HashSet<UiViewElementNode>(nodes);
+ boolean inRange = false;
+ int remaining = nodes.size();
+ for (UiElementNode node : siblings) {
+ boolean in = nodeSet.contains(node);
+ if (in) {
+ remaining--;
+ if (remaining == 0) {
+ break;
+ }
+ inRange = true;
+ } else if (inRange) {
+ status.addFatalError("The selected elements must be adjacent");
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ protected IndexedRegion getRegion(Node node) {
+ if (node instanceof IndexedRegion) {
+ return (IndexedRegion) node;
+ }
+
+ return null;
+ }
+
+ protected String ensureHasId(MultiTextEdit rootEdit, Element element) {
+ if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID)) {
+ String id = DomUtilities.getFreeWidgetId(element);
+ id = NEW_ID_PREFIX + id;
+ addAttributeDeclaration(rootEdit, element, getAndroidNamespacePrefix(), ATTR_ID, id);
+ return id;
+ }
+
+ return getId(element);
+ }
+
+ protected int getFirstAttributeOffset(Element element) {
+ IndexedRegion region = getRegion(element);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ String name = element.getLocalName();
+ int nameOffset = text.indexOf(name);
+ if (nameOffset != -1) {
+ return startOffset + nameOffset + name.length();
+ }
+ }
+
+ return -1;
+ }
+
+ protected static String getId(Element element) {
+ return element.getAttributeNS(ANDROID_URI, ATTR_ID);
+ }
+
+ protected String ensureNewId(String id) {
+ if (id != null && id.length() > 0) {
+ if (id.startsWith(ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id.substring(ID_PREFIX.length());
+ } else if (!id.startsWith(NEW_ID_PREFIX)) {
+ id = NEW_ID_PREFIX + id;
+ }
+ } else {
+ id = null;
+ }
+
+ return id;
+ }
+
+ protected String getViewClass(String fqcn) {
+ // Don't include android.widget. as a package prefix in layout files
+ if (fqcn.startsWith(ANDROID_WIDGET_PREFIX)) {
+ fqcn = fqcn.substring(ANDROID_WIDGET_PREFIX.length());
+ }
+
+ return fqcn;
+ }
+
+ protected void addAttributeDeclaration(MultiTextEdit rootEdit, Element element,
+ String attributePrefix, String attributeName, String attributeValue) {
+ int offset = getFirstAttributeOffset(element);
+ if (offset != -1) {
+ addAttributeDeclaration(rootEdit, offset, attributePrefix, attributeName,
+ attributeValue);
+ }
+ }
+
+ protected void addAttributeDeclaration(MultiTextEdit rootEdit, int offset,
+ String attributePrefix, String attributeName, String attributeValue) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(' ').append(attributePrefix).append(':');
+ sb.append(attributeName).append('=').append('"');
+ sb.append(attributeValue).append('"');
+
+ InsertEdit setAttribute = new InsertEdit(offset, sb.toString());
+ rootEdit.addChild(setAttribute);
+ }
+
+ /** Strips out the given attribute, if defined */
+ protected void removeAttribute(MultiTextEdit rootEdit, Element element, String uri,
+ String attributeName) {
+ if (element.hasAttributeNS(uri, attributeName)) {
+ Attr attribute = element.getAttributeNodeNS(uri, attributeName);
+ IndexedRegion region = getRegion(attribute);
+ if (region != null) {
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ rootEdit.addChild(deletion);
+ }
+ }
+ }
+
+ public abstract static class VisualRefactoringDescriptor extends RefactoringDescriptor {
+ private final Map<String, String> mArguments;
+
+ public VisualRefactoringDescriptor(
+ String id, String project, String description, String comment,
+ Map<String, String> arguments) {
+ super(id, project, description, comment, STRUCTURAL_CHANGE | MULTI_CHANGE);
+ mArguments = arguments;
+ }
+
+ public Map<String, String> getArguments() {
+ return mArguments;
+ }
+
+ protected abstract Refactoring createRefactoring(Map<String, String> args);
+
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ try {
+ return createRefactoring(mArguments);
+ } catch (NullPointerException e) {
+ status.addFatalError("Failed to recreate refactoring from descriptor");
+ return null;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java
new file mode 100644
index 0000000..e175da2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/VisualRefactoringAction.java
@@ -0,0 +1,185 @@
+/*
+ * 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.layout.refactoring;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidConstants;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.FileEditorInput;
+
+abstract class VisualRefactoringAction implements IWorkbenchWindowActionDelegate {
+ protected IWorkbenchWindow mWindow;
+ protected ITextSelection mTextSelection;
+ protected ITreeSelection mTreeSelection;
+ protected LayoutEditor mEditor;
+ protected IFile mFile;
+
+ /**
+ * Keep track of the current workbench window.
+ */
+ public void init(IWorkbenchWindow window) {
+ mWindow = window;
+ }
+
+ public void dispose() {
+ }
+
+ /**
+ * Examine the selection to determine if the action should be enabled or not.
+ * <p/>
+ * Keep a link to the relevant selection structure (i.e. a part of the Java AST).
+ */
+ public void selectionChanged(IAction action, ISelection selection) {
+ // Look for selections in XML and in the layout UI editor
+
+ // Note, two kinds of selections are returned here:
+ // ITextSelection on a Java source window
+ // IStructuredSelection in the outline or navigator
+ // This simply deals with the refactoring based on a non-empty selection.
+ // At that point, just enable the action and later decide if it's valid when it actually
+ // runs since we don't have access to the AST yet.
+
+ mTextSelection = null;
+ mTreeSelection = null;
+ mFile = null;
+
+ IEditorPart editor = null;
+
+ if (selection instanceof ITextSelection) {
+ mTextSelection = (ITextSelection) selection;
+ if (mTextSelection.getLength() > 0) {
+ editor = getActiveEditor();
+ mFile = getSelectedFile(editor);
+ }
+ } else if (selection instanceof ITreeSelection) {
+ Object firstElement = ((ITreeSelection)selection).getFirstElement();
+ if (firstElement instanceof CanvasViewInfo) {
+ mTreeSelection = (ITreeSelection) selection;
+ editor = getActiveEditor();
+ mFile = getSelectedFile(editor);
+ }
+ }
+
+
+ if (editor instanceof LayoutEditor) {
+ mEditor = (LayoutEditor) editor;
+ }
+
+ action.setEnabled((mTextSelection != null || mTreeSelection != null)
+ && mFile != null && mEditor != null);
+ }
+
+ /**
+ * Create a new instance of our refactoring and a wizard to configure it.
+ */
+ public abstract void run(IAction action);
+
+ /**
+ * Returns the active editor (hopefully matching our selection) or null.
+ */
+ private IEditorPart getActiveEditor() {
+ IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (wwin != null) {
+ IWorkbenchPage page = wwin.getActivePage();
+ if (page != null) {
+ return page.getActiveEditor();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the active {@link IFile} (hopefully matching our selection) or null.
+ * The file is only returned if it's a file from a project with an Android nature.
+ * <p/>
+ * At that point we do not try to analyze if the selection nor the file is suitable
+ * for the refactoring. This check is performed when the refactoring is invoked since
+ * it can then produce meaningful error messages as needed.
+ */
+ private IFile getSelectedFile(IEditorPart editor) {
+ if (editor != null) {
+ IEditorInput input = editor.getEditorInput();
+
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fi = (FileEditorInput) input;
+ IFile file = fi.getFile();
+ if (file.exists()) {
+ IProject proj = file.getProject();
+ try {
+ if (proj != null && proj.hasNature(AndroidConstants.NATURE_DEFAULT)) {
+ return file;
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static IAction create(String title, LayoutEditor editor,
+ Class<? extends VisualRefactoringAction> clz) {
+ return new ActionWrapper(title, editor, clz);
+ }
+
+ private static class ActionWrapper extends Action {
+ private Class<? extends VisualRefactoringAction> mClass;
+ private LayoutEditor mEditor;
+
+ ActionWrapper(String title, LayoutEditor editor,
+ Class<? extends VisualRefactoringAction> clz) {
+ super(title);
+ mEditor = editor;
+ mClass = clz;
+ }
+
+ @Override
+ public void run() {
+ VisualRefactoringAction action;
+ try {
+ action = mClass.newInstance();
+ } catch (Exception e) {
+ AdtPlugin.log(e, null);
+ return;
+ }
+ action.init(mEditor.getEditorSite().getWorkbenchWindow());
+ ISelection selection = mEditor.getEditorSite().getSelectionProvider().getSelection();
+ action.selectionChanged(ActionWrapper.this, selection);
+ if (isEnabled()) {
+ action.run(ActionWrapper.this);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java
new file mode 100644
index 0000000..865e406
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.layout.refactoring;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
+
+/**
+ * Action executed when the "Wrap In" menu item is invoked.
+ */
+public class WrapInAction extends VisualRefactoringAction {
+ @Override
+ public void run(IAction action) {
+ if ((mTextSelection != null || mTreeSelection != null) && mFile != null) {
+ WrapInRefactoring ref = new WrapInRefactoring(mFile, mEditor,
+ mTextSelection, mTreeSelection);
+ RefactoringWizard wizard = new WrapInWizard(ref, mFile.getProject());
+ RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
+ try {
+ op.run(mWindow.getShell(), wizard.getDefaultPageTitle());
+ } catch (InterruptedException e) {
+ // Interrupted. Pass.
+ }
+ }
+ }
+
+ public static IAction create(LayoutEditor editor) {
+ return create("Wrap in Container...", editor, WrapInAction.class);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java
new file mode 100644
index 0000000..61d7987
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInContribution.java
@@ -0,0 +1,40 @@
+/*
+ * 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.layout.refactoring;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+
+import java.util.Map;
+
+public class WrapInContribution extends RefactoringContribution {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description,
+ String comment, Map arguments, int flags) throws IllegalArgumentException {
+ return new WrapInRefactoring.Descriptor(project, description, comment, arguments);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof WrapInRefactoring.Descriptor) {
+ return ((WrapInRefactoring.Descriptor) descriptor).getArguments();
+ }
+ return super.retrieveArgumentMap(descriptor);
+ }
+}
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
new file mode 100644
index 0000000..0aea363
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInRefactoring.java
@@ -0,0 +1,387 @@
+/*
+ * 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.layout.refactoring;
+
+import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT;
+import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT;
+import static com.android.ide.eclipse.adt.AndroidConstants.EXT_XML;
+
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
+
+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.ITextSelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.TextEdit;
+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.w3c.dom.Attr;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Inserts a new layout surrounding the current selection, migrates namespace
+ * attributes (if wrapping the root node), and optionally migrates layout
+ * attributes and updates references elsewhere.
+ */
+@SuppressWarnings("restriction") // XML model
+public class WrapInRefactoring extends VisualRefactoring {
+ private static final String KEY_ID = "name"; //$NON-NLS-1$
+ private static final String KEY_TYPE = "type"; //$NON-NLS-1$
+ private static final String KEY_UPDATE_REFS = "update-refs"; //$NON-NLS-1$
+
+ private String mId;
+ private String mTypeFqcn;
+ private boolean mUpdateReferences;
+
+ /**
+ * This constructor is solely used by {@link Descriptor},
+ * to replay a previous refactoring.
+ * @param arguments argument map created by #createArgumentMap.
+ */
+ WrapInRefactoring(Map<String, String> arguments) {
+ super(arguments);
+ mId = arguments.get(KEY_ID);
+ mTypeFqcn = arguments.get(KEY_TYPE);
+ mUpdateReferences = Boolean.parseBoolean(arguments.get(KEY_UPDATE_REFS));
+ }
+
+ public WrapInRefactoring(IFile file, LayoutEditor editor, ITextSelection selection,
+ ITreeSelection treeSelection) {
+ super(file, editor, selection, treeSelection);
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException,
+ OperationCanceledException {
+ RefactoringStatus status = new RefactoringStatus();
+
+ try {
+ pm.beginTask("Checking preconditions...", 6);
+
+ if (mSelectionStart == -1 || mSelectionEnd == -1) {
+ status.addFatalError("No selection to wrap");
+ return status;
+ }
+
+ // Make sure the selection is contiguous
+ if (mTreeSelection != null) {
+ // TODO - don't do this if we based the selection on text. In this case,
+ // make sure we're -balanced-.
+
+ List<CanvasViewInfo> infos = getSelectedViewInfos();
+ if (!validateNotEmpty(infos, status)) {
+ return status;
+ }
+
+ // Enforce that the selection is -contiguous-
+ if (!validateContiguous(infos, status)) {
+ return status;
+ }
+ }
+
+ // This also ensures that we have a valid DOM model:
+ mElements = getElements();
+ if (mElements.size() == 0) {
+ status.addFatalError("Nothing to wrap");
+ return status;
+ }
+
+ pm.worked(1);
+ return status;
+
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected VisualRefactoringDescriptor createDescriptor() {
+ String comment = getName();
+ return new Descriptor(
+ mProject.getName(), //project
+ comment, //description
+ comment, //comment
+ createArgumentMap());
+ }
+
+ @Override
+ protected Map<String, String> createArgumentMap() {
+ Map<String, String> args = super.createArgumentMap();
+ args.put(KEY_TYPE, mTypeFqcn);
+ args.put(KEY_ID, mId);
+ args.put(KEY_UPDATE_REFS, Boolean.toString(mUpdateReferences));
+
+ return args;
+ }
+
+ @Override
+ public String getName() {
+ return "Wrap in Container";
+ }
+
+ void setId(String id) {
+ mId = id;
+ }
+
+ void setType(String typeFqcn) {
+ mTypeFqcn = typeFqcn;
+ }
+
+ void setUpdateReferences(boolean selection) {
+ mUpdateReferences = selection;
+ }
+
+ @Override
+ protected List<Change> computeChanges() {
+ // (1) Insert the new container in front of the beginning of the
+ // first wrapped view
+ // (2) If the container is the new root, transfer namespace declarations
+ // to it
+ // (3) Insert the closing tag of the new container at the end of the
+ // last wrapped view
+ // (4) Reindent the wrapped views
+ // (5) If the user requested it, update all layout references to the
+ // wrapped views with the new container?
+ // For that matter, does RelativeLayout even require it? Probably not,
+ // it can point inside the current layout...
+
+ // Add indent to all lines between mSelectionStart and mEnd
+ // TODO: Figure out the indentation amount?
+ // For now, use 4 spaces
+ String indentUnit = " "; //$NON-NLS-1$
+ boolean separateAttributes = true;
+ IStructuredDocument document = mEditor.getStructuredDocument();
+ String startIndent = AndroidXmlEditor.getIndentAtOffset(document, mSelectionStart);
+
+ String viewClass = getViewClass(mTypeFqcn);
+
+ IFile file = mEditor.getInputFile();
+ List<Change> changes = new ArrayList<Change>();
+ TextFileChange change = new TextFileChange(file.getName(), file);
+ MultiTextEdit rootEdit = new MultiTextEdit();
+ change.setEdit(rootEdit);
+ change.setTextType(EXT_XML);
+
+ String id = ensureNewId(mId);
+
+ // Update any layout references to the old id with the new id
+ if (mUpdateReferences && id != null) {
+ String rootId = getRootId();
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument doc = model.getStructuredDocument();
+ if (doc != null) {
+ List<TextEdit> replaceIds = replaceIds(doc, mSelectionStart,
+ mSelectionEnd, rootId, id);
+ for (TextEdit edit : replaceIds) {
+ rootEdit.addChild(edit);
+ }
+ }
+ } finally {
+ model.releaseFromRead();
+ }
+ }
+
+ // Insert namespace elements?
+ StringBuilder namespace = null;
+ List<DeleteEdit> deletions = new ArrayList<DeleteEdit>();
+ Element primary = getPrimaryElement();
+ if (primary != null && getDomDocument().getDocumentElement() == primary) {
+ namespace = new StringBuilder();
+
+ List<Attr> declarations = findNamespaceAttributes(primary);
+ for (Attr attribute : declarations) {
+ if (attribute instanceof IndexedRegion) {
+ // Delete the namespace declaration in the node which is no longer the root
+ IndexedRegion region = (IndexedRegion) attribute;
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ deletions.add(deletion);
+ rootEdit.addChild(deletion);
+ text = text.trim();
+
+ // Insert the namespace declaration in the new root
+ if (separateAttributes) {
+ namespace.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ namespace.append(' ');
+ }
+ namespace.append(text);
+ }
+ }
+ }
+
+ // Insert begin tag: <type ...>
+ StringBuilder sb = new StringBuilder();
+ sb.append('<');
+ sb.append(viewClass);
+
+ if (namespace != null) {
+ sb.append(namespace);
+ }
+
+ String androidNsPrefix = getAndroidNamespacePrefix();
+
+ // Set the ID if any
+ if (id != null) {
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_ID).append('=').append('"').append(id).append('"');
+ }
+
+ // If any of the elements are fill/match parent, use that instead
+ String width = VALUE_WRAP_CONTENT;
+ String height = VALUE_WRAP_CONTENT;
+
+ for (Element element : getElements()) {
+ String oldWidth = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+ String oldHeight = element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+
+ if (VALUE_MATCH_PARENT.equals(oldWidth) || VALUE_FILL_PARENT.equals(oldWidth)) {
+ width = oldWidth;
+ }
+ if (VALUE_MATCH_PARENT.equals(oldHeight) || VALUE_FILL_PARENT.equals(oldHeight)) {
+ height = oldHeight;
+ }
+ }
+
+ // Add in width/height.
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_LAYOUT_WIDTH).append('=').append('"').append(width).append('"');
+
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(androidNsPrefix).append(':');
+ sb.append(ATTR_LAYOUT_HEIGHT).append('=').append('"').append(height).append('"');
+
+ // Transfer layout_ attributes (other than width and height)
+ if (mUpdateReferences) {
+ List<Attr> layoutAttributes = findLayoutAttributes(primary);
+ for (Attr attribute : layoutAttributes) {
+ String name = attribute.getLocalName();
+ if ((name.equals(ATTR_LAYOUT_WIDTH) || name.equals(ATTR_LAYOUT_HEIGHT))
+ && ANDROID_URI.equals(attribute.getNamespaceURI())) {
+ // Already handled specially
+ continue;
+ }
+
+ if (attribute instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) attribute;
+ int startOffset = region.getStartOffset();
+ int endOffset = region.getEndOffset();
+ String text = getText(startOffset, endOffset);
+ DeleteEdit deletion = new DeleteEdit(startOffset, endOffset - startOffset);
+ rootEdit.addChild(deletion);
+ deletions.add(deletion);
+
+ if (separateAttributes) {
+ sb.append('\n').append(startIndent).append(indentUnit);
+ } else {
+ sb.append(' ');
+ }
+ sb.append(text.trim());
+ }
+ }
+ }
+
+ // Finish open tag:
+ sb.append('>');
+ sb.append('\n').append(startIndent).append(indentUnit);
+
+ InsertEdit beginEdit = new InsertEdit(mSelectionStart, sb.toString());
+ rootEdit.addChild(beginEdit);
+
+ String nested = getText(mSelectionStart, mSelectionEnd);
+ int index = 0;
+ while (index != -1) {
+ index = nested.indexOf('\n', index);
+ if (index != -1) {
+ index++;
+ InsertEdit newline = new InsertEdit(mSelectionStart + index, indentUnit);
+ // Some of the deleted namespaces may have had newlines - be careful
+ // not to overlap edits
+ boolean covered = false;
+ for (DeleteEdit deletion : deletions) {
+ if (deletion.covers(newline)) {
+ covered = true;
+ break;
+ }
+ }
+ if (!covered) {
+ rootEdit.addChild(newline);
+ }
+ }
+ }
+
+ // Insert end tag: </type>
+ sb.setLength(0);
+ sb.append('\n').append(startIndent);
+ sb.append('<').append('/').append(viewClass).append('>');
+ InsertEdit endEdit = new InsertEdit(mSelectionEnd, sb.toString());
+ rootEdit.addChild(endEdit);
+
+ changes.add(change);
+ return changes;
+ }
+
+ public static class Descriptor extends VisualRefactoringDescriptor {
+ public Descriptor(String project, String description, String comment,
+ Map<String, String> arguments) {
+ super("com.android.ide.eclipse.adt.refactoring.wrapin", //$NON-NLS-1$
+ project, description, comment, arguments);
+ }
+
+ @Override
+ protected Refactoring createRefactoring(Map<String, String> args) {
+ return new WrapInRefactoring(args);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java
new file mode 100644
index 0000000..ee746c3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/WrapInWizard.java
@@ -0,0 +1,226 @@
+/*
+ * 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.layout.refactoring;
+
+import static com.android.ide.common.layout.LayoutConstants.FQCN_LINEAR_LAYOUT;
+
+import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository;
+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.wizards.newxmlfile.ResourceNameValidator;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.android.util.Pair;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
+import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.List;
+
+class WrapInWizard extends RefactoringWizard {
+ private final IProject mProject;
+
+ public WrapInWizard(WrapInRefactoring ref, IProject project) {
+ super(ref, DIALOG_BASED_USER_INTERFACE | PREVIEW_EXPAND_FIRST_NODE);
+ mProject = project;
+ setDefaultPageTitle("Wrap in Container");
+ }
+
+ @Override
+ protected void addUserInputPages() {
+ addPage(new InputPage(mProject));
+ }
+
+ /** Wizard page which inputs parameters for the {@link WrapInRefactoring} operation */
+ private static class InputPage extends UserInputWizardPage {
+ private final IProject mProject;
+ private Text mIdText;
+ private Combo mTypeCombo;
+ private Button mUpdateReferences;
+
+ public InputPage(IProject project) {
+ super("WrapInInputPage"); //$NON-NLS-1$
+ mProject = project;
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(new GridLayout(2, false));
+
+ Label typeLabel = new Label(composite, SWT.NONE);
+ typeLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+ typeLabel.setText("Type of Container:");
+
+ mTypeCombo = new Combo(composite, SWT.READ_ONLY);
+ mTypeCombo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ SelectionAdapter selectionListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ }
+ };
+ mTypeCombo.addSelectionListener(selectionListener);
+
+ Label idLabel = new Label(composite, SWT.NONE);
+ idLabel.setText("New Layout Id:");
+ idLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+
+ mIdText = new Text(composite, SWT.BORDER);
+ mIdText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+ mIdText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ });
+
+ mUpdateReferences = new Button(composite, SWT.CHECK);
+ mUpdateReferences.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
+ false, false, 2, 1));
+ mUpdateReferences.setSelection(true);
+ mUpdateReferences.setText("Update layout references");
+ mUpdateReferences.addSelectionListener(selectionListener);
+
+ addLayouts(mProject, mTypeCombo, null);
+
+ setControl(composite);
+ validatePage();
+
+ mTypeCombo.setFocus();
+ }
+
+ private boolean validatePage() {
+ boolean ok = true;
+
+ String id = mIdText.getText().trim();
+
+ if (id.length() == 0) {
+ // It's okay to not define a title...
+ // ...unless you want to update references
+ if (mUpdateReferences.getSelection()) {
+ setErrorMessage("ID required when updating layout references");
+ ok = false;
+ }
+ } else {
+ // ...but if you do, it has to be valid!
+ ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
+ ResourceType.ID);
+ String message = validator.isValid(id);
+ if (message != null) {
+ setErrorMessage(message);
+ ok = false;
+ }
+ }
+
+ if (mTypeCombo.getText().equals(SEPARATOR_LABEL)) {
+ setErrorMessage("Select a container type");
+ ok = false;
+ }
+
+ if (ok) {
+ setErrorMessage(null);
+
+ // Record state
+ WrapInRefactoring refactoring =
+ (WrapInRefactoring) getRefactoring();
+ refactoring.setId(id);
+ refactoring.setType(mTypeCombo.getText());
+ refactoring.setUpdateReferences(mUpdateReferences.getSelection());
+ }
+
+ setPageComplete(ok);
+ return ok;
+ }
+ }
+
+ static final String SEPARATOR_LABEL =
+ "----------------------------------------"; //$NON-NLS-1$
+
+ static void addLayouts(IProject project, Combo combo, String exclude) {
+ // Populate type combo
+ // TODO 1: Include 3rd party add-ons
+ // TODO 2: Include custom layouts in the project
+ // TODO 3: Only display the basename, and keep track of the full class names
+ // here and associate them back when initializing the type for the user
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ int initialIndex = 0;
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData targetData = currentSdk.getTargetData(target);
+ if (targetData != null) {
+ ViewMetadataRepository repository = ViewMetadataRepository.get();
+ List<Pair<String,List<ViewElementDescriptor>>> entries =
+ repository.getPaletteEntries(targetData, false, true);
+ // Find the layout category - it contains LinearLayout
+ List<ViewElementDescriptor> layoutDescriptors = null;
+
+ search: for (Pair<String,List<ViewElementDescriptor>> pair : entries) {
+ List<ViewElementDescriptor> list = pair.getSecond();
+ for (ViewElementDescriptor d : list) {
+ if (d.getFullClassName().equals(FQCN_LINEAR_LAYOUT)) {
+ // Found - use this list
+ layoutDescriptors = list;
+ break search;
+ }
+ }
+ }
+ if (layoutDescriptors != null) {
+ for (ViewElementDescriptor d : layoutDescriptors) {
+ String className = d.getFullClassName();
+ if (exclude == null || !exclude.equals(className)) {
+ combo.add(className);
+ }
+ }
+
+ // SWT does not support separators in combo boxes
+ combo.add(SEPARATOR_LABEL);
+ }
+
+ // Now add ALL known layout descriptors in case the user has
+ // a special case
+ layoutDescriptors =
+ targetData.getLayoutDescriptors().getLayoutDescriptors();
+
+ for (ViewElementDescriptor d : layoutDescriptors) {
+ String className = d.getFullClassName();
+ if (exclude == null || !exclude.equals(className)) {
+ combo.add(className);
+ }
+ }
+ }
+ }
+ combo.select(initialIndex);
+ } else {
+ combo.add("SDK not initialized");
+ combo.setEnabled(false);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java
index 5ccb494..bc0f36f 100644
--- a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/descriptors/DescriptorsUtilsTest.java
@@ -65,6 +65,7 @@ public class DescriptorsUtilsTest extends TestCase {
assertEquals("Capital", DescriptorsUtils.capitalize("Capital"));
assertEquals("CamelCase", DescriptorsUtils.capitalize("camelCase"));
assertEquals("", DescriptorsUtils.capitalize(""));
+ assertSame("Foo", DescriptorsUtils.capitalize("Foo"));
}
public void testFormatTooltip() {
@@ -239,16 +240,4 @@ public class DescriptorsUtilsTest extends TestCase {
return super.findClass(name);
}
}
-
- public void testToXmlAttributeValue() throws Exception {
- assertEquals("", DescriptorsUtils.toXmlAttributeValue(""));
- assertEquals("foo", DescriptorsUtils.toXmlAttributeValue("foo"));
- assertEquals("foo<bar", DescriptorsUtils.toXmlAttributeValue("foo<bar"));
-
- assertEquals("&quot;", DescriptorsUtils.toXmlAttributeValue("\""));
- assertEquals("&apos;", DescriptorsUtils.toXmlAttributeValue("'"));
- assertEquals("foo&quot;b&apos;&apos;ar",
- DescriptorsUtils.toXmlAttributeValue("foo\"b''ar"));
- }
-
}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java
new file mode 100644
index 0000000..7504bef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DomUtilitiesTest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.layout.gle2;
+
+import junit.framework.TestCase;
+
+public class DomUtilitiesTest extends TestCase {
+
+ public void testToXmlAttributeValue() throws Exception {
+ assertEquals("", DomUtilities.toXmlAttributeValue(""));
+ assertEquals("foo", DomUtilities.toXmlAttributeValue("foo"));
+ assertEquals("foo<bar", DomUtilities.toXmlAttributeValue("foo<bar"));
+
+ assertEquals("&quot;", DomUtilities.toXmlAttributeValue("\""));
+ assertEquals("&apos;", DomUtilities.toXmlAttributeValue("'"));
+ assertEquals("foo&quot;b&apos;&apos;ar",
+ DomUtilities.toXmlAttributeValue("foo\"b''ar"));
+ }
+}